Merge pull request '#58 蜻蜓FM 安卓端支援' (#59) from feat/qingting-fm into main
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #59
This commit is contained in:
commit
e1db1b4618
@ -29,5 +29,5 @@ steps:
|
|||||||
commands:
|
commands:
|
||||||
# - git config --global --add safe.directory "/drone/src"
|
# - git config --global --add safe.directory "/drone/src"
|
||||||
- python3 -m zipfile -c um-react.zip dist/.
|
- python3 -m zipfile -c um-react.zip dist/.
|
||||||
# - ./scripts/publish.sh
|
- ./scripts/publish.sh
|
||||||
- ./scripts/deploy.sh
|
- ./scripts/deploy.sh
|
||||||
|
72
package.json
72
package.json
@ -16,56 +16,56 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/anatomy": "^2.2.1",
|
"@chakra-ui/anatomy": "^2.2.2",
|
||||||
"@chakra-ui/icons": "^2.1.1",
|
"@chakra-ui/icons": "^2.1.1",
|
||||||
"@chakra-ui/react": "^2.8.1",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@jixun/libparakeet": "0.3.0",
|
"@jixun/libparakeet": "0.4.2",
|
||||||
"@reduxjs/toolkit": "^1.9.7",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"framer-motion": "^10.16.4",
|
"framer-motion": "^10.16.16",
|
||||||
"immer": "^10.0.3",
|
"immer": "^10.0.3",
|
||||||
"nanoid": "^5.0.1",
|
"nanoid": "^5.0.4",
|
||||||
"radash": "^11.0.0",
|
"radash": "^11.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.12.0",
|
||||||
"react-promise-suspense": "^0.3.4",
|
"react-promise-suspense": "^0.3.4",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^9.0.4",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"sass": "^1.69.2",
|
"sass": "^1.69.5",
|
||||||
"sql.js": "^1.8.0"
|
"sql.js": "^1.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-replace": "^5.0.3",
|
"@rollup/plugin-replace": "^5.0.5",
|
||||||
"@testing-library/jest-dom": "^6.1.3",
|
"@testing-library/jest-dom": "^6.1.5",
|
||||||
"@testing-library/react": "^14.0.0",
|
"@testing-library/react": "^14.1.2",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"@types/node": "^20.8.4",
|
"@types/node": "^20.10.5",
|
||||||
"@types/react": "^18.2.28",
|
"@types/react": "^18.2.45",
|
||||||
"@types/react-dom": "^18.2.13",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-syntax-highlighter": "^15.5.8",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@types/sql.js": "^1.4.5",
|
"@types/sql.js": "^1.4.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.5",
|
"@typescript-eslint/eslint-plugin": "^6.15.0",
|
||||||
"@typescript-eslint/parser": "^6.7.5",
|
"@typescript-eslint/parser": "^6.15.0",
|
||||||
"@vitejs/plugin-react": "^4.1.0",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@vitest/coverage-v8": "^0.34.6",
|
"@vitest/coverage-v8": "^1.1.0",
|
||||||
"@vitest/ui": "^0.34.6",
|
"@vitest/ui": "^1.1.0",
|
||||||
"eslint": "^8.51.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^23.0.1",
|
||||||
"lint-staged": "^14.0.1",
|
"lint-staged": "^15.2.0",
|
||||||
"prettier": "^3.0.3",
|
"prettier": "^3.1.1",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.4.11",
|
"vite": "^5.0.10",
|
||||||
"vite-plugin-pwa": "^0.16.5",
|
"vite-plugin-pwa": "^0.17.4",
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
"vite-plugin-top-level-await": "^1.4.1",
|
||||||
"vite-plugin-wasm": "^3.2.2",
|
"vite-plugin-wasm": "^3.3.0",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^1.1.0",
|
||||||
"workbox-window": "^7.0.0"
|
"workbox-window": "^7.0.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
@ -80,7 +80,7 @@
|
|||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@rollup/plugin-terser@0.4.3": "patches/@rollup__plugin-terser@0.4.3.patch",
|
"@rollup/plugin-terser@0.4.3": "patches/@rollup__plugin-terser@0.4.3.patch",
|
||||||
"sql.js@1.8.0": "patches/sql.js@1.8.0.patch"
|
"sql.js@1.9.0": "patches/sql.js@1.9.0.patch"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"rollup-plugin-terser": "npm:@rollup/plugin-terser@0.4.3",
|
"rollup-plugin-terser": "npm:@rollup/plugin-terser@0.4.3",
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
diff --git a/dist/sql-wasm.js b/dist/sql-wasm.js
|
diff --git a/dist/sql-wasm.js b/dist/sql-wasm.js
|
||||||
index e0da60ba096433d9af1c7025d2ffb9c521f190ed..89a5da6af23e1a644106d38dafe7cfa85500a8c4 100644
|
index d29af3624109025e59966cf25cb357111bb459de..1b028e3d91ec37108f775627f31f1134aec47476 100644
|
||||||
--- a/dist/sql-wasm.js
|
--- a/dist/sql-wasm.js
|
||||||
+++ b/dist/sql-wasm.js
|
+++ b/dist/sql-wasm.js
|
||||||
@@ -192,3 +192,7 @@ else if (typeof define === 'function' && define['amd']) {
|
@@ -190,3 +190,6 @@ else if (typeof define === 'function' && define['amd']) {
|
||||||
else if (typeof exports === 'object'){
|
else if (typeof exports === 'object'){
|
||||||
exports["Module"] = initSqlJs;
|
exports["Module"] = initSqlJs;
|
||||||
}
|
}
|
||||||
+
|
+
|
||||||
+var module;
|
+var module;
|
||||||
+export default initSqlJs;
|
+export default initSqlJs;
|
||||||
+
|
|
3256
pnpm-lock.yaml
3256
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -9,6 +9,7 @@ import { XimalayaAndroidCrypto } from './xmly/xmly_android';
|
|||||||
import { KWMCrypto } from './kwm/kwm';
|
import { KWMCrypto } from './kwm/kwm';
|
||||||
import { MiguCrypto } from './migu/migu3d_keyless';
|
import { MiguCrypto } from './migu/migu3d_keyless';
|
||||||
import { TransparentCrypto } from './transparent/transparent';
|
import { TransparentCrypto } from './transparent/transparent';
|
||||||
|
import { QingTingFM$Device } from './qtfm/qtfm_device';
|
||||||
|
|
||||||
export const allCryptoFactories: CryptoFactory[] = [
|
export const allCryptoFactories: CryptoFactory[] = [
|
||||||
// Xiami (*.xm)
|
// Xiami (*.xm)
|
||||||
@ -40,6 +41,9 @@ export const allCryptoFactories: CryptoFactory[] = [
|
|||||||
XimalayaAndroidCrypto.makeX2M,
|
XimalayaAndroidCrypto.makeX2M,
|
||||||
XimalayaAndroidCrypto.makeX3M,
|
XimalayaAndroidCrypto.makeX3M,
|
||||||
|
|
||||||
|
// QingTingFM (Android)
|
||||||
|
QingTingFM$Device.make,
|
||||||
|
|
||||||
// Transparent crypto (not encrypted)
|
// Transparent crypto (not encrypted)
|
||||||
TransparentCrypto.make,
|
TransparentCrypto.make,
|
||||||
];
|
];
|
||||||
|
25
src/decrypt-worker/crypto/qtfm/qtfm_device.ts
Normal file
25
src/decrypt-worker/crypto/qtfm/qtfm_device.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { transformBlob } from '~/decrypt-worker/util/transformBlob';
|
||||||
|
import type { CryptoBase } from '../CryptoBase';
|
||||||
|
import { DecryptCommandOptions } from '~/decrypt-worker/types';
|
||||||
|
|
||||||
|
export class QingTingFM$Device implements CryptoBase {
|
||||||
|
cryptoName = 'QingTing FM/Device ID';
|
||||||
|
checkByDecryptHeader = false;
|
||||||
|
|
||||||
|
async checkBySignature(_buffer: ArrayBuffer, options: DecryptCommandOptions) {
|
||||||
|
return Boolean(/^\.p~?!.*\.qta$/.test(options.fileName) && options.qingTingAndroidKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(buffer: ArrayBuffer, options: DecryptCommandOptions): Promise<Blob> {
|
||||||
|
const { fileName: name, qingTingAndroidKey } = options;
|
||||||
|
if (!qingTingAndroidKey) {
|
||||||
|
throw new Error('QingTingFM Android Device Key was not provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformBlob(buffer, (p) => p.make.QingTingFM(name, qingTingAndroidKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static make() {
|
||||||
|
return new QingTingFM$Device();
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
export interface DecryptCommandOptions {
|
export interface DecryptCommandOptions {
|
||||||
|
fileName: string;
|
||||||
qmc2Key?: string;
|
qmc2Key?: string;
|
||||||
kwm2key?: string;
|
kwm2key?: string;
|
||||||
|
qingTingAndroidKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DecryptCommandPayload {
|
export interface DecryptCommandPayload {
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
// This is a dummy module for vite/rollup to resolve.
|
// This is a dummy module for vite/rollup to resolve.
|
||||||
Object.defineProperty(Object.create(null), { sideEffects: true });
|
export function createRequire() {
|
||||||
|
import('immer'); // we need to import something, so vite don't complain on build
|
||||||
|
throw new Error('this is a dummy module. Do not use');
|
||||||
|
}
|
||||||
|
@ -6,7 +6,7 @@ import type { DecryptionResult } from '~/decrypt-worker/constants';
|
|||||||
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
||||||
import { decryptionQueue } from '~/decrypt-worker/client';
|
import { decryptionQueue } from '~/decrypt-worker/client';
|
||||||
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
|
||||||
import { selectQMCv2KeyByFileName, selectKWMv2Key } from '../settings/settingsSelector';
|
import { selectQMCv2KeyByFileName, selectKWMv2Key, selectQtfmAndroidKey } from '../settings/settingsSelector';
|
||||||
|
|
||||||
export enum ProcessState {
|
export enum ProcessState {
|
||||||
QUEUED = 'QUEUED',
|
QUEUED = 'QUEUED',
|
||||||
@ -73,8 +73,10 @@ export const processFile = createAsyncThunk<
|
|||||||
.then((r) => r.arrayBuffer());
|
.then((r) => r.arrayBuffer());
|
||||||
|
|
||||||
const options: DecryptCommandOptions = {
|
const options: DecryptCommandOptions = {
|
||||||
|
fileName: file.fileName,
|
||||||
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
|
qmc2Key: selectQMCv2KeyByFileName(state, file.fileName),
|
||||||
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
kwm2key: selectKWMv2Key(state, new DataView(fileHeader)),
|
||||||
|
qingTingAndroidKey: selectQtfmAndroidKey(state),
|
||||||
};
|
};
|
||||||
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
return decryptionQueue.add({ id: fileId, blobURI: file.raw, options }, onPreProcess);
|
||||||
});
|
});
|
||||||
|
@ -30,10 +30,12 @@ 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';
|
import { selectIsSettingsNotSaved } from './settingsSelector';
|
||||||
|
import { PanelQingTing } from './panels/PanelQingTing';
|
||||||
|
|
||||||
const TABS: { name: string; Tab: () => JSX.Element }[] = [
|
const TABS: { name: string; Tab: () => JSX.Element }[] = [
|
||||||
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
|
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
|
||||||
{ name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
|
{ name: 'KWMv2 密钥', Tab: PanelKWMv2Key },
|
||||||
|
{ name: '蜻蜓 FM', Tab: PanelQingTing },
|
||||||
{
|
{
|
||||||
name: '其它/待定',
|
name: '其它/待定',
|
||||||
Tab: () => <Text>这里空空如也~</Text>,
|
Tab: () => <Text>这里空空如也~</Text>,
|
||||||
|
133
src/features/settings/panels/PanelQingTing.tsx
Normal file
133
src/features/settings/panels/PanelQingTing.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Code,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormHelperText,
|
||||||
|
FormLabel,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
ListItem,
|
||||||
|
Text,
|
||||||
|
UnorderedList,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
import { useAppDispatch, useAppSelector } from '~/hooks';
|
||||||
|
import { fetchParakeet } from '@jixun/libparakeet';
|
||||||
|
import { ExtLink } from '~/components/ExtLink';
|
||||||
|
import { ChangeEvent, ClipboardEvent } from 'react';
|
||||||
|
import { VQuote } from '~/components/HelpText/VQuote';
|
||||||
|
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
|
||||||
|
import { qtfmAndroidUpdateKey } from '../settingsSlice';
|
||||||
|
|
||||||
|
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
|
||||||
|
|
||||||
|
export function PanelQingTing() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const secretKey = useAppSelector(selectStagingQtfmAndroidKey);
|
||||||
|
const setSecretKey = (secretKey: string) => {
|
||||||
|
dispatch(qtfmAndroidUpdateKey({ deviceKey: secretKey }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataPaste = (e: ClipboardEvent<HTMLInputElement>) => {
|
||||||
|
const plainText = e.clipboardData.getData('text/plain');
|
||||||
|
const matchDeviceSecret = plainText.match(/^DEVICE_SECRET: ([0-9a-fA-F]+)/m);
|
||||||
|
if (matchDeviceSecret) {
|
||||||
|
e.preventDefault();
|
||||||
|
setSecretKey(matchDeviceSecret[1]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataMap = new Map();
|
||||||
|
for (const [_unused, key, value] of plainText.matchAll(
|
||||||
|
/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim,
|
||||||
|
)) {
|
||||||
|
dataMap.set(key.toLowerCase(), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = dataMap.get('product') ?? null;
|
||||||
|
const device = dataMap.get('device') ?? null;
|
||||||
|
const manufacturer = dataMap.get('manufacturer') ?? null;
|
||||||
|
const brand = dataMap.get('brand') ?? null;
|
||||||
|
const board = dataMap.get('board') ?? null;
|
||||||
|
const model = dataMap.get('model') ?? null;
|
||||||
|
if (
|
||||||
|
product !== null &&
|
||||||
|
device !== null &&
|
||||||
|
manufacturer !== null &&
|
||||||
|
brand !== null &&
|
||||||
|
board !== null &&
|
||||||
|
model !== null
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchParakeet().then((parakeet) => {
|
||||||
|
setSecretKey(parakeet.qtfm.createDeviceKey(product, device, manufacturer, brand, board, model));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDataInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSecretKey(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex minH={0} flexDir="column" flex={1}>
|
||||||
|
<Heading as="h2" size="lg">
|
||||||
|
<VQuote>蜻蜓 FM</VQuote>
|
||||||
|
设备密钥
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
<VQuote>蜻蜓 FM</VQuote>的安卓版本需要获取设备密钥,并以此来生成解密密钥。
|
||||||
|
</Text>
|
||||||
|
<Box mt={3} mb={3}>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>设备密钥</FormLabel>
|
||||||
|
<Input type="text" onPaste={handleDataPaste} value={secretKey} onChange={handleDataInput} />
|
||||||
|
<FormHelperText>
|
||||||
|
{'粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过 '}
|
||||||
|
<ExtLink href={QTFM_DEVICE_ID_URL}>
|
||||||
|
<Code>qtfm-device-id</Code>
|
||||||
|
</ExtLink>
|
||||||
|
{' 获取的设备信息)。'}
|
||||||
|
</FormHelperText>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Heading as="h3" size="md" pt={3} pb={2}>
|
||||||
|
注意事项
|
||||||
|
</Heading>
|
||||||
|
<UnorderedList>
|
||||||
|
<ListItem>
|
||||||
|
<Text>
|
||||||
|
下载的文件位于
|
||||||
|
<Code>[内部储存]/Android/data/fm.qingting.qtradio/files/Music/</Code>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<UnorderedList>
|
||||||
|
<ListItem>
|
||||||
|
<Text>
|
||||||
|
你可能需要使用有
|
||||||
|
<ruby>
|
||||||
|
特权
|
||||||
|
<rp> (</rp>
|
||||||
|
<rt>root</rt>
|
||||||
|
<rp>)</rp>
|
||||||
|
</ruby>
|
||||||
|
的文件浏览器访问。
|
||||||
|
</Text>
|
||||||
|
</ListItem>
|
||||||
|
</UnorderedList>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Text>
|
||||||
|
音频文件文件名为「<Code>.p~!</Code>」前缀。
|
||||||
|
</Text>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Text>因为解密密钥与文件名相关,因此解密前请不要更改文件名。</Text>
|
||||||
|
</ListItem>
|
||||||
|
</UnorderedList>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
// TODO: Popup dialog for QingTing instructions
|
@ -33,6 +33,10 @@ function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof settings?.qtfm?.android === 'string') {
|
||||||
|
draft.qtfm.android = settings.qtfm.android.replace(/[^0-9a-fA-F]/g, '');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +62,6 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
|
|||||||
localStorage.setItem(storageKey, JSON.stringify(currentSettings));
|
localStorage.setItem(storageKey, JSON.stringify(currentSettings));
|
||||||
getLogger().debug('settings saved');
|
getLogger().debug('settings saved');
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -47,3 +47,6 @@ export const selectKWMv2Key = (state: RootState, headerView: DataView): string |
|
|||||||
|
|
||||||
return ekey;
|
return ekey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectStagingQtfmAndroidKey = (state: RootState) => state.settings.staging.qtfm.android;
|
||||||
|
export const selectQtfmAndroidKey = (state: RootState) => state.settings.production.qtfm.android;
|
||||||
|
@ -24,6 +24,9 @@ export interface StagingSettings {
|
|||||||
kwm2: {
|
kwm2: {
|
||||||
keys: StagingKWMv2Key[];
|
keys: StagingKWMv2Key[];
|
||||||
};
|
};
|
||||||
|
qtfm: {
|
||||||
|
android: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductionSettings {
|
export interface ProductionSettings {
|
||||||
@ -34,6 +37,9 @@ export interface ProductionSettings {
|
|||||||
kwm2: {
|
kwm2: {
|
||||||
keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey }
|
keys: ProductionKWMv2Keys; // { [`${rid}-${quality}`]: ekey }
|
||||||
};
|
};
|
||||||
|
qtfm: {
|
||||||
|
android: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
@ -46,10 +52,12 @@ const initialState: SettingsState = {
|
|||||||
staging: {
|
staging: {
|
||||||
qmc2: { allowFuzzyNameSearch: true, keys: [] },
|
qmc2: { allowFuzzyNameSearch: true, keys: [] },
|
||||||
kwm2: { keys: [] },
|
kwm2: { keys: [] },
|
||||||
|
qtfm: { android: '' },
|
||||||
},
|
},
|
||||||
production: {
|
production: {
|
||||||
qmc2: { allowFuzzyNameSearch: true, keys: {} },
|
qmc2: { allowFuzzyNameSearch: true, keys: {} },
|
||||||
kwm2: { keys: {} },
|
kwm2: { keys: {} },
|
||||||
|
qtfm: { android: '' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -61,6 +69,7 @@ const stagingToProduction = (staging: StagingSettings): ProductionSettings => ({
|
|||||||
kwm2: {
|
kwm2: {
|
||||||
keys: stagingKeyToProduction(staging.kwm2.keys, kwm2StagingToProductionKey, kwm2StagingToProductionValue),
|
keys: stagingKeyToProduction(staging.kwm2.keys, kwm2StagingToProductionKey, kwm2StagingToProductionValue),
|
||||||
},
|
},
|
||||||
|
qtfm: staging.qtfm,
|
||||||
});
|
});
|
||||||
|
|
||||||
const productionToStaging = (production: ProductionSettings): StagingSettings => ({
|
const productionToStaging = (production: ProductionSettings): StagingSettings => ({
|
||||||
@ -71,6 +80,7 @@ const productionToStaging = (production: ProductionSettings): StagingSettings =>
|
|||||||
kwm2: {
|
kwm2: {
|
||||||
keys: productionKeyToStaging(production.kwm2.keys, kwm2ProductionToStaging),
|
keys: productionKeyToStaging(production.kwm2.keys, kwm2ProductionToStaging),
|
||||||
},
|
},
|
||||||
|
qtfm: production.qtfm,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const settingsSlice = createSlice({
|
export const settingsSlice = createSlice({
|
||||||
@ -101,7 +111,7 @@ export const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
qmc2UpdateKey(
|
qmc2UpdateKey(
|
||||||
state,
|
state,
|
||||||
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingQMCv2Key; value: string }>
|
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingQMCv2Key; value: string }>,
|
||||||
) {
|
) {
|
||||||
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) {
|
||||||
@ -134,7 +144,7 @@ export const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
kwm2UpdateKey(
|
kwm2UpdateKey(
|
||||||
state,
|
state,
|
||||||
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingKWMv2Key; value: string }>
|
{ payload: { id, field, value } }: PayloadAction<{ id: string; field: keyof StagingKWMv2Key; value: string }>,
|
||||||
) {
|
) {
|
||||||
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) {
|
||||||
@ -142,6 +152,10 @@ export const settingsSlice = createSlice({
|
|||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
qtfmAndroidUpdateKey(state, { payload: { deviceKey } }: PayloadAction<{ deviceKey: string }>) {
|
||||||
|
state.staging.qtfm.android = deviceKey;
|
||||||
|
state.dirty = true;
|
||||||
|
},
|
||||||
kwm2ClearKeys(state) {
|
kwm2ClearKeys(state) {
|
||||||
state.staging.kwm2.keys = [];
|
state.staging.kwm2.keys = [];
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
@ -183,6 +197,8 @@ export const {
|
|||||||
kwm2ClearKeys,
|
kwm2ClearKeys,
|
||||||
kwm2ImportKeys,
|
kwm2ImportKeys,
|
||||||
|
|
||||||
|
qtfmAndroidUpdateKey,
|
||||||
|
|
||||||
commitStagingChange,
|
commitStagingChange,
|
||||||
discardStagingChanges,
|
discardStagingChanges,
|
||||||
} = settingsSlice.actions;
|
} = settingsSlice.actions;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit';
|
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||||
import fileListingReducer from './features/file-listing/fileListingSlice';
|
import fileListingReducer from './features/file-listing/fileListingSlice';
|
||||||
import settingsReducer from './features/settings/settingsSlice';
|
import settingsReducer from './features/settings/settingsSlice';
|
||||||
|
|
||||||
@ -7,12 +7,13 @@ const rootReducer = combineReducers({
|
|||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupStore = (preloadedState?: PreloadedState<RootState>) =>
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
|
||||||
|
export const setupStore = (preloadedState?: Partial<RootState>) =>
|
||||||
configureStore({
|
configureStore({
|
||||||
reducer: rootReducer,
|
reducer: rootReducer,
|
||||||
preloadedState,
|
preloadedState,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RootState = ReturnType<typeof rootReducer>;
|
|
||||||
export type AppStore = ReturnType<typeof setupStore>;
|
export type AppStore = ReturnType<typeof setupStore>;
|
||||||
export type AppDispatch = AppStore['dispatch'];
|
export type AppDispatch = AppStore['dispatch'];
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { PreloadedState } from '@reduxjs/toolkit';
|
|
||||||
import { RenderOptions, render } from '@testing-library/react';
|
import { RenderOptions, render } from '@testing-library/react';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
@ -10,13 +9,13 @@ import { AppStore, RootState, setupStore } from '~/store';
|
|||||||
export * from '@testing-library/react';
|
export * from '@testing-library/react';
|
||||||
|
|
||||||
export interface ExtendedRenderOptions extends RenderOptions {
|
export interface ExtendedRenderOptions extends RenderOptions {
|
||||||
preloadedState?: PreloadedState<RootState>;
|
preloadedState?: Partial<RootState>;
|
||||||
store?: AppStore;
|
store?: AppStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderWithProviders(
|
export function renderWithProviders(
|
||||||
ui: React.ReactElement,
|
ui: React.ReactElement,
|
||||||
{ preloadedState = {}, store = setupStore(preloadedState), ...renderOptions }: ExtendedRenderOptions = {}
|
{ preloadedState = {}, store = setupStore(preloadedState), ...renderOptions }: ExtendedRenderOptions = {},
|
||||||
) {
|
) {
|
||||||
function Wrapper({ children }: PropsWithChildren<unknown>): JSX.Element {
|
function Wrapper({ children }: PropsWithChildren<unknown>): JSX.Element {
|
||||||
return <Provider store={store}>{children}</Provider>;
|
return <Provider store={store}>{children}</Provider>;
|
||||||
|
@ -11,8 +11,9 @@ export const theme = extendTheme({
|
|||||||
].join(','),
|
].join(','),
|
||||||
mono: [
|
mono: [
|
||||||
'SFMono-Regular,Menlo,Monaco',
|
'SFMono-Regular,Menlo,Monaco',
|
||||||
'"Sarasa Mono CJK SC",',
|
'"Sarasa Mono CJK SC"',
|
||||||
'Consolas,"Liberation Mono","Courier New",monospace',
|
'Consolas,"Liberation Mono","Courier New",monospace',
|
||||||
|
'"Microsoft YaHei UI"',
|
||||||
].join(','),
|
].join(','),
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -22,12 +22,14 @@ test('should be able to forward request to worker client bus', async () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const queue = new DecryptionQueue(bus, 1);
|
const queue = new DecryptionQueue(bus, 1);
|
||||||
await expect(queue.add({ id: 'file://1', blobURI: 'blob://mock-file', options: {} })).resolves.toEqual({
|
await expect(
|
||||||
|
queue.add({ id: 'file://1', blobURI: 'blob://mock-file', options: { fileName: 'test.bin' } }),
|
||||||
|
).resolves.toEqual({
|
||||||
actionName: DECRYPTION_WORKER_ACTION_NAME.DECRYPT,
|
actionName: DECRYPTION_WORKER_ACTION_NAME.DECRYPT,
|
||||||
payload: {
|
payload: {
|
||||||
blobURI: 'blob://mock-file',
|
blobURI: 'blob://mock-file',
|
||||||
id: 'file://1',
|
id: 'file://1',
|
||||||
options: {},
|
options: { fileName: 'test.bin' },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -86,6 +86,8 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'~': path.resolve(__dirname, 'src'),
|
'~': path.resolve(__dirname, 'src'),
|
||||||
'@nm': path.resolve(__dirname, 'node_modules'),
|
'@nm': path.resolve(__dirname, 'node_modules'),
|
||||||
|
|
||||||
|
// workaround for vite, workbox (PWA) and Emscripten transpiled parakeet lib (use of `import("module")`)
|
||||||
module: path.resolve(__dirname, 'src', 'dummy.mjs'),
|
module: path.resolve(__dirname, 'src', 'dummy.mjs'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -106,12 +108,6 @@ export default defineConfig({
|
|||||||
mockReset: true,
|
mockReset: true,
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
setupFiles: ['src/test-utils/setup-jest.ts'],
|
setupFiles: ['src/test-utils/setup-jest.ts'],
|
||||||
alias: [
|
|
||||||
{
|
|
||||||
find: /^~\/(.*)/,
|
|
||||||
replacement: 'src/$1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// workaround: sql.js is not ESModule friendly, yet...
|
// workaround: sql.js is not ESModule friendly, yet...
|
||||||
deps: {
|
deps: {
|
||||||
optimizer: {
|
optimizer: {
|
||||||
|
Loading…
Reference in New Issue
Block a user