refactor: major package updates

This commit is contained in:
鲁树人
2025-10-16 01:00:38 +09:00
parent 2e52c67533
commit a11caaae92
34 changed files with 373 additions and 240 deletions

View File

@@ -1,15 +1,16 @@
import eslint from '@eslint/js'; import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint'; import tseslint from 'typescript-eslint';
import reactRefresh from 'eslint-plugin-react-refresh'; import reactRefresh from 'eslint-plugin-react-refresh';
import reactHooks from 'eslint-plugin-react-hooks'; import reactHooks from 'eslint-plugin-react-hooks';
import eslintConfigPrettier from 'eslint-config-prettier/flat'; import eslintConfigPrettier from 'eslint-config-prettier/flat';
import globals from 'globals'; import globals from 'globals';
export default tseslint.config( export default defineConfig(
eslint.configs.recommended, eslint.configs.recommended,
tseslint.configs.recommended, tseslint.configs.recommendedTypeChecked,
reactRefresh.configs.recommended, reactRefresh.configs.recommended,
reactHooks.configs['recommended-latest'], reactHooks.configs.flat.recommended,
eslintConfigPrettier, eslintConfigPrettier,
{ {
@@ -40,4 +41,14 @@ export default tseslint.config(
}, },
}, },
}, },
{
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['*.mjs', 'src/*.mjs', 'scripts/*.mjs'],
},
},
},
},
); );

View File

@@ -54,11 +54,11 @@
"daisyui": "^5.3.2", "daisyui": "^5.3.2",
"eslint": "^9.37.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "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", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.4.0", "globals": "^16.4.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"jsdom": "^26.1.0", "jsdom": "^27.0.0",
"lint-staged": "^16.2.4", "lint-staged": "^16.2.4",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"rollup": "^4.52.4", "rollup": "^4.52.4",

222
pnpm-lock.yaml generated
View File

