mirror of
https://git.um-react.app/um/um-react.git
synced 2025-11-28 11:33:02 +00:00
feat(kgm): kgm v5 (aka. kgg) support
This commit is contained in:
@@ -5,13 +5,18 @@ import type { RootState } from '~/store';
|
||||
import { DECRYPTION_WORKER_ACTION_NAME, type DecryptionResult } from '~/decrypt-worker/constants';
|
||||
import type {
|
||||
DecryptCommandOptions,
|
||||
FetchMusicExNamePayload,
|
||||
FetchMusicExNamePayload, ParseKugouHeaderPayload, ParseKugouHeaderResponse,
|
||||
ParseKuwoHeaderPayload,
|
||||
ParseKuwoHeaderResponse,
|
||||
ParseKuwoHeaderResponse
|
||||
} from '~/decrypt-worker/types';
|
||||
import { decryptionQueue, workerClientBus } from '~/decrypt-worker/client';
|
||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||
import { selectKWMv2Key, selectQMCv2KeyByFileName, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||
import {
|
||||
selectKugouKey,
|
||||
selectKWMv2Key,
|
||||
selectQMCv2KeyByFileName,
|
||||
selectQtfmAndroidKey
|
||||
} from '../settings/settingsSelector';
|
||||
|
||||
export enum ProcessState {
|
||||
QUEUED = 'QUEUED',
|
||||
@@ -70,7 +75,7 @@ export const processFile = createAsyncThunk<
|
||||
thunkAPI.dispatch(setFileAsProcessing({ id: fileId }));
|
||||
};
|
||||
|
||||
const [qmcv2MusicExMediaFile, kuwoHdr] = await Promise.all([
|
||||
const [qmcv2MusicExMediaFile, kuwoHdr, kugouHdr] = await Promise.all([
|
||||
workerClientBus.request<string, FetchMusicExNamePayload>(DECRYPTION_WORKER_ACTION_NAME.FIND_QMC_MUSICEX_NAME, {
|
||||
blobURI: file.raw,
|
||||
}),
|
||||
@@ -78,12 +83,17 @@ export const processFile = createAsyncThunk<
|
||||
DECRYPTION_WORKER_ACTION_NAME.KUWO_PARSE_HEADER,
|
||||
{ blobURI: file.raw },
|
||||
),
|
||||
workerClientBus.request<ParseKugouHeaderResponse, ParseKugouHeaderPayload>(
|
||||
DECRYPTION_WORKER_ACTION_NAME.KUGOU_PARSE_HEADER,
|
||||
{ blobURI: file.raw },
|
||||
),
|
||||
]);
|
||||
|
||||
const options: DecryptCommandOptions = {
|
||||
fileName: file.fileName,
|
||||
qmc2Key: selectQMCv2KeyByFileName(state, qmcv2MusicExMediaFile || file.fileName),
|
||||
kwm2key: selectKWMv2Key(state, kuwoHdr),
|
||||
kugouKey: selectKugouKey(state, kugouHdr),
|
||||
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||
};
|
||||
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
||||
|
||||
@@ -31,10 +31,12 @@ import { commitStagingChange, discardStagingChanges } from './settingsSlice';
|
||||
import { PanelKWMv2Key } from './panels/PanelKWMv2Key';
|
||||
import { selectIsSettingsNotSaved } from './settingsSelector';
|
||||
import { PanelQingTing } from './panels/PanelQingTing';
|
||||
import { PanelKGGKey } from '~/features/settings/panels/PanelKGGKey.tsx';
|
||||
|
||||
const TABS: { name: string; Tab: () => JSX.Element }[] = [
|
||||
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
|
||||
{ name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
|
||||
{ name: 'KGG 密钥', Tab: PanelKGGKey },
|
||||
{ name: '蜻蜓 FM', Tab: PanelQingTing },
|
||||
{
|
||||
name: '其它/待定',
|
||||
|
||||
@@ -14,6 +14,7 @@ export function productionKeyToStaging<S, P extends Record<string, unknown>>(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stagingKeyToProduction<S, P>(src: S[], toKey: (s: S) => keyof P, toValue: (s: S) => P[keyof P]): P {
|
||||
return objectify(src, toKey, toValue) as P;
|
||||
}
|
||||
@@ -41,7 +42,6 @@ export const qmc2ProductionToStaging = (
|
||||
};
|
||||
|
||||
// KWMv2 (KuWo)
|
||||
|
||||
export interface StagingKWMv2Key {
|
||||
id: string;
|
||||
/**
|
||||
@@ -64,7 +64,7 @@ export const parseKwm2ProductionKey = (key: string): null | { rid: string; quali
|
||||
|
||||
return { rid, quality };
|
||||
};
|
||||
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality.replace(/[\D]/g, '')}`;
|
||||
export const kwm2StagingToProductionKey = (key: StagingKWMv2Key) => `${key.rid}-${key.quality.replace(/\D/g, '')}`;
|
||||
export const kwm2StagingToProductionValue = (key: StagingKWMv2Key) => key.ekey;
|
||||
export const kwm2ProductionToStaging = (
|
||||
key: keyof ProductionKWMv2Keys,
|
||||
@@ -78,3 +78,21 @@ export const kwm2ProductionToStaging = (
|
||||
|
||||
return { id: nanoid(), rid, quality, ekey: value };
|
||||
};
|
||||
|
||||
// KuGou (kgg, kgm v5)
|
||||
export interface StagingKugouKey {
|
||||
id: string;
|
||||
audioHash: string;
|
||||
ekey: string;
|
||||
}
|
||||
|
||||
export type ProductionKugouKey = Record<string /* audioHash */, string /* ekey */>;
|
||||
export const kugouStagingToProductionKey = (key: StagingKugouKey) => key.audioHash.normalize();
|
||||
export const kugouStagingToProductionValue = (key: StagingKugouKey) => key.ekey.normalize();
|
||||
export const kugouProductionToStaging = (
|
||||
key: keyof ProductionKugouKey,
|
||||
value: ProductionKugouKey[keyof ProductionKugouKey],
|
||||
): null | StagingKugouKey => {
|
||||
if (typeof value !== 'string') return null;
|
||||
return { id: nanoid(), audioHash: key.normalize(), ekey: value };
|
||||
};
|
||||
|
||||
34
src/features/settings/panels/Kugou/InstructionsPC.tsx
Normal file
34
src/features/settings/panels/Kugou/InstructionsPC.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Code, Heading, ListItem, OrderedList, Text } from '@chakra-ui/react';
|
||||
import { FilePathBlock } from '~/components/FilePathBlock.tsx';
|
||||
|
||||
export function InstructionsPC() {
|
||||
return (
|
||||
<>
|
||||
<Text>酷狗的 Windows 客户端使用 <abbr title="SQLite w/ SQLCipher">SQLite</abbr> 数据库储存密钥。</Text>
|
||||
<Text>该密钥文件通常存储在下述路径:</Text>
|
||||
<FilePathBlock>%APPDATA%\KuGou8\KGMusicV3.db</FilePathBlock>
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
src/features/settings/panels/Kugou/KugouAllInstructions.tsx
Normal file
25
src/features/settings/panels/Kugou/KugouAllInstructions.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
||||
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
|
||||
import { InstructionsPC } from './InstructionsPC';
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
72
src/features/settings/panels/Kugou/KugouEKeyItem.tsx
Normal file
72
src/features/settings/panels/Kugou/KugouEKeyItem.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
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';
|
||||
|
||||
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 }));
|
||||
|
||||
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>
|
||||
);
|
||||
});
|
||||
87
src/features/settings/panels/PanelKGGKey.tsx
Normal file
87
src/features/settings/panels/PanelKGGKey.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Box, Flex, Heading, List, Text, useToast } from '@chakra-ui/react';
|
||||
import { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { ImportSecretModal } from '~/components/ImportSecretModal';
|
||||
|
||||
import { kugouAddKey, kugouClearKeys, kugouImportKeys } from '../settingsSlice';
|
||||
import { selectStagingKugouV5Keys } from '../settingsSelector';
|
||||
import type { StagingKugouKey } from '../keyFormats';
|
||||
import { AddKey } from '~/components/AddKey.tsx';
|
||||
import { KugouEKeyItem } from '~/features/settings/panels/Kugou/KugouEKeyItem.tsx';
|
||||
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
|
||||
import { parseAndroidKugouMMKV } from '~/util/mmkv/kugou.ts';
|
||||
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor.ts';
|
||||
|
||||
export function PanelKGGKey() {
|
||||
const toast = useToast();
|
||||
const dispatch = useDispatch();
|
||||
const kugouKeys = useSelector(selectStagingKugouV5Keys);
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
|
||||
const addKey = () => dispatch(kugouAddKey());
|
||||
const clearAll = () => dispatch(kugouClearKeys());
|
||||
const handleSecretImport = async (file: File) => {
|
||||
let keys: Omit<StagingKugouKey, 'id'>[] | null = null;
|
||||
if (/mggkey_multi_process/i.test(file.name)) {
|
||||
keys = parseAndroidKugouMMKV(new DataView(await file.arrayBuffer()));
|
||||
} else if (/^KGMusicV3\.db$/.test(file.name)) {
|
||||
const extractor = await DatabaseKeyExtractor.getInstance();
|
||||
keys = extractor.extractKugouKeyFromEncryptedDb(await file.arrayBuffer());
|
||||
}
|
||||
|
||||
if (keys?.length === 0) {
|
||||
toast({
|
||||
title: '未导入密钥',
|
||||
description: '选择的密钥数据库文件未发现任何可用的密钥。',
|
||||
isClosable: true,
|
||||
status: 'warning',
|
||||
});
|
||||
} else if (keys) {
|
||||
dispatch(kugouImportKeys(keys));
|
||||
setShowImportModal(false);
|
||||
toast({
|
||||
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
|
||||
description: '记得按下「保存」来应用。',
|
||||
isClosable: true,
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: `不支持的文件:${file.name}`,
|
||||
isClosable: true,
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex minH={0} flexDir="column" flex={1}>
|
||||
<Heading as="h2" size="lg">
|
||||
酷狗解密密钥 (KGG / KGM v5)
|
||||
</Heading>
|
||||
|
||||
<Text>酷狗已经升级了加密方式,现在使用 KGG / KGM v5 加密。</Text>
|
||||
|
||||
<AddKey addKey={addKey} 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>
|
||||
|
||||
<ImportSecretModal
|
||||
clientName="酷狗音乐"
|
||||
show={showImportModal}
|
||||
onClose={() => setShowImportModal(false)}
|
||||
onImport={handleSecretImport}
|
||||
>
|
||||
<KugouAllInstructions />
|
||||
</ImportSecretModal>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,16 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
||||
}
|
||||
}
|
||||
|
||||
if (settings?.kugou) {
|
||||
const { keys } = settings.kugou;
|
||||
|
||||
for (const [k, v] of enumObject(keys)) {
|
||||
if (typeof v === 'string') {
|
||||
draft.kugou.keys[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof settings?.qtfm?.android === 'string') {
|
||||
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { RootState } from '~/store';
|
||||
import { closestByLevenshtein } from '~/util/levenshtein';
|
||||
import { hasOwn } from '~/util/objects';
|
||||
import { kwm2StagingToProductionKey } from './keyFormats';
|
||||
import type { ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
|
||||
import type { ParseKugouHeaderResponse, ParseKuwoHeaderResponse } from '~/decrypt-worker/types.ts';
|
||||
|
||||
export const selectIsSettingsNotSaved = (state: RootState) => state.settings.dirty;
|
||||
|
||||
@@ -12,6 +12,9 @@ export const selectFinalQMCv2Settings = (state: RootState) => state.settings.pro
|
||||
export const selectStagingKWMv2Keys = (state: RootState) => state.settings.staging.kwm2.keys;
|
||||
export const selectFinalKWMv2Keys = (state: RootState) => state.settings.production.kwm2.keys;
|
||||
|
||||
export const selectStagingKugouV5Keys = (state: RootState) => state.settings.staging.kugou.keys;
|
||||
export const selectFinalKugouV5Keys = (state: RootState) => state.settings.production.kugou.keys;
|
||||
|
||||
export const selectQMCv2KeyByFileName = (state: RootState, name: string): string | undefined => {
|
||||
const normalizedName = name.normalize();
|
||||
|
||||
@@ -50,5 +53,16 @@ export const selectKWMv2Key = (state: RootState, hdr: ParseKuwoHeaderResponse):
|
||||
return ekey;
|
||||
};
|
||||
|
||||
export const selectKugouKey = (state: RootState, hdr: ParseKugouHeaderResponse): string | undefined => {
|
||||
if (!hdr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = selectFinalKugouV5Keys(state);
|
||||
const lookupKey = hdr.audioHash;
|
||||
|
||||
return hasOwn(keys, lookupKey) ? keys[lookupKey] : undefined;
|
||||
};
|
||||
|
||||
export const selectStagingQtfmAndroidKey = (state: RootState) => state.settings.staging.qtfm.android;
|
||||
export const selectQtfmAndroidKey = (state: RootState) => state.settings.production.qtfm.android;
|
||||
|
||||
@@ -14,6 +14,11 @@ import {
|
||||
qmc2StagingToProductionKey,
|
||||
qmc2StagingToProductionValue,
|
||||
stagingKeyToProduction,
|
||||
ProductionKugouKey,
|
||||
kugouProductionToStaging,
|
||||
kugouStagingToProductionKey,
|
||||
kugouStagingToProductionValue,
|
||||
StagingKugouKey,
|
||||
} from './keyFormats';
|
||||
|
||||
export interface StagingSettings {
|
||||
@@ -24,6 +29,9 @@ export interface StagingSettings {
|
||||
kwm2: {
|
||||
keys: StagingKWMv2Key[];
|
||||
};
|
||||
kugou: {
|
||||
keys: StagingKugouKey[];
|
||||
};
|
||||
qtfm: {
|
||||
android: string;
|
||||
};
|
||||
@@ -37,6 +45,9 @@ export interface ProductionSettings {
|
||||
kwm2: {
|
||||
keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey }
|
||||
};
|
||||
kugou: {
|
||||
keys: ProductionKugouKey; // { [fileName]: ekey }
|
||||
};
|
||||
qtfm: {
|
||||
android: string;
|
||||
};
|
||||
@@ -47,16 +58,19 @@ export interface SettingsState {
|
||||
staging: StagingSettings;
|
||||
production: ProductionSettings;
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
dirty: false,
|
||||
staging: {
|
||||
qmc2: { allowFuzzyNameSearch: true, keys: [] },
|
||||
kwm2: { keys: [] },
|
||||
qtfm: { android: '' },
|
||||
kugou: { keys: [] },
|
||||
},
|
||||
production: {
|
||||
qmc2: { allowFuzzyNameSearch: true, keys: {} },
|
||||
kwm2: { keys: {} },
|
||||
kugou: { keys: {} },
|
||||
qtfm: { android: '' },
|
||||
},
|
||||
};
|
||||
@@ -69,6 +83,9 @@ const stagingToProduction = (staging: StagingSettings): ProductionSettings => ({
|
||||
kwm2: {
|
||||
keys: stagingKeyToProduction(staging.kwm2.keys, kwm2StagingToProductionKey, kwm2StagingToProductionValue),
|
||||
},
|
||||
kugou: {
|
||||
keys: stagingKeyToProduction(staging.kugou.keys, kugouStagingToProductionKey, kugouStagingToProductionValue),
|
||||
},
|
||||
qtfm: staging.qtfm,
|
||||
});
|
||||
|
||||
@@ -80,6 +97,9 @@ const productionToStaging = (production: ProductionSettings): StagingSettings =>
|
||||
kwm2: {
|
||||
keys: productionKeyToStaging(production.kwm2.keys, kwm2ProductionToStaging),
|
||||
},
|
||||
kugou: {
|
||||
keys: productionKeyToStaging(production.kugou.keys, kugouProductionToStaging),
|
||||
},
|
||||
qtfm: production.qtfm,
|
||||
});
|
||||
|
||||
@@ -152,14 +172,42 @@ export const settingsSlice = createSlice({
|
||||
state.dirty = true;
|
||||
}
|
||||
},
|
||||
qtfmAndroidUpdateKey(state, { payload: { deviceKey } }: PayloadAction<{ deviceKey: string }>) {
|
||||
state.staging.qtfm.android = deviceKey;
|
||||
state.dirty = true;
|
||||
},
|
||||
kwm2ClearKeys(state) {
|
||||
state.staging.kwm2.keys = [];
|
||||
state.dirty = true;
|
||||
},
|
||||
kugouAddKey(state) {
|
||||
state.staging.kugou.keys.push({ id: nanoid(), audioHash: '', ekey: '' });
|
||||
state.dirty = true;
|
||||
},
|
||||
kugouImportKeys(state, { payload }: PayloadAction<Omit<StagingKugouKey, 'id'>[]>) {
|
||||
const newItems = payload.map((item) => ({ id: nanoid(), ...item }));
|
||||
state.staging.kugou.keys.push(...newItems);
|
||||
state.dirty = true;
|
||||
},
|
||||
kugouDeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) {
|
||||
const kugou = state.staging.kugou;
|
||||
kugou.keys = kugou.keys.filter((item) => item.id !== id);
|
||||
state.dirty = true;
|
||||
},
|
||||
kugouUpdateKey(
|
||||
state,
|
||||
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingKugouKey; value: string }>,
|
||||
) {
|
||||
const keyItem = state.staging.kugou.keys.find((item) => item.id === id);
|
||||
if (keyItem) {
|
||||
keyItem[field] = value;
|
||||
state.dirty = true;
|
||||
}
|
||||
},
|
||||
kugouClearKeys(state) {
|
||||
state.staging.kugou.keys = [];
|
||||
state.dirty = true;
|
||||
},
|
||||
qtfmAndroidUpdateKey(state, { payload: { deviceKey } }: PayloadAction<{ deviceKey: string }>) {
|
||||
state.staging.qtfm.android = deviceKey;
|
||||
state.dirty = true;
|
||||
},
|
||||
//
|
||||
discardStagingChanges: (state) => {
|
||||
state.dirty = false;
|
||||
@@ -197,6 +245,12 @@ export const {
|
||||
kwm2ClearKeys,
|
||||
kwm2ImportKeys,
|
||||
|
||||
kugouAddKey,
|
||||
kugouUpdateKey,
|
||||
kugouDeleteKey,
|
||||
kugouClearKeys,
|
||||
kugouImportKeys,
|
||||
|
||||
qtfmAndroidUpdateKey,
|
||||
|
||||
commitStagingChange,
|
||||
|
||||
Reference in New Issue
Block a user