Merge pull request 'ux/qmcv2-keys' (#39) from ux/qmcv2-keys into main
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: #39
This commit is contained in:
周训 2023-07-02 14:55:41 +00:00
commit 8f09882470
11 changed files with 102 additions and 21 deletions

View File

@ -5,7 +5,7 @@ name: default
steps: steps:
- name: test & build - name: test & build
image: node:18.16.0-bullseye image: node:18.16.1-bookworm
commands: commands:
# - git config --global --add safe.directory "/drone/src" # - git config --global --add safe.directory "/drone/src"
- corepack enable - corepack enable
@ -17,7 +17,7 @@ steps:
npm_config_registry: https://registry.npmmirror.com npm_config_registry: https://registry.npmmirror.com
- name: publish - name: publish
image: node:18.16.0-bullseye image: node:18.16.1-bookworm
environment: environment:
DRONE_GITEA_SERVER: https://git.unlock-music.dev DRONE_GITEA_SERVER: https://git.unlock-music.dev
GITEA_API_KEY: GITEA_API_KEY:

View File

@ -28,7 +28,7 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor
<Modal isOpen={show} onClose={onClose} closeOnOverlayClick={false} scrollBehavior="inside" size="xl"> <Modal isOpen={show} onClose={onClose} closeOnOverlayClick={false} scrollBehavior="inside" size="xl">
<ModalOverlay /> <ModalOverlay />
<ModalContent> <ModalContent>
<ModalHeader></ModalHeader> <ModalHeader></ModalHeader>
<ModalCloseButton /> <ModalCloseButton />
<Flex as={ModalBody} gap={2} flexDir="column" flex={1}> <Flex as={ModalBody} gap={2} flexDir="column" flex={1}>
<Center> <Center>

View File

@ -1,4 +1,5 @@
import { import {
chakra,
Box, Box,
Button, Button,
Center, Center,
@ -20,13 +21,15 @@ import {
Text, Text,
VStack, VStack,
useBreakpointValue, useBreakpointValue,
useToast,
} 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';
import { MdExpandMore, MdMenu, MdOutlineSettingsBackupRestore } from 'react-icons/md'; import { MdExpandMore, MdMenu, MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { useAppDispatch } from '~/hooks'; import { useAppDispatch, useAppSelector } from '~/hooks';
import { commitStagingChange, discardStagingChanges } from './settingsSlice'; import { commitStagingChange, discardStagingChanges } from './settingsSlice';
import { PanelKWMv2Key } from './panels/PanelKWMv2Key'; import { PanelKWMv2Key } from './panels/PanelKWMv2Key';
import { selectIsSettingsNotSaved } from './settingsSelector';
const TABS: { name: string; Tab: () => JSX.Element }[] = [ const TABS: { name: string; Tab: () => JSX.Element }[] = [
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key }, { name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
@ -38,6 +41,7 @@ const TABS: { name: string; Tab: () => JSX.Element }[] = [
]; ];
export function Settings() { export function Settings() {
const toast = useToast();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isLargeWidthDevice = const isLargeWidthDevice =
useBreakpointValue({ useBreakpointValue({
@ -49,8 +53,25 @@ export function Settings() {
const handleTabChange = (idx: number) => { const handleTabChange = (idx: number) => {
setTabIndex(idx); setTabIndex(idx);
}; };
const handleResetSettings = () => dispatch(discardStagingChanges()); const handleResetSettings = () => {
const handleApplySettings = () => dispatch(commitStagingChange()); dispatch(discardStagingChanges());
toast({
status: 'info',
title: '未储存的设定已舍弃',
description: '已还原到更改前的状态。',
isClosable: true,
});
};
const handleApplySettings = () => {
dispatch(commitStagingChange());
toast({
status: 'success',
title: '设定已应用',
isClosable: true,
});
};
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
return ( return (
<Flex flexDir="column" flex={1}> <Flex flexDir="column" flex={1}>
@ -104,7 +125,16 @@ export function Settings() {
<VStack mt="4" alignItems="flex-start" w="full"> <VStack mt="4" alignItems="flex-start" w="full">
<Flex flexDir="row" gap="2" w="full"> <Flex flexDir="row" gap="2" w="full">
<Center> <Center>
<Box color="gray"></Box> {isSettingsNotSaved ? (
<Box color="gray">
{' '}
<chakra.span color="red" wordBreak="keep-all">
</chakra.span>
</Box>
) : (
<Box color="gray"></Box>
)}
</Center> </Center>
<Spacer /> <Spacer />
<HStack gap="2" justifyContent="flex-end"> <HStack gap="2" justifyContent="flex-end">

View File

@ -53,7 +53,12 @@ export const KWMv2EKeyItem = memo(({ id, ekey, quality, rid, i }: StagingKWMv2Ke
<InputLeftElement pr="2"> <InputLeftElement pr="2">
<Icon as={MdVpnKey} /> <Icon as={MdVpnKey} />
</InputLeftElement> </InputLeftElement>
<Input variant="flushed" placeholder="密钥" value={ekey} onChange={(e) => updateKey('ekey', e)} /> <Input
variant="flushed"
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
value={ekey}
onChange={(e) => updateKey('ekey', e)}
/>
<InputRightElement> <InputRightElement>
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}> <Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
<code>{ekey.length || '?'}</code> <code>{ekey.length || '?'}</code>

View File

@ -49,8 +49,14 @@ export function PanelKWMv2Key() {
const fileBuffer = await file.arrayBuffer(); const fileBuffer = await file.arrayBuffer();
keys = MMKVParser.parseKuwoEKey(new DataView(fileBuffer)); keys = MMKVParser.parseKuwoEKey(new DataView(fileBuffer));
} }
if (keys?.length === 0) {
if (keys) { toast({
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (keys) {
dispatch(kwm2ImportKeys(keys)); dispatch(kwm2ImportKeys(keys));
setShowImportModal(false); setShowImportModal(false);
toast({ toast({
@ -88,7 +94,7 @@ export function PanelKWMv2Key() {
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton> <MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
<MenuList> <MenuList>
<MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}> <MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}>
</MenuItem> </MenuItem>
<MenuDivider /> <MenuDivider />
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}> <MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>

View File

@ -71,14 +71,20 @@ export function PanelQMCv2Key() {
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey })); qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
} }
if (qmc2Keys) { if (qmc2Keys?.length === 0) {
toast({
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (qmc2Keys) {
dispatch(qmc2ImportKeys(qmc2Keys)); dispatch(qmc2ImportKeys(qmc2Keys));
setShowImportModal(false); setShowImportModal(false);
toast({ toast({
title: `导入成功 (${qmc2Keys.length})`, title: `导入成功 (${qmc2Keys.length})`,
description: '记得保存更改来应用。', description: '记得保存更改来应用。',
isClosable: true, isClosable: true,
duration: 5000,
status: 'success', status: 'success',
}); });
} else { } else {
@ -93,7 +99,7 @@ export function PanelQMCv2Key() {
return ( return (
<Flex minH={0} flexDir="column" flex={1}> <Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg"> <Heading as="h2" size="lg">
QMCv2 QMCv2
</Heading> </Heading>
<Text> <Text>
@ -110,7 +116,7 @@ export function PanelQMCv2Key() {
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton> <MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
<MenuList> <MenuList>
<MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}> <MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}>
</MenuItem> </MenuItem>
<MenuDivider /> <MenuDivider />
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}> <MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
@ -141,6 +147,7 @@ export function PanelQMCv2Key() {
</Text> </Text>
<Text></Text> <Text></Text>
<Text></Text>
</Box> </Box>
} }
> >

View File

@ -30,13 +30,23 @@ export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: st
</Text> </Text>
<VStack flex={1}> <VStack flex={1}>
<Input variant="flushed" placeholder="文件名" value={name} onChange={(e) => updateKey('name', e)} /> <Input
variant="flushed"
placeholder="文件名,包括后缀名。如 “AAA - BBB.mflac”"
value={name}
onChange={(e) => updateKey('name', e)}
/>
<InputGroup size="xs"> <InputGroup size="xs">
<InputLeftElement pr="2"> <InputLeftElement pr="2">
<Icon as={MdVpnKey} /> <Icon as={MdVpnKey} />
</InputLeftElement> </InputLeftElement>
<Input variant="flushed" placeholder="密钥" value={ekey} onChange={(e) => updateKey('ekey', e)} /> <Input
variant="flushed"
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
value={ekey}
onChange={(e) => updateKey('ekey', e)}
/>
<InputRightElement> <InputRightElement>
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}> <Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
<code>{ekey.length || '?'}</code> <code>{ekey.length || '?'}</code>

View File

@ -1,9 +1,11 @@
import { parseKuwoHeader } from '~/crypto/pasreKuwo'; import { parseKuwoHeader } from '~/crypto/parseKuwo';
import type { RootState } from '~/store'; import type { RootState } from '~/store';
import { closestByLevenshtein } from '~/util/levenshtein'; import { closestByLevenshtein } from '~/util/levenshtein';
import { hasOwn } from '~/util/objects'; import { hasOwn } from '~/util/objects';
import { kwm2StagingToProductionKey } from './keyFormats'; import { kwm2StagingToProductionKey } from './keyFormats';
export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty;
export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2; export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2;
export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2; export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2;

View File

@ -37,16 +37,18 @@ export interface ProductionSettings {
} }
export interface SettingsState { export interface SettingsState {
dirty: boolean;
staging: StagingSettings; staging: StagingSettings;
production: ProductionSettings; production: ProductionSettings;
} }
const initialState: SettingsState = { const initialState: SettingsState = {
dirty: false,
staging: { staging: {
qmc2: { allowFuzzyNameSearch: false, keys: [] }, qmc2: { allowFuzzyNameSearch: true, keys: [] },
kwm2: { keys: [] }, kwm2: { keys: [] },
}, },
production: { production: {
qmc2: { allowFuzzyNameSearch: false, keys: {} }, qmc2: { allowFuzzyNameSearch: true, keys: {} },
kwm2: { keys: {} }, kwm2: { keys: {} },
}, },
}; };
@ -77,6 +79,7 @@ export const settingsSlice = createSlice({
reducers: { reducers: {
setProductionChanges: (_state, { payload }: PayloadAction<ProductionSettings>) => { setProductionChanges: (_state, { payload }: PayloadAction<ProductionSettings>) => {
return { return {
dirty: false,
production: payload, production: payload,
staging: productionToStaging(payload), staging: productionToStaging(payload),
}; };
@ -84,14 +87,17 @@ export const settingsSlice = createSlice({
// //
qmc2AddKey(state) { qmc2AddKey(state) {
state.staging.qmc2.keys.push({ id: nanoid(), name: '', ekey: '' }); state.staging.qmc2.keys.push({ id: nanoid(), name: '', ekey: '' });
state.dirty = true;
}, },
qmc2ImportKeys(state, { payload }: PayloadAction<Omit<StagingQMCv2Key, 'id'>[]>) { qmc2ImportKeys(state, { payload }: PayloadAction<Omit<StagingQMCv2Key, 'id'>[]>) {
const newItems = payload.map((item) => ({ id: nanoid(), ...item })); const newItems = payload.map((item) => ({ id: nanoid(), ...item }));
state.staging.qmc2.keys.push(...newItems); state.staging.qmc2.keys.push(...newItems);
state.dirty = true;
}, },
qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) { qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) {
const qmc2 = state.staging.qmc2; const qmc2 = state.staging.qmc2;
qmc2.keys = qmc2.keys.filter((item) => item.id !== id); qmc2.keys = qmc2.keys.filter((item) => item.id !== id);
state.dirty = true;
}, },
qmc2UpdateKey( qmc2UpdateKey(
state, state,
@ -100,25 +106,31 @@ export const settingsSlice = createSlice({
const keyItem = state.staging.qmc2.keys.find((item) => item.id === id); const keyItem = state.staging.qmc2.keys.find((item) => item.id === id);
if (keyItem) { if (keyItem) {
keyItem[field] = value; keyItem[field] = value;
state.dirty = true;
} }
}, },
qmc2ClearKeys(state) { qmc2ClearKeys(state) {
state.staging.qmc2.keys = []; state.staging.qmc2.keys = [];
state.dirty = true;
}, },
qmc2AllowFuzzyNameSearch(state, { payload: { enable } }: PayloadAction<{ enable: boolean }>) { qmc2AllowFuzzyNameSearch(state, { payload: { enable } }: PayloadAction<{ enable: boolean }>) {
state.staging.qmc2.allowFuzzyNameSearch = enable; state.staging.qmc2.allowFuzzyNameSearch = enable;
state.dirty = true;
}, },
// TODO: reuse the logic somehow? // TODO: reuse the logic somehow?
kwm2AddKey(state) { kwm2AddKey(state) {
state.staging.kwm2.keys.push({ id: nanoid(), ekey: '', quality: '', rid: '' }); state.staging.kwm2.keys.push({ id: nanoid(), ekey: '', quality: '', rid: '' });
state.dirty = true;
}, },
kwm2ImportKeys(state, { payload }: PayloadAction<Omit<StagingKWMv2Key, 'id'>[]>) { kwm2ImportKeys(state, { payload }: PayloadAction<Omit<StagingKWMv2Key, 'id'>[]>) {
const newItems = payload.map((item) => ({ id: nanoid(), ...item })); const newItems = payload.map((item) => ({ id: nanoid(), ...item }));
state.staging.kwm2.keys.push(...newItems); state.staging.kwm2.keys.push(...newItems);
state.dirty = true;
}, },
kwm2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) { kwm2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) {
const kwm2 = state.staging.kwm2; const kwm2 = state.staging.kwm2;
kwm2.keys = kwm2.keys.filter((item) => item.id !== id); kwm2.keys = kwm2.keys.filter((item) => item.id !== id);
state.dirty = true;
}, },
kwm2UpdateKey( kwm2UpdateKey(
state, state,
@ -127,18 +139,22 @@ export const settingsSlice = createSlice({
const keyItem = state.staging.kwm2.keys.find((item) => item.id === id); const keyItem = state.staging.kwm2.keys.find((item) => item.id === id);
if (keyItem) { if (keyItem) {
keyItem[field] = value; keyItem[field] = value;
state.dirty = true;
} }
}, },
kwm2ClearKeys(state) { kwm2ClearKeys(state) {
state.staging.kwm2.keys = []; state.staging.kwm2.keys = [];
state.dirty = true;
}, },
// //
discardStagingChanges: (state) => { discardStagingChanges: (state) => {
state.dirty = false;
state.staging = productionToStaging(state.production); state.staging = productionToStaging(state.production);
}, },
commitStagingChange: (state) => { commitStagingChange: (state) => {
const production = stagingToProduction(state.staging); const production = stagingToProduction(state.staging);
return { return {
dirty: false,
// Sync back to staging // Sync back to staging
staging: productionToStaging(production), staging: productionToStaging(production),
production, production,

View File

@ -32,7 +32,12 @@ export class DatabaseKeyExtractor {
return null; return null;
} }
const keys = db.exec('select file_path, ekey from `audio_file_ekey_table`')[0].values; const result = db.exec('select file_path, ekey from audio_file_ekey_table');
if (result.length === 0) {
return [];
}
const keys = result[0].values;
return keys.map(([path, ekey]) => ({ return keys.map(([path, ekey]) => ({
// strip dir name // strip dir name
name: getFileName(String(path)), name: getFileName(String(path)),