@@ -123,8 +123,8 @@ importers:
specifier: ^10.1.8 specifier: ^10.1.8
version: 10.1.8(eslint@9.37.0(jiti@2.6.1)) version: 10.1.8(eslint@9.37.0(jiti@2.6.1))
eslint-plugin-react-hooks: eslint-plugin-react-hooks:
specifier: ^5.2.0 specifier: ^7.0.0
version: 5.2.0(eslint@9.37.0(jiti@2.6.1)) version: 7.0.0(eslint@9.37.0(jiti@2.6.1))
eslint-plugin-react-refresh: eslint-plugin-react-refresh:
specifier: ^0.4.24 specifier: ^0.4.24
version: 0.4.24(eslint@9.37.0(jiti@2.6.1)) version: 0.4.24(eslint@9.37.0(jiti@2.6.1))
@@ -135,8 +135,8 @@ importers:
specifier: ^9.1.7 specifier: ^9.1.7
version: 9.1.7 version: 9.1.7
jsdom: jsdom:
specifier: ^26.1.0 specifier: ^27.0.0
version: 26.1.0 version: 27.0.0(postcss@8.5.6)
lint-staged: lint-staged:
specifier: ^16.2.4 specifier: ^16.2.4
version: 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)) 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: vitest:
specifier: ^3.2.4 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: workbox-build:
specifier: ^7.3.0 specifier: ^7.3.0
version: 7.3.0(@types/babel__core@7.20.5) version: 7.3.0(@types/babel__core@7.20.5)
@@ -204,8 +204,14 @@ packages:
peerDependencies: peerDependencies:
ajv: '>=8' ajv: '>=8'
'@asamuzakjp/css-color@3.2.0': '@asamuzakjp/css-color@4.0.5':
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} 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': '@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
@@ -742,6 +748,12 @@ packages:
peerDependencies: peerDependencies:
'@csstools/css-tokenizer': ^3.0.4 '@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': '@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1796,6 +1808,9 @@ packages:
resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==} resolution: {integrity: sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==}
hasBin: true hasBin: true
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
brace-expansion@1.1.12: brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -1926,12 +1941,16 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'} 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: css.escape@1.5.1:
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
cssstyle@4.6.0: cssstyle@5.3.1:
resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} resolution: {integrity: sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==}
engines: {node: '>=18'} engines: {node: '>=20'}
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
@@ -1939,9 +1958,9 @@ packages:
daisyui@5.3.2: daisyui@5.3.2:
resolution: {integrity: sha512-hw6NmQvFHdZI1Zb94EX+vA0DhuXGfqwQInDQ6XNZ5dgkoDy+e01P4OkJ8umAQAAzVSKGwm6QDMXOw6eJV63OEQ==} resolution: {integrity: sha512-hw6NmQvFHdZI1Zb94EX+vA0DhuXGfqwQInDQ6XNZ5dgkoDy+e01P4OkJ8umAQAAzVSKGwm6QDMXOw6eJV63OEQ==}
data-urls@5.0.0: data-urls@6.0.0:
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==}
engines: {node: '>=18'} engines: {node: '>=20'}
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
@@ -2087,9 +2106,9 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=7.0.0' eslint: '>=7.0.0'
eslint-plugin-react-hooks@5.2.0: eslint-plugin-react-hooks@7.0.0:
resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} resolution: {integrity: sha512-fNXaOwvKwq2+pXiRpXc825Vd63+KM4DLL40Rtlycb8m7fYpp6efrTp1sa6ZbP/Ap58K2bEKFXRmhURE+CJAQWw==}
engines: {node: '>=10'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 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: hastscript@6.0.0:
resolution: {integrity: sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==} 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: highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@@ -2606,9 +2631,9 @@ packages:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
hasBin: true hasBin: true
jsdom@26.1.0: jsdom@27.0.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} resolution: {integrity: sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==}
engines: {node: '>=18'} engines: {node: '>=20'}
peerDependencies: peerDependencies:
canvas: ^3.0.0 canvas: ^3.0.0
peerDependenciesMeta: peerDependenciesMeta:
@@ -2764,6 +2789,10 @@ packages:
lru-cache@10.4.3: lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 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: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -2788,6 +2817,9 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
mdn-data@2.12.2:
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
merge2@1.4.1: merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2853,9 +2885,6 @@ packages:
node-releases@2.0.23: node-releases@2.0.23:
resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==}
nwsapi@2.2.22:
resolution: {integrity: sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==}
object-assign@4.1.1: object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -3426,11 +3455,11 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
tldts-core@6.1.86: tldts-core@7.0.17:
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} resolution: {integrity: sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==}
tldts@6.1.86: tldts@7.0.17:
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} resolution: {integrity: sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==}
hasBin: true hasBin: true
to-regex-range@5.0.1: to-regex-range@5.0.1:
@@ -3441,16 +3470,16 @@ packages:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
tough-cookie@5.1.2: tough-cookie@6.0.0:
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
engines: {node: '>=16'} engines: {node: '>=16'}
tr46@1.0.1: tr46@1.0.1:
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
tr46@5.1.1: tr46@6.0.0:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=18'} engines: {node: '>=20'}
ts-api-utils@2.1.0: ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
@@ -3652,9 +3681,9 @@ packages:
webidl-conversions@4.0.2: webidl-conversions@4.0.2:
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
webidl-conversions@7.0.0: webidl-conversions@8.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} resolution: {integrity: sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==}
engines: {node: '>=12'} engines: {node: '>=20'}
whatwg-encoding@3.1.1: whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
@@ -3664,9 +3693,9 @@ packages:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'} engines: {node: '>=18'}
whatwg-url@14.2.0: whatwg-url@15.1.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
engines: {node: '>=18'} engines: {node: '>=20'}
whatwg-url@7.1.0: whatwg-url@7.1.0:
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
@@ -3804,6 +3833,15 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'} 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: snapshots:
'@adobe/css-tools@4.4.4': {} '@adobe/css-tools@4.4.4': {}
@@ -3820,13 +3858,23 @@ snapshots:
jsonpointer: 5.0.1 jsonpointer: 5.0.1
leven: 3.1.0 leven: 3.1.0
'@asamuzakjp/css-color@3.2.0': '@asamuzakjp/css-color@4.0.5':
dependencies: 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-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-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-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
'@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': '@babel/code-frame@7.27.1':
dependencies: dependencies:
@@ -4512,6 +4560,10 @@ snapshots:
dependencies: dependencies:
'@csstools/css-tokenizer': 3.0.4 '@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': {} '@csstools/css-tokenizer@3.0.4': {}
'@esbuild/aix-ppc64@0.25.11': '@esbuild/aix-ppc64@0.25.11':
@@ -5285,7 +5337,7 @@ snapshots:
std-env: 3.10.0 std-env: 3.10.0
test-exclude: 7.0.1 test-exclude: 7.0.1
tinyrainbow: 2.0.0 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: transitivePeerDependencies:
- supports-color - supports-color
@@ -5334,7 +5386,7 @@ snapshots:
sirv: 3.0.2 sirv: 3.0.2
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinyrainbow: 2.0.0 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': '@vitest/utils@3.2.4':
dependencies: dependencies:
@@ -5455,6 +5507,10 @@ snapshots:
baseline-browser-mapping@2.8.16: {} baseline-browser-mapping@2.8.16: {}
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
brace-expansion@1.1.12: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -5575,21 +5631,29 @@ snapshots:
crypto-random-string@2.0.0: {} 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: {} css.escape@1.5.1: {}
cssstyle@4.6.0: cssstyle@5.3.1(postcss@8.5.6):
dependencies: dependencies:
'@asamuzakjp/css-color': 3.2.0 '@asamuzakjp/css-color': 4.0.5
rrweb-cssom: 0.8.0 '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6)
css-tree: 3.1.0
transitivePeerDependencies:
- postcss
csstype@3.1.3: {} csstype@3.1.3: {}
daisyui@5.3.2: {} daisyui@5.3.2: {}
data-urls@5.0.0: data-urls@6.0.0:
dependencies: dependencies:
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0 whatwg-url: 15.1.0
data-view-buffer@1.0.2: data-view-buffer@1.0.2:
dependencies: dependencies:
@@ -5790,9 +5854,16 @@ snapshots:
dependencies: dependencies:
eslint: 9.37.0(jiti@2.6.1) 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: dependencies:
'@babel/core': 7.28.4
'@babel/parser': 7.28.4
eslint: 9.37.0(jiti@2.6.1) 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)): eslint-plugin-react-refresh@0.4.24(eslint@9.37.0(jiti@2.6.1)):
dependencies: dependencies:
@@ -6086,6 +6157,12 @@ snapshots:
property-information: 5.6.0 property-information: 5.6.0
space-separated-tokens: 1.1.5 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: {} highlight.js@10.7.3: {}
highlightjs-vue@1.0.0: {} highlightjs-vue@1.0.0: {}
@@ -6330,30 +6407,31 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
jsdom@26.1.0: jsdom@27.0.0(postcss@8.5.6):
dependencies: dependencies:
cssstyle: 4.6.0 '@asamuzakjp/dom-selector': 6.6.2
data-urls: 5.0.0 cssstyle: 5.3.1(postcss@8.5.6)
data-urls: 6.0.0
decimal.js: 10.6.0 decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0 html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2 http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6 https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1 is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.22
parse5: 7.3.0 parse5: 7.3.0
rrweb-cssom: 0.8.0 rrweb-cssom: 0.8.0
saxes: 6.0.0 saxes: 6.0.0
symbol-tree: 3.2.4 symbol-tree: 3.2.4
tough-cookie: 5.1.2 tough-cookie: 6.0.0
w3c-xmlserializer: 5.0.0 w3c-xmlserializer: 5.0.0
webidl-conversions: 7.0.0 webidl-conversions: 8.0.0
whatwg-encoding: 3.1.1 whatwg-encoding: 3.1.1
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0 whatwg-url: 15.1.0
ws: 8.18.3 ws: 8.18.3
xml-name-validator: 5.0.0 xml-name-validator: 5.0.0
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- postcss
- supports-color - supports-color
- utf-8-validate - utf-8-validate
@@ -6487,6 +6565,8 @@ snapshots:
lru-cache@10.4.3: {} lru-cache@10.4.3: {}
lru-cache@11.2.2: {}
lru-cache@5.1.1: lru-cache@5.1.1:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
@@ -6513,6 +6593,8 @@ snapshots:
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
mdn-data@2.12.2: {}
merge2@1.4.1: {} merge2@1.4.1: {}
micromatch@4.0.8: micromatch@4.0.8:
@@ -6559,8 +6641,6 @@ snapshots:
node-releases@2.0.23: {} node-releases@2.0.23: {}
nwsapi@2.2.22: {}
object-assign@4.1.1: {} object-assign@4.1.1: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
@@ -7192,11 +7272,11 @@ snapshots:
tinyspy@4.0.4: {} tinyspy@4.0.4: {}
tldts-core@6.1.86: {} tldts-core@7.0.17: {}
tldts@6.1.86: tldts@7.0.17:
dependencies: dependencies:
tldts-core: 6.1.86 tldts-core: 7.0.17
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
@@ -7204,15 +7284,15 @@ snapshots:
totalist@3.0.1: {} totalist@3.0.1: {}
tough-cookie@5.1.2: tough-cookie@6.0.0:
dependencies: dependencies:
tldts: 6.1.86 tldts: 7.0.17
tr46@1.0.1: tr46@1.0.1:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
tr46@5.1.1: tr46@6.0.0:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
@@ -7382,7 +7462,7 @@ snapshots:
terser: 5.44.0 terser: 5.44.0
yaml: 2.8.1 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: dependencies:
'@types/chai': 5.2.2 '@types/chai': 5.2.2
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
@@ -7410,7 +7490,7 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 24.7.2 '@types/node': 24.7.2
'@vitest/ui': 3.2.4(vitest@3.2.4) '@vitest/ui': 3.2.4(vitest@3.2.4)
jsdom: 26.1.0 jsdom: 27.0.0(postcss@8.5.6)
transitivePeerDependencies: transitivePeerDependencies:
- jiti - jiti
- less - less
@@ -7431,7 +7511,7 @@ snapshots:
webidl-conversions@4.0.2: {} webidl-conversions@4.0.2: {}
webidl-conversions@7.0.0: {} webidl-conversions@8.0.0: {}
whatwg-encoding@3.1.1: whatwg-encoding@3.1.1:
dependencies: dependencies:
@@ -7439,10 +7519,10 @@ snapshots:
whatwg-mimetype@4.0.0: {} whatwg-mimetype@4.0.0: {}
whatwg-url@14.2.0: whatwg-url@15.1.0:
dependencies: dependencies:
tr46: 5.1.1 tr46: 6.0.0
webidl-conversions: 7.0.0 webidl-conversions: 8.0.0
whatwg-url@7.1.0: whatwg-url@7.1.0:
dependencies: dependencies:
@@ -7650,3 +7730,9 @@ snapshots:
yaml@2.8.1: {} yaml@2.8.1: {}
yocto-queue@0.1.0: {} yocto-queue@0.1.0: {}
zod-validation-error@4.0.2(zod@4.1.12):
dependencies:
zod: 4.1.12
zod@4.1.12: {}

