From a11caaae923399fe3dc28de3b0f7368fdf8e7191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Thu, 16 Oct 2025 01:00:38 +0900 Subject: [PATCH] refactor: major package updates --- eslint.config.mjs | 17 +- package.json | 4 +- pnpm-lock.yaml | 222 ++++++++++++------ src/components/DownloadAll.tsx | 92 ++++---- src/components/DownloadBase64.tsx | 2 +- src/components/ExtLink.tsx | 2 +- src/components/ImportSecretModal.tsx | 7 +- src/components/SDKVersion.tsx | 2 +- src/components/SelectFile.tsx | 5 +- src/decrypt-worker/decipher/KugouMusic.ts | 6 +- src/decrypt-worker/decipher/KuwoMusic.ts | 6 +- src/decrypt-worker/decipher/Migu3d.ts | 17 +- .../decipher/NetEaseCloudMusic.ts | 6 +- src/decrypt-worker/decipher/QQMusic.ts | 25 +- src/decrypt-worker/decipher/QingTingFM.ts | 17 +- src/decrypt-worker/decipher/Transparent.ts | 6 +- src/decrypt-worker/decipher/XiamiMusic.ts | 19 +- src/decrypt-worker/decipher/Ximalaya.ts | 48 ++-- src/decrypt-worker/util/wasmClass.ts | 2 +- src/decrypt-worker/worker/qtfm_device_key.ts | 2 +- src/dummy.mjs | 2 +- src/features/file-listing/FileError.tsx | 12 +- .../settings/panels/Kugou/InstructionsPC.tsx | 15 +- .../settings/panels/PanelQMCv2Key.tsx | 2 +- .../settings/panels/PanelQingTing.tsx | 18 +- src/features/settings/persistSettings.ts | 2 +- src/test-utils/setup-jest.ts | 3 +- src/util/SimpleQueue.ts | 28 +++ src/util/WorkerEventBus.ts | 2 +- src/util/__tests__/ConcurrentQueue.test.ts | 2 +- src/util/__tests__/DecryptionQueue.test.ts | 2 +- src/util/applyTemplate.ts | 4 +- src/util/clipboard.ts | 12 +- src/util/deepClone.ts | 2 +- 34 files changed, 373 insertions(+), 240 deletions(-) create mode 100644 src/util/SimpleQueue.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index 23dfca6..e703952 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,15 +1,16 @@ import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; import tseslint from 'typescript-eslint'; import reactRefresh from 'eslint-plugin-react-refresh'; import reactHooks from 'eslint-plugin-react-hooks'; import eslintConfigPrettier from 'eslint-config-prettier/flat'; import globals from 'globals'; -export default tseslint.config( +export default defineConfig( eslint.configs.recommended, - tseslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, reactRefresh.configs.recommended, - reactHooks.configs['recommended-latest'], + reactHooks.configs.flat.recommended, eslintConfigPrettier, { @@ -40,4 +41,14 @@ export default tseslint.config( }, }, }, + + { + languageOptions: { + parserOptions: { + projectService: { + allowDefaultProject: ['*.mjs', 'src/*.mjs', 'scripts/*.mjs'], + }, + }, + }, + }, ); diff --git a/package.json b/package.json index ec1ee0f..a05af6a 100644 --- a/package.json +++ b/package.json @@ -54,11 +54,11 @@ "daisyui": "^5.3.2", "eslint": "^9.37.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.4.0", "husky": "^9.1.7", - "jsdom": "^26.1.0", + "jsdom": "^27.0.0", "lint-staged": "^16.2.4", "prettier": "^3.6.2", "rollup": "^4.52.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 542ea09..a8bcfe1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,8 +123,8 @@ importers: specifier: ^10.1.8 version: 10.1.8(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react-hooks: - specifier: ^5.2.0 - version: 5.2.0(eslint@9.37.0(jiti@2.6.1)) + specifier: ^7.0.0 + version: 7.0.0(eslint@9.37.0(jiti@2.6.1)) eslint-plugin-react-refresh: specifier: ^0.4.24 version: 0.4.24(eslint@9.37.0(jiti@2.6.1)) @@ -135,8 +135,8 @@ importers: specifier: ^9.1.7 version: 9.1.7 jsdom: - specifier: ^26.1.0 - version: 26.1.0 + specifier: ^27.0.0 + version: 27.0.0(postcss@8.5.6) lint-staged: specifier: ^16.2.4 version: 16.2.4 @@ -181,7 +181,7 @@ importers: version: 3.5.0(vite@7.1.10(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) workbox-build: specifier: ^7.3.0 version: 7.3.0(@types/babel__core@7.20.5) @@ -204,8 +204,14 @@ packages: peerDependencies: ajv: '>=8' - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/css-color@4.0.5': + resolution: {integrity: sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==} + + '@asamuzakjp/dom-selector@6.6.2': + resolution: {integrity: sha512-+AG0jN9HTwfDLBhjhX1FKi6zlIAc/YGgEHlN/OMaHD1pOPFsC5CpYQpLkPX0aFjyaVmoq9330cQDCU4qnSL1qA==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} @@ -742,6 +748,12 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-syntax-patches-for-csstree@1.0.14': + resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==} + engines: {node: '>=18'} + peerDependencies: + postcss: ^8.4 + '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} @@ -1796,6 +1808,9 @@ packages: resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1926,12 +1941,16 @@ packages: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} + cssstyle@5.3.1: + resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==} + engines: {node: '>=20'} csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -1939,9 +1958,9 @@ packages: daisyui@5.3.2: resolution: {integrity: sha512-hw6NmQvFHdZI1Zb94EX+vA0DhuXGfqwQInDQ6XNZ5dgkoDy+e01P4OkJ8umAQAAzVSKGwm6QDMXOw6eJV63OEQ==} - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} @@ -2087,9 +2106,9 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-react-hooks@5.2.0: - resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} - engines: {node: '>=10'} + eslint-plugin-react-hooks@7.0.0: + resolution: {integrity: sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==} + engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -2354,6 +2373,12 @@ packages: hastscript@6.0.0: resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -2606,9 +2631,9 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} + jsdom@27.0.0: + resolution: {integrity: sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==} + engines: {node: '>=20'} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -2764,6 +2789,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.2: + resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2788,6 +2817,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2853,9 +2885,6 @@ packages: node-releases@2.0.23: resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} - nwsapi@2.2.22: - resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3426,11 +3455,11 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.17: + resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==} - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + tldts@7.0.17: + resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==} hasBin: true to-regex-range@5.0.1: @@ -3441,16 +3470,16 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} @@ -3652,9 +3681,9 @@ packages: webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + webidl-conversions@8.0.0: + resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==} + engines: {node: '>=20'} whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} @@ -3664,9 +3693,9 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -3804,6 +3833,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -3820,13 +3858,23 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@asamuzakjp/css-color@3.2.0': + '@asamuzakjp/css-color@4.0.5': dependencies: '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 + lru-cache: 11.2.2 + + '@asamuzakjp/dom-selector@6.6.2': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.2 + + '@asamuzakjp/nwsapi@2.3.9': {} '@babel/code-frame@7.27.1': dependencies: @@ -4512,6 +4560,10 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.6)': + dependencies: + postcss: 8.5.6 + '@csstools/css-tokenizer@3.0.4': {} '@esbuild/aix-ppc64@0.25.11': @@ -5285,7 +5337,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -5334,7 +5386,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) + vitest: 3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1) '@vitest/utils@3.2.4': dependencies: @@ -5455,6 +5507,10 @@ snapshots: baseline-browser-mapping@2.8.16: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5575,21 +5631,29 @@ snapshots: crypto-random-string@2.0.0: {} + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + css.escape@1.5.1: {} - cssstyle@4.6.0: + cssstyle@5.3.1(postcss@8.5.6): dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 + '@asamuzakjp/css-color': 4.0.5 + '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6) + css-tree: 3.1.0 + transitivePeerDependencies: + - postcss csstype@3.1.3: {} daisyui@5.3.2: {} - data-urls@5.0.0: + data-urls@6.0.0: dependencies: whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 + whatwg-url: 15.1.0 data-view-buffer@1.0.2: dependencies: @@ -5790,9 +5854,16 @@ snapshots: dependencies: eslint: 9.37.0(jiti@2.6.1) - eslint-plugin-react-hooks@5.2.0(eslint@9.37.0(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.0(eslint@9.37.0(jiti@2.6.1)): dependencies: + '@babel/core': 7.28.4 + '@babel/parser': 7.28.4 eslint: 9.37.0(jiti@2.6.1) + hermes-parser: 0.25.1 + zod: 4.1.12 + zod-validation-error: 4.0.2(zod@4.1.12) + transitivePeerDependencies: + - supports-color eslint-plugin-react-refresh@0.4.24(eslint@9.37.0(jiti@2.6.1)): dependencies: @@ -6086,6 +6157,12 @@ snapshots: property-information: 5.6.0 space-separated-tokens: 1.1.5 + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + highlight.js@10.7.3: {} highlightjs-vue@1.0.0: {} @@ -6330,30 +6407,31 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@26.1.0: + jsdom@27.0.0(postcss@8.5.6): dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 + '@asamuzakjp/dom-selector': 6.6.2 + cssstyle: 5.3.1(postcss@8.5.6) + data-urls: 6.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.22 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.2 + tough-cookie: 6.0.0 w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 + webidl-conversions: 8.0.0 whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 + whatwg-url: 15.1.0 ws: 8.18.3 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil + - postcss - supports-color - utf-8-validate @@ -6487,6 +6565,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.2: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -6513,6 +6593,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.12.2: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -6559,8 +6641,6 @@ snapshots: node-releases@2.0.23: {} - nwsapi@2.2.22: {} - object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -7192,11 +7272,11 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@6.1.86: {} + tldts-core@7.0.17: {} - tldts@6.1.86: + tldts@7.0.17: dependencies: - tldts-core: 6.1.86 + tldts-core: 7.0.17 to-regex-range@5.0.1: dependencies: @@ -7204,15 +7284,15 @@ snapshots: totalist@3.0.1: {} - tough-cookie@5.1.2: + tough-cookie@6.0.0: dependencies: - tldts: 6.1.86 + tldts: 7.0.17 tr46@1.0.1: dependencies: punycode: 2.3.1 - tr46@5.1.1: + tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -7382,7 +7462,7 @@ snapshots: terser: 5.44.0 yaml: 2.8.1 - vitest@3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.7.2)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.1)(sass@1.93.2)(terser@5.44.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -7410,7 +7490,7 @@ snapshots: optionalDependencies: '@types/node': 24.7.2 '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 + jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: - jiti - less @@ -7431,7 +7511,7 @@ snapshots: webidl-conversions@4.0.2: {} - webidl-conversions@7.0.0: {} + webidl-conversions@8.0.0: {} whatwg-encoding@3.1.1: dependencies: @@ -7439,10 +7519,10 @@ snapshots: whatwg-mimetype@4.0.0: {} - whatwg-url@14.2.0: + whatwg-url@15.1.0: dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 + tr46: 6.0.0 + webidl-conversions: 8.0.0 whatwg-url@7.1.0: dependencies: @@ -7650,3 +7730,9 @@ snapshots: yaml@2.8.1: {} yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.1.12): + dependencies: + zod: 4.1.12 + + zod@4.1.12: {} diff --git a/src/components/DownloadAll.tsx b/src/components/DownloadAll.tsx index e31e8ee..94b6873 100644 --- a/src/components/DownloadAll.tsx +++ b/src/components/DownloadAll.tsx @@ -2,83 +2,89 @@ import { DecryptedAudioFile, ProcessState, selectFiles } from '~/features/file-l import { FaDownload } from 'react-icons/fa'; import { useAppSelector } from '~/hooks'; import { toast } from 'react-toastify'; +import { SimpleQueue } from '~/util/SimpleQueue'; export function DownloadAll() { const files = useAppSelector(selectFiles); - const onClickDownloadAll = async () => { - console.time('DownloadAll'); //开始计时 - const fileCount = Object.keys(files).length; + const downloadAllAsync = async () => { + const fileList = Object.values(files); + const fileCount = fileList.length; if (fileCount === 0) { toast.warning('未添加文件'); return; } - //判断所有文件是否处理完成 - const allComplete = Object.values(files).every((file) => file.state !== ProcessState.PROCESSING); + // 判断所有文件是否处理完成 + const allComplete = fileList.every((file) => file.state !== ProcessState.PROCESSING); if (!allComplete) { toast.warning('请等待所有文件解密完成'); return; } - //过滤处理失败的文件 - const completeFiles = Object.values(files).filter((file) => file.state === ProcessState.COMPLETE); + // 过滤处理失败的文件 + const completeFiles = fileList.filter((file) => file.state === ProcessState.COMPLETE); - //开始下载 - let dir: FileSystemDirectoryHandle | undefined; + // 准备下载 + let dir: FileSystemDirectoryHandle | null = null; try { dir = await window.showDirectoryPicker({ mode: 'readwrite' }); } catch (e) { - console.error(e); if (e instanceof Error && e.name === 'AbortError') { - return; + return; // user cancelled } + console.error(e); } toast.warning('开始下载,请稍候'); - + const queue = new SimpleQueue(8); const promises = Object.values(completeFiles).map(async (file) => { - console.log(`开始下载: ${file.fileName}`); try { - if (dir) { - await DownloadNew(dir, file); - } else { - await DownloadOld(file); - } - console.log(`成功下载: ${file.fileName}`); + await queue.enter(); + await downloadFile(file, dir); } catch (e) { console.error(`下载失败: ${file.fileName}`, e); - toast.error(`出现错误: ${e}`); + toast.error(`出现错误: ${e as Error}`); throw e; + } finally { + queue.leave(); } }); - await Promise.allSettled(promises).then((f) => { - const success = f.filter((result) => result.status === 'fulfilled').length; - if (success === fileCount) { - toast.success(`成功下载: ${success}/${fileCount}首`); - } else { - toast.warning(`成功下载: ${success}/${fileCount}首`); - } - }); - console.timeEnd('DownloadAll'); //停止计时 + + const promiseResults = await Promise.allSettled(promises); + const success = promiseResults.filter((result) => result.status === 'fulfilled').length; + const level = success === fileCount ? 'success' : success === 0 ? 'error' : 'warning'; + toast[level](`成功下载: ${success}/${fileCount}首`); }; + function onDownloadAll() { + downloadAllAsync().catch((e) => { + // this should not happen + console.error('下载全部出现错误', e); + }); + } + return ( - ); } -async function DownloadNew(dir: FileSystemDirectoryHandle, file: DecryptedAudioFile) { - const fileHandle = await dir.getFileHandle(file.cleanName + '.' + file.ext, { create: true }); - const writable = await fileHandle.createWritable(); - await fetch(file.decrypted).then((res) => res.body?.pipeTo(writable)); -} - -async function DownloadOld(file: DecryptedAudioFile) { - const a = document.createElement('a'); - a.href = file.decrypted; - a.download = file.cleanName + '.' + file.ext; - document.body.append(a); - a.click(); - a.remove(); +async function downloadFile(file: DecryptedAudioFile, dir: FileSystemDirectoryHandle | null) { + if (dir) { + const fileHandle = await dir.getFileHandle(file.cleanName + '.' + file.ext, { create: true }); + const fileStream = await fileHandle.createWritable(); + try { + const res = await fetch(file.decrypted); + await res.body?.pipeTo(fileStream); + } catch { + await fileStream.abort(); + } + } else { + const anchor = document.createElement('a'); + anchor.href = file.decrypted; + anchor.download = file.cleanName + '.' + file.ext; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + } } diff --git a/src/components/DownloadBase64.tsx b/src/components/DownloadBase64.tsx index e7a13c3..33ca9ae 100644 --- a/src/components/DownloadBase64.tsx +++ b/src/components/DownloadBase64.tsx @@ -7,7 +7,7 @@ export type DownloadBase64Props = { filename: string; mimetype?: string; className?: string; - icon?: boolean | ReactNode; + icon?: ReactNode | true | false; children?: ReactNode; }; diff --git a/src/components/ExtLink.tsx b/src/components/ExtLink.tsx index 4d6a6de..7d3ba3c 100644 --- a/src/components/ExtLink.tsx +++ b/src/components/ExtLink.tsx @@ -2,7 +2,7 @@ import type { AnchorHTMLAttributes, ReactNode } from 'react'; import { FiExternalLink } from 'react-icons/fi'; export type ExtLinkProps = AnchorHTMLAttributes & { - icon?: boolean | ReactNode; + icon?: ReactNode | true | false; }; export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) { diff --git a/src/components/ImportSecretModal.tsx b/src/components/ImportSecretModal.tsx index dbc275b..dde33fa 100644 --- a/src/components/ImportSecretModal.tsx +++ b/src/components/ImportSecretModal.tsx @@ -13,13 +13,12 @@ export interface ImportSecretModalProps { export function ImportSecretModal({ clientName, children, show, onClose, onImport }: ImportSecretModalProps) { const handleFileReceived = (files: File[]) => { - const promise = onImport(files[0]); - if (promise instanceof Promise) { - promise.catch((err) => { + const importResult = onImport(files[0]); + if (importResult instanceof Promise) { + importResult.catch((err) => { console.error('could not import: ', err); }); } - return promise; }; const refModel = useRef(null); diff --git a/src/components/SDKVersion.tsx b/src/components/SDKVersion.tsx index 8537fb5..f5508df 100644 --- a/src/components/SDKVersion.tsx +++ b/src/components/SDKVersion.tsx @@ -11,7 +11,7 @@ export function SDKVersion() { const refDialog = useRef(null); const [sdkVersion, setSdkVersion] = useState('...'); useEffect(() => { - getSDKVersion().then(setSdkVersion); + getSDKVersion().then(setSdkVersion, () => setSdkVersion('N/A')); }, []); return ( diff --git a/src/components/SelectFile.tsx b/src/components/SelectFile.tsx index 0e5e87c..de9f565 100644 --- a/src/components/SelectFile.tsx +++ b/src/components/SelectFile.tsx @@ -27,7 +27,10 @@ export function SelectFile() { fileName, }), ); - dispatch(processFile({ fileId })); + + dispatch(processFile({ fileId })).catch((err) => { + console.log(`failed to add file (id=${fileId}, name=${fileName}, err=${err as Error})`); + }); } }; diff --git a/src/decrypt-worker/decipher/KugouMusic.ts b/src/decrypt-worker/decipher/KugouMusic.ts index f5cebcc..6d9bed9 100644 --- a/src/decrypt-worker/decipher/KugouMusic.ts +++ b/src/decrypt-worker/decipher/KugouMusic.ts @@ -19,18 +19,18 @@ export class KugouMusicDecipher implements DecipherInstance { kgm.decrypt(block, offset); } - return { + return Promise.resolve({ status: Status.OK, cipherName: this.cipherName, data: audioBuffer, - }; + }); } finally { kgmHdr?.free(); kgm?.free(); } } - public static make() { + public static make(this: void) { return new KugouMusicDecipher(); } } diff --git a/src/decrypt-worker/decipher/KuwoMusic.ts b/src/decrypt-worker/decipher/KuwoMusic.ts index b3e7369..72d20d7 100644 --- a/src/decrypt-worker/decipher/KuwoMusic.ts +++ b/src/decrypt-worker/decipher/KuwoMusic.ts @@ -18,18 +18,18 @@ export class KuwoMusicDecipher implements DecipherInstance { for (const [block, offset] of chunkBuffer(audioBuffer)) { kwm.decrypt(block, offset); } - return { + return Promise.resolve({ status: Status.OK, cipherName: this.cipherName, data: audioBuffer, - }; + }); } finally { kwm?.free(); header?.free(); } } - public static make() { + public static make(this: void) { return new KuwoMusicDecipher(); } } diff --git a/src/decrypt-worker/decipher/Migu3d.ts b/src/decrypt-worker/decipher/Migu3d.ts index 4a793f0..2703f78 100644 --- a/src/decrypt-worker/decipher/Migu3d.ts +++ b/src/decrypt-worker/decipher/Migu3d.ts @@ -6,22 +6,25 @@ export class Migu3DKeylessDecipher implements DecipherInstance { cipherName = 'Migu3D (Keyless)'; async decrypt(buffer: Uint8Array): Promise { - const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100)); const audioBuffer = new Uint8Array(buffer); + const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100)); - for (const [block, i] of chunkBuffer(audioBuffer)) { - mg3d.decrypt(block, i); + try { + for (const [block, i] of chunkBuffer(audioBuffer)) { + mg3d.decrypt(block, i); + } + } finally { + mg3d.free(); } - mg3d.free(); - return { + return Promise.resolve({ cipherName: this.cipherName, status: Status.OK, data: audioBuffer, - }; + }); } - public static make() { + public static make(this: void) { return new Migu3DKeylessDecipher(); } } diff --git a/src/decrypt-worker/decipher/NetEaseCloudMusic.ts b/src/decrypt-worker/decipher/NetEaseCloudMusic.ts index f3ecd1f..f7719ac 100644 --- a/src/decrypt-worker/decipher/NetEaseCloudMusic.ts +++ b/src/decrypt-worker/decipher/NetEaseCloudMusic.ts @@ -26,17 +26,17 @@ export class NetEaseCloudMusicDecipher implements DecipherInstance { for (const [block, offset] of chunkBuffer(audioBuffer)) { ncm.decrypt(block, offset); } - return { + return Promise.resolve({ status: Status.OK, cipherName: this.cipherName, data: audioBuffer, - }; + }); } finally { ncm.free(); } } - public static make() { + public static make(this: void) { return new NetEaseCloudMusicDecipher(); } } diff --git a/src/decrypt-worker/decipher/QQMusic.ts b/src/decrypt-worker/decipher/QQMusic.ts index 111cb99..88a0848 100644 --- a/src/decrypt-worker/decipher/QQMusic.ts +++ b/src/decrypt-worker/decipher/QQMusic.ts @@ -19,14 +19,14 @@ export class QQMusicV1Decipher implements DecipherInstance { for (const [block, offset] of chunkBuffer(audioBuffer)) { decryptQMC1(block, offset); } - return { + return Promise.resolve({ status: Status.OK, cipherName: this.cipherName, data: audioBuffer, - }; + }); } - public static create() { + public static create(this: void) { return new QQMusicV1Decipher(); } } @@ -62,25 +62,28 @@ export class QQMusicV2Decipher implements DecipherInstance { throw new Error('EKey required'); } - const qmc2 = new QMC2(ekey); const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size); - for (const [block, offset] of chunkBuffer(audioBuffer)) { - qmc2.decrypt(block, offset); + const qmc2 = new QMC2(ekey); + try { + for (const [block, offset] of chunkBuffer(audioBuffer)) { + qmc2.decrypt(block, offset); + } + } finally { + qmc2.free(); } - qmc2.free(); - return { + return Promise.resolve({ status: Status.OK, cipherName: this.cipherName, data: audioBuffer, - }; + }); } - public static createWithUserKey() { + public static createWithUserKey(this: void) { return new QQMusicV2Decipher(true); } - public static createWithEmbeddedEKey() { + public static createWithEmbeddedEKey(this: void) { return new QQMusicV2Decipher(false); } } diff --git a/src/decrypt-worker/decipher/QingTingFM.ts b/src/decrypt-worker/decipher/QingTingFM.ts index f128e65..a19e410 100644 --- a/src/decrypt-worker/decipher/QingTingFM.ts +++ b/src/decrypt-worker/decipher/QingTingFM.ts @@ -18,20 +18,23 @@ export class QignTingFMDecipher implements DecipherInstance { }; } - const qtfm = new QingTingFM(key, iv); const audioBuffer = new Uint8Array(buffer); - for (const [block, i] of chunkBuffer(audioBuffer)) { - qtfm.decrypt(block, i); + const qtfm = new QingTingFM(key, iv); + try { + for (const [block, i] of chunkBuffer(audioBuffer)) { + qtfm.decrypt(block, i); + } + } finally { + qtfm.free(); } - - return { + return Promise.resolve({ cipherName: this.cipherName, status: Status.OK, data: audioBuffer, - }; + }); } - public static make() { + public static make(this: void) { return new QignTingFMDecipher(); } } diff --git a/src/decrypt-worker/decipher/Transparent.ts b/src/decrypt-worker/decipher/Transparent.ts index fd275c2..00d4d2d 100644 --- a/src/decrypt-worker/decipher/Transparent.ts +++ b/src/decrypt-worker/decipher/Transparent.ts @@ -4,15 +4,15 @@ export class TransparentDecipher implements DecipherInstance { cipherName = 'none'; async decrypt(buffer: Uint8Array): Promise { - return { + return Promise.resolve({ cipherName: 'None', status: Status.OK, data: buffer, message: 'No decipher applied', - }; + }); } - public static make() { + public static make(this: void) { return new TransparentDecipher(); } } diff --git a/src/decrypt-worker/decipher/XiamiMusic.ts b/src/decrypt-worker/decipher/XiamiMusic.ts index 778d916..063f525 100644 --- a/src/decrypt-worker/decipher/XiamiMusic.ts +++ b/src/decrypt-worker/decipher/XiamiMusic.ts @@ -6,23 +6,26 @@ export class XiamiDecipher implements DecipherInstance { cipherName = 'Xiami (XM)'; async decrypt(buffer: Uint8Array): Promise { - const xm = Xiami.from_header(buffer.subarray(0, 0x10)); - const { copyPlainLength } = xm; const audioBuffer = buffer.slice(0x10); - for (const [block] of chunkBuffer(audioBuffer.subarray(copyPlainLength))) { - xm.decrypt(block); + const xm = Xiami.from_header(buffer.subarray(0, 0x10)); + try { + const { copyPlainLength } = xm; + for (const [block] of chunkBuffer(audioBuffer.subarray(copyPlainLength))) { + xm.decrypt(block); + } + } finally { + xm.free(); } - xm.free(); - return { + return Promise.resolve({ cipherName: this.cipherName, status: Status.OK, data: audioBuffer, - }; + }); } - public static make() { + public static make(this: void) { return new XiamiDecipher(); } } diff --git a/src/decrypt-worker/decipher/Ximalaya.ts b/src/decrypt-worker/decipher/Ximalaya.ts index 17edc4c..64c56e1 100644 --- a/src/decrypt-worker/decipher/Ximalaya.ts +++ b/src/decrypt-worker/decipher/Ximalaya.ts @@ -23,18 +23,18 @@ export class XimalayaAndroidDecipher implements DecipherInstance { } const result = new Uint8Array(buffer); result.set(slice, 0); - return { + return Promise.resolve({ cipherName: this.cipherName, status: Status.OK, data: result, - }; + }); } - public static makeX2M() { + public static makeX2M(this: void) { return new XimalayaAndroidDecipher(decryptX2MHeader, 'X2M'); } - public static makeX3M() { + public static makeX3M(this: void) { return new XimalayaAndroidDecipher(decryptX3MHeader, 'X3M'); } } @@ -45,27 +45,31 @@ export class XimalayaPCDecipher implements DecipherInstance { async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise { // Detect with first 0x400 bytes const headerSize = XmlyPC.getHeaderSize(buffer.subarray(0, 1024)); - const xm = new XmlyPC(buffer.subarray(0, headerSize)); - const { audioHeader, encryptedHeaderOffset, encryptedHeaderSize } = xm; - const plainAudioDataOffset = encryptedHeaderOffset + encryptedHeaderSize; - const plainAudioDataLength = buffer.byteLength - plainAudioDataOffset; - const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset); - const encryptedAudioPartLen = xm.decrypt(encryptedAudioPart); - const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength; - xm.free(); + const xmly = new XmlyPC(buffer.subarray(0, headerSize)); - const result = new Uint8Array(audioSize); - result.set(audioHeader); - result.set(encryptedAudioPart, audioHeader.byteLength); - result.set(buffer.subarray(plainAudioDataOffset), audioHeader.byteLength + encryptedAudioPartLen); - return { - status: Status.OK, - data: result, - cipherName: this.cipherName, - }; + try { + const { audioHeader, encryptedHeaderOffset, encryptedHeaderSize } = xmly; + const plainAudioDataOffset = encryptedHeaderOffset + encryptedHeaderSize; + const plainAudioDataLength = buffer.byteLength - plainAudioDataOffset; + const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset); + const encryptedAudioPartLen = xmly.decrypt(encryptedAudioPart); + const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength; + + const result = new Uint8Array(audioSize); + result.set(audioHeader); + result.set(encryptedAudioPart, audioHeader.byteLength); + result.set(buffer.subarray(plainAudioDataOffset), audioHeader.byteLength + encryptedAudioPartLen); + return Promise.resolve({ + status: Status.OK, + data: result, + cipherName: this.cipherName, + }); + } finally { + xmly.free(); + } } - public static make() { + public static make(this: void) { return new XimalayaPCDecipher(); } } diff --git a/src/decrypt-worker/util/wasmClass.ts b/src/decrypt-worker/util/wasmClass.ts index b5f38d0..8744b4c 100644 --- a/src/decrypt-worker/util/wasmClass.ts +++ b/src/decrypt-worker/util/wasmClass.ts @@ -6,7 +6,7 @@ export function withWasmClass void }, R>(instance: T, cb const resp = cb(instance); if (resp && isPromise(resp)) { isAsync = true; - resp.finally(() => instance.free()); + resp.finally(() => instance.free()).catch(() => {}); } return resp; } finally { diff --git a/src/decrypt-worker/worker/qtfm_device_key.ts b/src/decrypt-worker/worker/qtfm_device_key.ts index 03a7730..357eb3d 100644 --- a/src/decrypt-worker/worker/qtfm_device_key.ts +++ b/src/decrypt-worker/worker/qtfm_device_key.ts @@ -11,5 +11,5 @@ export async function workerGetQtfmDeviceKey({ board, }: GetQingTingFMDeviceKeyPayload) { const buffer = QingTingFM.getDeviceKey(device, brand, model, product, manufacturer, board); - return hex(buffer); + return Promise.resolve(hex(buffer)); } diff --git a/src/dummy.mjs b/src/dummy.mjs index bdd93cf..193d30f 100644 --- a/src/dummy.mjs +++ b/src/dummy.mjs @@ -1,5 +1,5 @@ // This is a dummy module for vite/rollup to resolve. export function createRequire() { - import('radash'); // we need to import something, so vite don't complain on build + const _ = import('radash'); // we need to import something, so vite don't complain on build throw new Error('this is a dummy module. Do not use'); } diff --git a/src/features/file-listing/FileError.tsx b/src/features/file-listing/FileError.tsx index cfad5ea..81af02c 100644 --- a/src/features/file-listing/FileError.tsx +++ b/src/features/file-listing/FileError.tsx @@ -26,14 +26,10 @@ export function FileError({ error, code }: FileErrorProps) { const copyError = () => { if (error) { - navigator.clipboard - .writeText(applyTemplate(ERROR_TEMPLATE, { summary, error })) - .then(() => { - toast.success('错误信息已复制到剪贴板'); - }) - .catch((e) => { - toast.error(`复制错误信息失败: ${e}`); - }); + navigator.clipboard.writeText(applyTemplate(ERROR_TEMPLATE, { summary, error })).then( + () => toast.success('错误信息已复制到剪贴板'), + (e) => toast.error(`复制错误信息失败: ${e as Error}`), + ); } }; diff --git a/src/features/settings/panels/Kugou/InstructionsPC.tsx b/src/features/settings/panels/Kugou/InstructionsPC.tsx index 5252a6f..f1035e6 100644 --- a/src/features/settings/panels/Kugou/InstructionsPC.tsx +++ b/src/features/settings/panels/Kugou/InstructionsPC.tsx @@ -1,20 +1,11 @@ import { RiFileCopyLine } from 'react-icons/ri'; -import { toast } from 'react-toastify'; import { ExtLink } from '~/components/ExtLink'; import { FilePathBlock } from '~/components/FilePathBlock.tsx'; +import { copyToClipboard } from '~/util/clipboard'; +const DB_PATH = '%APPDATA%\\KuGou8\\KGMusicV3.db'; 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}`); - }); - }; + const copyDbPathToClipboard = () => copyToClipboard(DB_PATH); return ( <> diff --git a/src/features/settings/panels/PanelQMCv2Key.tsx b/src/features/settings/panels/PanelQMCv2Key.tsx index 1bffcf5..342a69c 100644 --- a/src/features/settings/panels/PanelQMCv2Key.tsx +++ b/src/features/settings/panels/PanelQMCv2Key.tsx @@ -53,7 +53,7 @@ export function PanelQMCv2Key() { toastImportResult(file.name, keys); } catch (e) { console.error('error during import: ', e); - alert(`导入数据库时发生错误:${e}`); + alert(`导入数据库时发生错误:${e as Error}`); } }; diff --git a/src/features/settings/panels/PanelQingTing.tsx b/src/features/settings/panels/PanelQingTing.tsx index b87212c..6c25b0e 100644 --- a/src/features/settings/panels/PanelQingTing.tsx +++ b/src/features/settings/panels/PanelQingTing.tsx @@ -9,6 +9,7 @@ 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'; +import { toast } from 'react-toastify'; const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest'; @@ -28,23 +29,20 @@ export function PanelQingTing() { return; } - const dataMap = Object.create(null); + const dataMap = Object.create(null) as GetQingTingFMDeviceKeyPayload; for (const [, key, value] of plainText.matchAll(/^(PRODUCT|DEVICE|MANUFACTURER|BRAND|BOARD|MODEL): (.+)/gim)) { - dataMap[key.toLowerCase()] = value; + dataMap[key.toLowerCase() as keyof GetQingTingFMDeviceKeyPayload] = value; } const { product, device, manufacturer, brand, board, model } = dataMap; if (product && device && manufacturer && brand && board && model) { e.preventDefault(); workerClientBus - .request( - DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, - dataMap, - ) - .then(setSecretKey) - .catch((err) => { - alert(`生成设备密钥时发生错误: ${err}`); - }); + .request< + string, + GetQingTingFMDeviceKeyPayload + >(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, dataMap) + .then(setSecretKey, (err) => toast.error(`生成设备密钥时发生错误: ${err}`)); } }; diff --git a/src/features/settings/persistSettings.ts b/src/features/settings/persistSettings.ts index 21f4fc7..314aaeb 100644 --- a/src/features/settings/persistSettings.ts +++ b/src/features/settings/persistSettings.ts @@ -55,7 +55,7 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE let lastSettings: unknown; try { - const loadedSettings: ProductionSettings = JSON.parse(localStorage.getItem(storageKey) ?? ''); + const loadedSettings = JSON.parse(localStorage.getItem(storageKey) ?? '') as ProductionSettings; if (loadedSettings) { const mergedSettings = mergeSettings(loadedSettings); store.dispatch(setProductionChanges(mergedSettings)); diff --git a/src/test-utils/setup-jest.ts b/src/test-utils/setup-jest.ts index b93e9b1..ff01aa0 100644 --- a/src/test-utils/setup-jest.ts +++ b/src/test-utils/setup-jest.ts @@ -3,8 +3,9 @@ import '@testing-library/jest-dom'; // FIXME: Use something like jsdom-worker? // see: https://github.com/developit/jsdom-worker if (!global.Worker) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access (global as any).Worker = class MockWorker { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment events: Record void> = Object.create(null); onmessage = undefined; diff --git a/src/util/SimpleQueue.ts b/src/util/SimpleQueue.ts new file mode 100644 index 0000000..0a64920 --- /dev/null +++ b/src/util/SimpleQueue.ts @@ -0,0 +1,28 @@ +export class SimpleQueue { + private queue: (() => void)[] = []; + private running = 0; + + constructor(private concurrency: number) {} + + async enter() { + return new Promise((resolve) => { + this.queue.push(resolve); + setTimeout(this.next); + }); + } + + leave() { + this.running--; + setTimeout(this.next); + } + + private next = () => { + while (this.running < this.concurrency && this.queue.length > 0) { + const fn = this.queue.shift(); + if (fn) { + this.running++; + setTimeout(fn); + } + } + }; +} diff --git a/src/util/WorkerEventBus.ts b/src/util/WorkerEventBus.ts index 54ad649..32b8568 100644 --- a/src/util/WorkerEventBus.ts +++ b/src/util/WorkerEventBus.ts @@ -50,7 +50,7 @@ export class WorkerClientBus { async request(actionName: T, payload: P): Promise { return new Promise((resolve, reject) => { - const id = `request://${actionName}/${nanoid()}`; + const id = `request://${actionName as string}/${nanoid()}`; this.idPromiseMap.set(id, [resolve, reject]); this.worker.postMessage({ id, diff --git a/src/util/__tests__/ConcurrentQueue.test.ts b/src/util/__tests__/ConcurrentQueue.test.ts index 0231bd0..50012de 100644 --- a/src/util/__tests__/ConcurrentQueue.test.ts +++ b/src/util/__tests__/ConcurrentQueue.test.ts @@ -2,7 +2,7 @@ import { ConcurrentQueue } from '../ConcurrentQueue'; import { nextTickAsync } from '../nextTick'; class SimpleQueue extends ConcurrentQueue { - handler(_item: T): Promise { + handler(this: void, _item: T): Promise { throw new Error('Method not overridden'); } } diff --git a/src/util/__tests__/DecryptionQueue.test.ts b/src/util/__tests__/DecryptionQueue.test.ts index c4702af..96f2609 100644 --- a/src/util/__tests__/DecryptionQueue.test.ts +++ b/src/util/__tests__/DecryptionQueue.test.ts @@ -17,7 +17,7 @@ test('should be able to forward request to worker client bus', async () => { const bus = new WorkerClientBus(null as never); vi.spyOn(bus, 'request').mockImplementation( async (actionName: DECRYPTION_WORKER_ACTION_NAME, payload: unknown): Promise => { - return { actionName, payload }; + return Promise.resolve({ actionName, payload }); }, ); diff --git a/src/util/applyTemplate.ts b/src/util/applyTemplate.ts index 9a8a091..3f8dd58 100644 --- a/src/util/applyTemplate.ts +++ b/src/util/applyTemplate.ts @@ -1,3 +1,5 @@ export function applyTemplate(tpl: string, values: Record) { - return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => (Object.hasOwn(values, key) ? String(values[key]) : '')); + return tpl.replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key: string) => + Object.hasOwn(values, key) ? String(values[key]) : '', + ); } diff --git a/src/util/clipboard.ts b/src/util/clipboard.ts index c9eace0..2f30a3a 100644 --- a/src/util/clipboard.ts +++ b/src/util/clipboard.ts @@ -1,12 +1,8 @@ import { toast } from 'react-toastify'; export const copyToClipboard = (text: string) => { - navigator.clipboard - .writeText(text) - .then(() => { - toast.success('已复制到剪贴板'); - }) - .catch((err) => { - toast.error(`复制失败,请手动复制\n${err}`); - }); + navigator.clipboard.writeText(text).then( + () => toast.success('已复制到剪贴板'), + (err) => toast.error(`复制失败,请手动复制。\n错误: ${err as Error}`), + ); }; diff --git a/src/util/deepClone.ts b/src/util/deepClone.ts index e8971ac..c8719e7 100644 --- a/src/util/deepClone.ts +++ b/src/util/deepClone.ts @@ -1,3 +1,3 @@ export function deepClone(obj: T): T { - return JSON.parse(JSON.stringify(obj)); + return JSON.parse(JSON.stringify(obj)) as T; }