View File

@@ -2,83 +2,89 @@ import { DecryptedAudioFile, ProcessState, selectFiles } from '~/features/file-l
import { FaDownload } from 'react-icons/fa'; import { FaDownload } from 'react-icons/fa';
import { useAppSelector } from '~/hooks'; import { useAppSelector } from '~/hooks';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { SimpleQueue } from '~/util/SimpleQueue';
export function DownloadAll() { export function DownloadAll() {
const files = useAppSelector(selectFiles); const files = useAppSelector(selectFiles);
const onClickDownloadAll = async () => { const downloadAllAsync = async () => {
console.time('DownloadAll'); //开始计时 const fileList = Object.values(files);
const fileCount = Object.keys(files).length; const fileCount = fileList.length;
if (fileCount === 0) { if (fileCount === 0) {
toast.warning('未添加文件'); toast.warning('未添加文件');
return; return;
} }
// 判断所有文件是否处理完成 // 判断所有文件是否处理完成
const allComplete = Object.values(files).every((file) => file.state !== ProcessState.PROCESSING); const allComplete = fileList.every((file) => file.state !== ProcessState.PROCESSING);
if (!allComplete) { if (!allComplete) {
toast.warning('请等待所有文件解密完成'); toast.warning('请等待所有文件解密完成');
return; 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 { try {
dir = await window.showDirectoryPicker({ mode: 'readwrite' }); dir = await window.showDirectoryPicker({ mode: 'readwrite' });
} catch (e) { } catch (e) {
console.error(e);
if (e instanceof Error && e.name === 'AbortError') { if (e instanceof Error && e.name === 'AbortError') {
return; return; // user cancelled
} }
console.error(e);
} }
toast.warning('开始下载,请稍候'); toast.warning('开始下载,请稍候');
const queue = new SimpleQueue(8);
const promises = Object.values(completeFiles).map(async (file) => { const promises = Object.values(completeFiles).map(async (file) => {
console.log(`开始下载: ${file.fileName}`);
try { try {
if (dir) { await queue.enter();
await DownloadNew(dir, file); await downloadFile(file, dir);
} else {
await DownloadOld(file);
}
console.log(`成功下载: ${file.fileName}`);
} catch (e) { } catch (e) {
console.error(`下载失败: ${file.fileName}`, e); console.error(`下载失败: ${file.fileName}`, e);
toast.error(`出现错误: ${e}`); toast.error(`出现错误: ${e as Error}`);
throw e; throw e;
} finally {
queue.leave();
} }
}); });
await Promise.allSettled(promises).then((f) => {
const success = f.filter((result) => result.status === 'fulfilled').length; const promiseResults = await Promise.allSettled(promises);
if (success === fileCount) { const success = promiseResults.filter((result) => result.status === 'fulfilled').length;
toast.success(`成功下载: ${success}/${fileCount}`); const level = success === fileCount ? 'success' : success === 0 ? 'error' : 'warning';
} else { toast[level](`成功下载: ${success}/${fileCount}`);
toast.warning(`成功下载: ${success}/${fileCount}`);
}
});
console.timeEnd('DownloadAll'); //停止计时
}; };
function onDownloadAll() {
downloadAllAsync().catch((e) => {
// this should not happen
console.error('下载全部出现错误', e);
});
}
return ( return (
<button className="btn btn-primary" id="downloadAll" onClick={onClickDownloadAll} title="下载全部"> <button className="btn btn-primary" id="downloadAll" onClick={onDownloadAll} title="下载全部">
<FaDownload /> <FaDownload />
</button> </button>
); );
} }
async function DownloadNew(dir: FileSystemDirectoryHandle, file: DecryptedAudioFile) { async function downloadFile(file: DecryptedAudioFile, dir: FileSystemDirectoryHandle | null) {
if (dir) {
const fileHandle = await dir.getFileHandle(file.cleanName + '.' + file.ext, { create: true }); const fileHandle = await dir.getFileHandle(file.cleanName + '.' + file.ext, { create: true });
const writable = await fileHandle.createWritable(); const fileStream = await fileHandle.createWritable();
await fetch(file.decrypted).then((res) => res.body?.pipeTo(writable)); 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();
} }
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();
} }

View File

@@ -7,7 +7,7 @@ export type DownloadBase64Props = {
filename: string; filename: string;
mimetype?: string; mimetype?: string;
className?: string; className?: string;
icon?: boolean | ReactNode; icon?: ReactNode | true | false;
children?: ReactNode; children?: ReactNode;
}; };

View File

@@ -2,7 +2,7 @@ import type { AnchorHTMLAttributes, ReactNode } from 'react';
import { FiExternalLink } from 'react-icons/fi'; import { FiExternalLink } from 'react-icons/fi';
export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & { export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
icon?: boolean | ReactNode; icon?: ReactNode | true | false;
}; };
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) { export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {

View File

@@ -13,13 +13,12 @@ export interface ImportSecretModalProps {
export function ImportSecretModal({ clientName, children, show, onClose, onImport }: ImportSecretModalProps) { export function ImportSecretModal({ clientName, children, show, onClose, onImport }: ImportSecretModalProps) {
const handleFileReceived = (files: File[]) => { const handleFileReceived = (files: File[]) => {
const promise = onImport(files[0]); const importResult = onImport(files[0]);
if (promise instanceof Promise) { if (importResult instanceof Promise) {
promise.catch((err) => { importResult.catch((err) => {
console.error('could not import: ', err); console.error('could not import: ', err);
}); });
} }
return promise;
}; };
const refModel = useRef<HTMLDialogElement>(null); const refModel = useRef<HTMLDialogElement>(null);

View File

@@ -11,7 +11,7 @@ export function SDKVersion() {
const refDialog = useRef<HTMLDialogElement>(null); const refDialog = useRef<HTMLDialogElement>(null);
const [sdkVersion, setSdkVersion] = useState('...'); const [sdkVersion, setSdkVersion] = useState('...');
useEffect(() => { useEffect(() => {
getSDKVersion().then(setSdkVersion); getSDKVersion().then(setSdkVersion, () => setSdkVersion('N/A'));
}, []); }, []);
return ( return (

View File

@@ -27,7 +27,10 @@ export function SelectFile() {
fileName, fileName,
}), }),
); );
dispatch(processFile({ fileId }));
dispatch(processFile({ fileId })).catch((err) => {
console.log(`failed to add file (id=${fileId}, name=${fileName}, err=${err as Error})`);
});
} }
}; };

View File

@@ -19,18 +19,18 @@ export class KugouMusicDecipher implements DecipherInstance {
kgm.decrypt(block, offset); kgm.decrypt(block, offset);
} }
return { return Promise.resolve({
status: Status.OK, status: Status.OK,
cipherName: this.cipherName, cipherName: this.cipherName,
data: audioBuffer, data: audioBuffer,
}; });
} finally { } finally {
kgmHdr?.free(); kgmHdr?.free();
kgm?.free(); kgm?.free();
} }
} }
public static make() { public static make(this: void) {
return new KugouMusicDecipher(); return new KugouMusicDecipher();
} }
} }

View File

@@ -18,18 +18,18 @@ export class KuwoMusicDecipher implements DecipherInstance {
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {
kwm.decrypt(block, offset); kwm.decrypt(block, offset);
} }
return { return Promise.resolve({
status: Status.OK, status: Status.OK,
cipherName: this.cipherName, cipherName: this.cipherName,
data: audioBuffer, data: audioBuffer,
}; });
} finally { } finally {
kwm?.free(); kwm?.free();
header?.free(); header?.free();
} }
} }
public static make() { public static make(this: void) {
return new KuwoMusicDecipher(); return new KuwoMusicDecipher();
} }
} }

View File

@@ -6,22 +6,25 @@ export class Migu3DKeylessDecipher implements DecipherInstance {
cipherName = 'Migu3D (Keyless)'; cipherName = 'Migu3D (Keyless)';
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100));
const audioBuffer = new Uint8Array(buffer); const audioBuffer = new Uint8Array(buffer);
const mg3d = Migu3D.fromHeader(buffer.subarray(0, 0x100));
try {
for (const [block, i] of chunkBuffer(audioBuffer)) { for (const [block, i] of chunkBuffer(audioBuffer)) {
mg3d.decrypt(block, i); mg3d.decrypt(block, i);
} }
} finally {
mg3d.free(); mg3d.free();
}
return { return Promise.resolve({
cipherName: this.cipherName, cipherName: this.cipherName,
status: Status.OK, status: Status.OK,
data: audioBuffer, data: audioBuffer,
}; });
} }
public static make() { public static make(this: void) {
return new Migu3DKeylessDecipher(); return new Migu3DKeylessDecipher();
} }
} }

View File

@@ -26,17 +26,17 @@ export class NetEaseCloudMusicDecipher implements DecipherInstance {
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {
ncm.decrypt(block, offset); ncm.decrypt(block, offset);
} }
return { return Promise.resolve({
status: Status.OK, status: Status.OK,
cipherName: this.cipherName, cipherName: this.cipherName,
data: audioBuffer, data: audioBuffer,
}; });
} finally { } finally {
ncm.free(); ncm.free();
} }
} }
public static make() { public static make(this: void) {
return new NetEaseCloudMusicDecipher(); return new NetEaseCloudMusicDecipher();
} }
} }

View File

@@ -19,14 +19,14 @@ export class QQMusicV1Decipher implements DecipherInstance {
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {
decryptQMC1(block, offset); decryptQMC1(block, offset);
} }
return { return Promise.resolve({
status: Status.OK, status: Status.OK,
cipherName: this.cipherName, cipherName: this.cipherName,
data: audioBuffer, data: audioBuffer,
}; });
} }
public static create() { public static create(this: void) {
return new QQMusicV1Decipher(); return new QQMusicV1Decipher();
} }
} }
@@ -62,25 +62,28 @@ export class QQMusicV2Decipher implements DecipherInstance {
throw new Error('EKey required'); throw new Error('EKey required');
} }
const qmc2 = new QMC2(ekey);
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size); const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size);
const qmc2 = new QMC2(ekey);
try {
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {
qmc2.decrypt(block, offset); qmc2.decrypt(block, offset);
} }
} finally {
qmc2.free(); qmc2.free();
}
return { return Promise.resolve({
status: Status.OK, status: Status.OK,
cipherName: this.cipherName, cipherName: this.cipherName,
data: audioBuffer, data: audioBuffer,
}; });
} }
public static createWithUserKey() { public static createWithUserKey(this: void) {
return new QQMusicV2Decipher(true); return new QQMusicV2Decipher(true);
} }
public static createWithEmbeddedEKey() { public static createWithEmbeddedEKey(this: void) {
return new QQMusicV2Decipher(false); return new QQMusicV2Decipher(false);
} }
} }

View File

@@ -18,20 +18,23 @@ export class QignTingFMDecipher implements DecipherInstance {
}; };
} }
const qtfm = new QingTingFM(key, iv);
const audioBuffer = new Uint8Array(buffer); const audioBuffer = new Uint8Array(buffer);
const qtfm = new QingTingFM(key, iv);
try {
for (const [block, i] of chunkBuffer(audioBuffer)) { for (const [block, i] of chunkBuffer(audioBuffer)) {
qtfm.decrypt(block, i); qtfm.decrypt(block, i);
} }
} finally {
return { qtfm.free();
}
return Promise.resolve({
cipherName: this.cipherName, cipherName: this.cipherName,
status: Status.OK, status: Status.OK,
data: audioBuffer, data: audioBuffer,
}; });
} }
public static make() { public static make(this: void) {
return new QignTingFMDecipher(); return new QignTingFMDecipher();
} }
} }

View File

@@ -4,15 +4,15 @@ export class TransparentDecipher implements DecipherInstance {
cipherName = 'none'; cipherName = 'none';
async decrypt(buffer: Uint8Array<ArrayBuffer>): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array<ArrayBuffer>): Promise<DecipherResult | DecipherOK> {
return { return Promise.resolve({
cipherName: 'None', cipherName: 'None',
status: Status.OK, status: Status.OK,
data: buffer, data: buffer,
message: 'No decipher applied', message: 'No decipher applied',
}; });
} }
public static make() { public static make(this: void) {
return new TransparentDecipher(); return new TransparentDecipher();
} }
} }

View File

@@ -6,23 +6,26 @@ export class XiamiDecipher implements DecipherInstance {
cipherName = 'Xiami (XM)'; cipherName = 'Xiami (XM)';
async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array): Promise<DecipherResult | DecipherOK> {
const xm = Xiami.from_header(buffer.subarray(0, 0x10));
const { copyPlainLength } = xm;
const audioBuffer = buffer.slice(0x10); const audioBuffer = buffer.slice(0x10);
const xm = Xiami.from_header(buffer.subarray(0, 0x10));
try {
const { copyPlainLength } = xm;
for (const [block] of chunkBuffer(audioBuffer.subarray(copyPlainLength))) { for (const [block] of chunkBuffer(audioBuffer.subarray(copyPlainLength))) {
xm.decrypt(block); xm.decrypt(block);
} }
} finally {
xm.free(); xm.free();
}
return { return Promise.resolve({
cipherName: this.cipherName, cipherName: this.cipherName,
status: Status.OK, status: Status.OK,
data: audioBuffer, data: audioBuffer,
}; });
} }
public static make() { public static make(this: void) {
return new XiamiDecipher(); return new XiamiDecipher();
} }
} }

View File

@@ -23,18 +23,18 @@ export class XimalayaAndroidDecipher implements DecipherInstance {
} }
const result = new Uint8Array(buffer); const result = new Uint8Array(buffer);
result.set(slice, 0); result.set(slice, 0);
return { return Promise.resolve({
cipherName: this.cipherName, cipherName: this.cipherName,
status: Status.OK, status: Status.OK,
data: result, data: result,
}; });
} }
public static makeX2M() { public static makeX2M(this: void) {
return new XimalayaAndroidDecipher(decryptX2MHeader, 'X2M'); return new XimalayaAndroidDecipher(decryptX2MHeader, 'X2M');
} }
public static makeX3M() { public static makeX3M(this: void) {
return new XimalayaAndroidDecipher(decryptX3MHeader, 'X3M'); return new XimalayaAndroidDecipher(decryptX3MHeader, 'X3M');
} }
} }
@@ -45,27 +45,31 @@ export class XimalayaPCDecipher implements DecipherInstance {
async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> { async decrypt(buffer: Uint8Array, _options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
// Detect with first 0x400 bytes // Detect with first 0x400 bytes
const headerSize = XmlyPC.getHeaderSize(buffer.subarray(0, 1024)); const headerSize = XmlyPC.getHeaderSize(buffer.subarray(0, 1024));
const xm = new XmlyPC(buffer.subarray(0, headerSize)); const xmly = new XmlyPC(buffer.subarray(0, headerSize));
const { audioHeader, encryptedHeaderOffset, encryptedHeaderSize } = xm;
try {
const { audioHeader, encryptedHeaderOffset, encryptedHeaderSize } = xmly;
const plainAudioDataOffset = encryptedHeaderOffset + encryptedHeaderSize; const plainAudioDataOffset = encryptedHeaderOffset + encryptedHeaderSize;
const plainAudioDataLength = buffer.byteLength - plainAudioDataOffset; const plainAudioDataLength = buffer.byteLength - plainAudioDataOffset;
const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset); const encryptedAudioPart = buffer.slice(encryptedHeaderOffset, plainAudioDataOffset);
const encryptedAudioPartLen = xm.decrypt(encryptedAudioPart); const encryptedAudioPartLen = xmly.decrypt(encryptedAudioPart);
const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength; const audioSize = audioHeader.byteLength + encryptedAudioPartLen + plainAudioDataLength;
xm.free();
const result = new Uint8Array(audioSize); const result = new Uint8Array(audioSize);
result.set(audioHeader); result.set(audioHeader);
result.set(encryptedAudioPart, audioHeader.byteLength); result.set(encryptedAudioPart, audioHeader.byteLength);
result.set(buffer.subarray(plainAudioDataOffset), audioHeader.byteLength + encryptedAudioPartLen); result.set(buffer.subarray(plainAudioDataOffset), audioHeader.byteLength + encryptedAudioPartLen);
return { return Promise.resolve({
status: Status.OK, status: Status.OK,
data: result, data: result,
cipherName: this.cipherName, cipherName: this.cipherName,
}; });
} finally {
xmly.free();
}
} }
public static make() { public static make(this: void) {
return new XimalayaPCDecipher(); return new XimalayaPCDecipher();
} }
} }

View File

@@ -6,7 +6,7 @@ export function withWasmClass<T extends { free: () => void }, R>(instance: T, cb
const resp = cb(instance); const resp = cb(instance);
if (resp && isPromise(resp)) { if (resp && isPromise(resp)) {
isAsync = true; isAsync = true;
resp.finally(() => instance.free()); resp.finally(() => instance.free()).catch(() => {});
} }
return resp; return resp;
} finally { } finally {

View File

@@ -11,5 +11,5 @@ export async function workerGetQtfmDeviceKey({
board, board,
}: GetQingTingFMDeviceKeyPayload) { }: GetQingTingFMDeviceKeyPayload) {
const buffer = QingTingFM.getDeviceKey(device, brand, model, product, manufacturer, board); const buffer = QingTingFM.getDeviceKey(device, brand, model, product, manufacturer, board);
return hex(buffer); return Promise.resolve(hex(buffer));
} }

View File

@@ -1,5 +1,5 @@
// This is a dummy module for vite/rollup to resolve. // This is a dummy module for vite/rollup to resolve.
export function createRequire() { 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'); throw new Error('this is a dummy module. Do not use');
} }

View File

@@ -26,14 +26,10 @@ export function FileError({ error, code }: FileErrorProps) {
const copyError = () => { const copyError = () => {
if (error) { if (error) {
navigator.clipboard navigator.clipboard.writeText(applyTemplate(ERROR_TEMPLATE, { summary, error })).then(
.writeText(applyTemplate(ERROR_TEMPLATE, { summary, error })) () => toast.success('错误信息已复制到剪贴板'),
.then(() => { (e) => toast.error(`复制错误信息失败: ${e as Error}`),
toast.success('错误信息已复制到剪贴板'); );
})
.catch((e) => {
toast.error(`复制错误信息失败: ${e}`);
});
} }
}; };

View File

@@ -1,20 +1,11 @@
import { RiFileCopyLine } from 'react-icons/ri'; import { RiFileCopyLine } from 'react-icons/ri';
import { toast } from 'react-toastify';
import { ExtLink } from '~/components/ExtLink'; import { ExtLink } from '~/components/ExtLink';
import { FilePathBlock } from '~/components/FilePathBlock.tsx'; import { FilePathBlock } from '~/components/FilePathBlock.tsx';
import { copyToClipboard } from '~/util/clipboard';
export function InstructionsPC() {
const DB_PATH = '%APPDATA%\\KuGou8\\KGMusicV3.db'; const DB_PATH = '%APPDATA%\\KuGou8\\KGMusicV3.db';
const copyDbPathToClipboard = () => { export function InstructionsPC() {
navigator.clipboard const copyDbPathToClipboard = () => copyToClipboard(DB_PATH);
.writeText(DB_PATH)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
return ( return (
<> <>

View File

@@ -53,7 +53,7 @@ export function PanelQMCv2Key() {
toastImportResult(file.name, keys); toastImportResult(file.name, keys);
} catch (e) { } catch (e) {
console.error('error during import: ', e); console.error('error during import: ', e);
alert(`导入数据库时发生错误:${e}`); alert(`导入数据库时发生错误:${e as Error}`);
} }
}; };

View File

@@ -9,6 +9,7 @@ import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts';
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts'; import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts';
import { Ruby } from '~/components/Ruby'; import { Ruby } from '~/components/Ruby';
import { HiWord } from '~/components/HelpText/HiWord'; 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'; const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
@@ -28,23 +29,20 @@ export function PanelQingTing() {
return; 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)) { 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; const { product, device, manufacturer, brand, board, model } = dataMap;
if (product && device && manufacturer && brand && board && model) { if (product && device && manufacturer && brand && board && model) {
e.preventDefault(); e.preventDefault();
workerClientBus workerClientBus
.request<string, GetQingTingFMDeviceKeyPayload>( .request<
DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, string,
dataMap, GetQingTingFMDeviceKeyPayload
) >(DECRYPTION_WORKER_ACTION_NAME.QINGTING_FM_GET_DEVICE_KEY, dataMap)
.then(setSecretKey) .then(setSecretKey, (err) => toast.error(`生成设备密钥时发生错误: ${err}`));
.catch((err) => {
alert(`生成设备密钥时发生错误: ${err}`);
});
} }
}; };

View File

@@ -55,7 +55,7 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
let lastSettings: unknown; let lastSettings: unknown;
try { try {
const loadedSettings: ProductionSettings = JSON.parse(localStorage.getItem(storageKey) ?? ''); const loadedSettings = JSON.parse(localStorage.getItem(storageKey) ?? '') as ProductionSettings;
if (loadedSettings) { if (loadedSettings) {
const mergedSettings = mergeSettings(loadedSettings); const mergedSettings = mergeSettings(loadedSettings);
store.dispatch(setProductionChanges(mergedSettings)); store.dispatch(setProductionChanges(mergedSettings));

View File

@@ -3,8 +3,9 @@ import '@testing-library/jest-dom';
// FIXME: Use something like jsdom-worker? // FIXME: Use something like jsdom-worker?
// see: https://github.com/developit/jsdom-worker // see: https://github.com/developit/jsdom-worker
if (!global.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 { (global as any).Worker = class MockWorker {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
events: Record<string, (e: unknown) => void> = Object.create(null); events: Record<string, (e: unknown) => void> = Object.create(null);
onmessage = undefined; onmessage = undefined;

28
src/util/SimpleQueue.ts Normal file
View File

@@ -0,0 +1,28 @@
export class SimpleQueue {
private queue: (() => void)[] = [];
private running = 0;
constructor(private concurrency: number) {}
async enter() {
return new Promise<void>((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);
}
}
};
}

View File

@@ -50,7 +50,7 @@ export class WorkerClientBus<T = string> {
async request<R, P>(actionName: T, payload: P): Promise<R> { async request<R, P>(actionName: T, payload: P): Promise<R> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const id = `request://${actionName}/${nanoid()}`; const id = `request://${actionName as string}/${nanoid()}`;
this.idPromiseMap.set(id, [resolve, reject]); this.idPromiseMap.set(id, [resolve, reject]);
this.worker.postMessage({ this.worker.postMessage({
id, id,

View File

@@ -2,7 +2,7 @@ import { ConcurrentQueue } from '../ConcurrentQueue';
import { nextTickAsync } from '../nextTick'; import { nextTickAsync } from '../nextTick';
class SimpleQueue<T, R = void> extends ConcurrentQueue<T> { class SimpleQueue<T, R = void> extends ConcurrentQueue<T> {
handler(_item: T): Promise<R> { handler(this: void, _item: T): Promise<R> {
throw new Error('Method not overridden'); throw new Error('Method not overridden');
} }
} }

View File

@@ -17,7 +17,7 @@ test('should be able to forward request to worker client bus', async () => {
const bus = new WorkerClientBus<DECRYPTION_WORKER_ACTION_NAME>(null as never); const bus = new WorkerClientBus<DECRYPTION_WORKER_ACTION_NAME>(null as never);
vi.spyOn(bus, 'request').mockImplementation( vi.spyOn(bus, 'request').mockImplementation(
async (actionName: DECRYPTION_WORKER_ACTION_NAME, payload: unknown): Promise<unknown> => { async (actionName: DECRYPTION_WORKER_ACTION_NAME, payload: unknown): Promise<unknown> => {
return { actionName, payload }; return Promise.resolve({ actionName, payload });
}, },
); );

View File

@@ -1,3 +1,5 @@
export function applyTemplate(tpl: string, values: Record<string, unknown>) { export function applyTemplate(tpl: string, values: Record<string, unknown>) {
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]) : '',
);
} }

View File

@@ -1,12 +1,8 @@
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
export const copyToClipboard = (text: string) => { export const copyToClipboard = (text: string) => {
navigator.clipboard navigator.clipboard.writeText(text).then(
.writeText(text) () => toast.success('已复制到剪贴板'),
.then(() => { (err) => toast.error(`复制失败,请手动复制。\n错误: ${err as Error}`),
toast.success('已复制到剪贴板'); );
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
}; };

View File

@@ -1,3 +1,3 @@
export function deepClone<T>(obj: T): T { export function deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj)) as T;
} }