feat(shortcut): ✨ add shortcut keys, screenshots and voice, video calls
This commit is contained in:
49
.gitattributes
vendored
49
.gitattributes
vendored
@@ -25,32 +25,39 @@
|
||||
|
||||
# 代码统计设置 - 只统计 Vue、TypeScript 和 Rust 文件
|
||||
# 排除 JavaScript 文件
|
||||
"*.js" linguist-vendored
|
||||
"*.jsx" linguist-vendored
|
||||
"*.cjs" linguist-vendored
|
||||
"*.mjs" linguist-vendored
|
||||
*.js linguist-vendored
|
||||
*.jsx linguist-vendored
|
||||
*.cjs linguist-vendored
|
||||
*.mjs linguist-vendored
|
||||
|
||||
# 排除样式文件
|
||||
"*.css" linguist-vendored
|
||||
"*.scss" linguist-vendored
|
||||
"*.sass" linguist-vendored
|
||||
"*.less" linguist-vendored
|
||||
"*.styl" linguist-vendored
|
||||
*.css linguist-vendored
|
||||
*.scss linguist-vendored
|
||||
*.sass linguist-vendored
|
||||
*.less linguist-vendored
|
||||
*.styl linguist-vendored
|
||||
|
||||
# 排除配置和文档文件
|
||||
"*.json" linguist-vendored
|
||||
"*.html" linguist-vendored
|
||||
"*.md" linguist-documentation
|
||||
"*.yml" linguist-vendored
|
||||
"*.yaml" linguist-vendored
|
||||
*.json linguist-vendored
|
||||
*.html linguist-vendored
|
||||
*.md linguist-documentation
|
||||
*.yml linguist-vendored
|
||||
*.yaml linguist-vendored
|
||||
|
||||
# 排除其他语言文件
|
||||
"*.kt" linguist-vendored
|
||||
"*.java" linguist-vendored
|
||||
"*.py" linguist-vendored
|
||||
*.kt linguist-vendored
|
||||
*.java linguist-vendored
|
||||
*.py linguist-vendored
|
||||
|
||||
# 确保构建目录和依赖目录不被统计
|
||||
"**/target/**" linguist-vendored
|
||||
"/node_modules/**" linguist-vendored
|
||||
"/scripts/**" linguist-vendored
|
||||
"/build/**" linguist-vendored
|
||||
**/target/** linguist-vendored
|
||||
**/node_modules/** linguist-vendored
|
||||
scripts/** linguist-vendored
|
||||
**/build/** linguist-vendored
|
||||
**/dist/** linguist-vendored
|
||||
|
||||
# 确保主要代码文件被正确识别
|
||||
*.vue linguist-detectable
|
||||
*.ts linguist-detectable
|
||||
*.tsx linguist-detectable
|
||||
*.rs linguist-detectable
|
||||
|
||||
13
biome.json
vendored
13
biome.json
vendored
@@ -5,12 +5,12 @@
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**/*.{js,jsx,ts,tsx,vue,json}",
|
||||
"!.*/**",
|
||||
"!public/**",
|
||||
"!.*",
|
||||
"!public/*",
|
||||
"!src/typings/*.d.ts",
|
||||
"!src-tauri/**",
|
||||
"!node_modules/**",
|
||||
"!dist/**"
|
||||
"!src-tauri/*",
|
||||
"!node_modules/*",
|
||||
"!dist/*"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
@@ -58,7 +58,8 @@
|
||||
"noAssignInExpressions": "off",
|
||||
"noImplicitAnyLet": "off",
|
||||
"noConfusingVoidType": "off",
|
||||
"noArrayIndexKey": "off"
|
||||
"noArrayIndexKey": "off",
|
||||
"useIterableCallbackReturn": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
6
package.json
vendored
6
package.json
vendored
@@ -77,6 +77,7 @@
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.3.2",
|
||||
"@tauri-apps/plugin-fs": "^2.4.1",
|
||||
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
|
||||
"@tauri-apps/plugin-http": "2.5.1",
|
||||
"@tauri-apps/plugin-log": "^2.6.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.0",
|
||||
@@ -113,10 +114,9 @@
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.1.4",
|
||||
"@biomejs/biome": "^2.2.0",
|
||||
"@commitlint/cli": "^19.6.0",
|
||||
"@commitlint/config-conventional": "^19.6.0",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@release-it/bumper": "^6.0.1",
|
||||
"@release-it/conventional-changelog": "8.0.2",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
@@ -147,7 +147,7 @@
|
||||
"typescript": "^5.9.2",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "7.1.1",
|
||||
"vite": "7.1.2",
|
||||
"vite-plugin-vue-setup-extend": "^0.4.0",
|
||||
"vitest": "^3.2.4",
|
||||
"vue-tsc": "^3.0.5"
|
||||
|
||||
424
pnpm-lock.yaml
generated
vendored
424
pnpm-lock.yaml
generated
vendored
@@ -32,6 +32,9 @@ importers:
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: ^2.4.1
|
||||
version: 2.4.1
|
||||
'@tauri-apps/plugin-global-shortcut':
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
@@ -136,17 +139,14 @@ importers:
|
||||
version: 4.5.1(vue@3.5.18(typescript@5.9.2))
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: ^2.1.4
|
||||
version: 2.1.4
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0
|
||||
'@commitlint/cli':
|
||||
specifier: ^19.6.0
|
||||
version: 19.6.1(@types/node@24.0.10)(typescript@5.9.2)
|
||||
'@commitlint/config-conventional':
|
||||
specifier: ^19.6.0
|
||||
version: 19.6.0
|
||||
'@iconify/vue':
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(vue@3.5.18(typescript@5.9.2))
|
||||
'@release-it/bumper':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(release-it@17.11.0(typescript@5.9.2))
|
||||
@@ -182,13 +182,13 @@ importers:
|
||||
version: 66.4.2
|
||||
'@unocss/vite':
|
||||
specifier: ^66.4.2
|
||||
version: 66.4.2(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
version: 66.4.2(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))
|
||||
version: 6.0.1(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))
|
||||
'@vitejs/plugin-vue-jsx':
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))
|
||||
version: 5.0.1(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4)
|
||||
@@ -238,11 +238,11 @@ importers:
|
||||
specifier: ^28.8.0
|
||||
version: 28.8.0(@babel/parser@7.28.0)(@nuxt/kit@3.17.5(magicast@0.3.5))(vue@3.5.18(typescript@5.9.2))
|
||||
vite:
|
||||
specifier: 7.1.1
|
||||
version: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
specifier: 7.1.2
|
||||
version: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite-plugin-vue-setup-extend:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
version: 0.4.0(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/node@24.0.10)(@vitest/ui@3.2.4)(happy-dom@15.11.7)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
@@ -417,59 +417,59 @@ packages:
|
||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@biomejs/biome@2.1.4':
|
||||
resolution: {integrity: sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==}
|
||||
'@biomejs/biome@2.2.0':
|
||||
resolution: {integrity: sha512-3On3RSYLsX+n9KnoSgfoYlckYBoU6VRM22cw1gB4Y0OuUVSYd/O/2saOJMrA4HFfA1Ff0eacOvMN1yAAvHtzIw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.1.4':
|
||||
resolution: {integrity: sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==}
|
||||
'@biomejs/cli-darwin-arm64@2.2.0':
|
||||
resolution: {integrity: sha512-zKbwUUh+9uFmWfS8IFxmVD6XwqFcENjZvEyfOxHs1epjdH3wyyMQG80FGDsmauPwS2r5kXdEM0v/+dTIA9FXAg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.1.4':
|
||||
resolution: {integrity: sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==}
|
||||
'@biomejs/cli-darwin-x64@2.2.0':
|
||||
resolution: {integrity: sha512-+OmT4dsX2eTfhD5crUOPw3RPhaR+SKVspvGVmSdZ9y9O/AgL8pla6T4hOn1q+VAFBHuHhsdxDRJgFCSC7RaMOw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.4':
|
||||
resolution: {integrity: sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==}
|
||||
'@biomejs/cli-linux-arm64-musl@2.2.0':
|
||||
resolution: {integrity: sha512-egKpOa+4FL9YO+SMUMLUvf543cprjevNc3CAgDNFLcjknuNMcZ0GLJYa3EGTCR2xIkIUJDVneBV3O9OcIlCEZQ==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.1.4':
|
||||
resolution: {integrity: sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==}
|
||||
'@biomejs/cli-linux-arm64@2.2.0':
|
||||
resolution: {integrity: sha512-6eoRdF2yW5FnW9Lpeivh7Mayhq0KDdaDMYOJnH9aT02KuSIX5V1HmWJCQQPwIQbhDh68Zrcpl8inRlTEan0SXw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.1.4':
|
||||
resolution: {integrity: sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==}
|
||||
'@biomejs/cli-linux-x64-musl@2.2.0':
|
||||
resolution: {integrity: sha512-I5J85yWwUWpgJyC1CcytNSGusu2p9HjDnOPAFG4Y515hwRD0jpR9sT9/T1cKHtuCvEQ/sBvx+6zhz9l9wEJGAg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@biomejs/cli-linux-x64@2.1.4':
|
||||
resolution: {integrity: sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==}
|
||||
'@biomejs/cli-linux-x64@2.2.0':
|
||||
resolution: {integrity: sha512-5UmQx/OZAfJfi25zAnAGHUMuOd+LOsliIt119x2soA2gLggQYrVPA+2kMUxR6Mw5M1deUF/AWWP2qpxgH7Nyfw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.1.4':
|
||||
resolution: {integrity: sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==}
|
||||
'@biomejs/cli-win32-arm64@2.2.0':
|
||||
resolution: {integrity: sha512-n9a1/f2CwIDmNMNkFs+JI0ZjFnMO0jdOyGNtihgUNFnlmd84yIYY2KMTBmMV58ZlVHjgmY5Y6E1hVTnSRieggA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@2.1.4':
|
||||
resolution: {integrity: sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==}
|
||||
'@biomejs/cli-win32-x64@2.2.0':
|
||||
resolution: {integrity: sha512-Nawu5nHjP/zPKTIryh2AavzTc/KEg4um/MxWdXW0A6P/RZOyIpa7+QSjeXwAwX/utJGaCoXRPWtF3m5U/bB3Ww==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -580,8 +580,8 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.8':
|
||||
resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==}
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
@@ -592,8 +592,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==}
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
@@ -604,8 +604,8 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.8':
|
||||
resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==}
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
@@ -616,8 +616,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.8':
|
||||
resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==}
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
@@ -628,8 +628,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==}
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
@@ -640,8 +640,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.8':
|
||||
resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==}
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
@@ -652,8 +652,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==}
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
@@ -664,8 +664,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.8':
|
||||
resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==}
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
@@ -676,8 +676,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==}
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
@@ -688,8 +688,8 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.8':
|
||||
resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==}
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
@@ -700,8 +700,8 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.8':
|
||||
resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==}
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
@@ -712,8 +712,8 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.8':
|
||||
resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==}
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
@@ -724,8 +724,8 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.8':
|
||||
resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==}
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
@@ -736,8 +736,8 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.8':
|
||||
resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==}
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
@@ -748,8 +748,8 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.8':
|
||||
resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==}
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
@@ -760,8 +760,8 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.8':
|
||||
resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==}
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
@@ -772,14 +772,14 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.8':
|
||||
resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==}
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==}
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
@@ -790,8 +790,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.8':
|
||||
resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==}
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
@@ -802,8 +802,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==}
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
@@ -814,14 +814,14 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.8':
|
||||
resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==}
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==}
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
@@ -832,8 +832,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.8':
|
||||
resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==}
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
@@ -844,8 +844,8 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.8':
|
||||
resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==}
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
@@ -856,8 +856,8 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.8':
|
||||
resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==}
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
@@ -868,8 +868,8 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.8':
|
||||
resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==}
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -888,14 +888,6 @@ packages:
|
||||
'@iarna/toml@2.2.5':
|
||||
resolution: {integrity: sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
'@iconify/vue@5.0.0':
|
||||
resolution: {integrity: sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg==}
|
||||
peerDependencies:
|
||||
vue: '>=3'
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
@@ -1028,10 +1020,16 @@ packages:
|
||||
'@jridgewell/gen-mapping@0.3.12':
|
||||
resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -1049,12 +1047,18 @@ packages:
|
||||
'@jridgewell/sourcemap-codec@1.5.4':
|
||||
resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.29':
|
||||
resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.30':
|
||||
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
||||
|
||||
'@juggle/resize-observer@3.4.0':
|
||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||
|
||||
@@ -1499,6 +1503,9 @@ packages:
|
||||
'@tauri-apps/plugin-fs@2.4.1':
|
||||
resolution: {integrity: sha512-vJlKZVGF3UAFGoIEVT6Oq5L4HGDCD78WmA4uhzitToqYiBKWAvZR61M6zAyQzHqLs0ADemkE4RSy/5sCmZm6ZQ==}
|
||||
|
||||
'@tauri-apps/plugin-global-shortcut@2.3.0':
|
||||
resolution: {integrity: sha512-WbAz0ElhpP+0kzQZRScdCC7UQ7OPH8PAn//fsBNu7+ywihsnVSVOg1L9YhieAtLNtAlnmFI69Yl5AGaA3ge5IQ==}
|
||||
|
||||
'@tauri-apps/plugin-http@2.5.1':
|
||||
resolution: {integrity: sha512-SpQ1azXEdQI0UB2NZTIPljJTDEe0bIaKzHYR/k4UQp6yzRYGLC/ktmIgEfQ2RvKAWus8GcYgGr5K6LJPbo/NZw==}
|
||||
|
||||
@@ -2448,8 +2455,8 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.25.8:
|
||||
resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==}
|
||||
esbuild@0.25.9:
|
||||
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -2550,6 +2557,15 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: ^3 || ^4
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.2:
|
||||
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
|
||||
|
||||
@@ -4108,6 +4124,10 @@ packages:
|
||||
resolution: {integrity: sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
unplugin@2.3.6:
|
||||
resolution: {integrity: sha512-+/MdXl8bLTXI2lJF22gUBeCFqZruEpL/oM9f8wxCuKh9+Mw9qeul3gTqgbKpMeOFlusCzc0s7x2Kax2xKW+FQg==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
|
||||
untyped@2.0.0:
|
||||
resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==}
|
||||
hasBin: true
|
||||
@@ -4147,8 +4167,8 @@ packages:
|
||||
peerDependencies:
|
||||
vite: '>=2.0.0'
|
||||
|
||||
vite@7.1.1:
|
||||
resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==}
|
||||
vite@7.1.2:
|
||||
resolution: {integrity: sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -4587,39 +4607,39 @@ snapshots:
|
||||
|
||||
'@bcoe/v8-coverage@1.0.2': {}
|
||||
|
||||
'@biomejs/biome@2.1.4':
|
||||
'@biomejs/biome@2.2.0':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 2.1.4
|
||||
'@biomejs/cli-darwin-x64': 2.1.4
|
||||
'@biomejs/cli-linux-arm64': 2.1.4
|
||||
'@biomejs/cli-linux-arm64-musl': 2.1.4
|
||||
'@biomejs/cli-linux-x64': 2.1.4
|
||||
'@biomejs/cli-linux-x64-musl': 2.1.4
|
||||
'@biomejs/cli-win32-arm64': 2.1.4
|
||||
'@biomejs/cli-win32-x64': 2.1.4
|
||||
'@biomejs/cli-darwin-arm64': 2.2.0
|
||||
'@biomejs/cli-darwin-x64': 2.2.0
|
||||
'@biomejs/cli-linux-arm64': 2.2.0
|
||||
'@biomejs/cli-linux-arm64-musl': 2.2.0
|
||||
'@biomejs/cli-linux-x64': 2.2.0
|
||||
'@biomejs/cli-linux-x64-musl': 2.2.0
|
||||
'@biomejs/cli-win32-arm64': 2.2.0
|
||||
'@biomejs/cli-win32-x64': 2.2.0
|
||||
|
||||
'@biomejs/cli-darwin-arm64@2.1.4':
|
||||
'@biomejs/cli-darwin-arm64@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@2.1.4':
|
||||
'@biomejs/cli-darwin-x64@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@2.1.4':
|
||||
'@biomejs/cli-linux-arm64-musl@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@2.1.4':
|
||||
'@biomejs/cli-linux-arm64@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@2.1.4':
|
||||
'@biomejs/cli-linux-x64-musl@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@2.1.4':
|
||||
'@biomejs/cli-linux-x64@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@2.1.4':
|
||||
'@biomejs/cli-win32-arm64@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@2.1.4':
|
||||
'@biomejs/cli-win32-x64@2.2.0':
|
||||
optional: true
|
||||
|
||||
'@breezystack/lamejs@1.2.7': {}
|
||||
@@ -4760,151 +4780,151 @@ snapshots:
|
||||
'@esbuild/aix-ppc64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.8':
|
||||
'@esbuild/aix-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.8':
|
||||
'@esbuild/android-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.8':
|
||||
'@esbuild/android-arm@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.8':
|
||||
'@esbuild/android-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.8':
|
||||
'@esbuild/darwin-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.8':
|
||||
'@esbuild/darwin-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.8':
|
||||
'@esbuild/freebsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.8':
|
||||
'@esbuild/freebsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.8':
|
||||
'@esbuild/linux-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.8':
|
||||
'@esbuild/linux-arm@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.8':
|
||||
'@esbuild/linux-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.8':
|
||||
'@esbuild/linux-loong64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.8':
|
||||
'@esbuild/linux-mips64el@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.8':
|
||||
'@esbuild/linux-ppc64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.8':
|
||||
'@esbuild/linux-riscv64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.8':
|
||||
'@esbuild/linux-s390x@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.8':
|
||||
'@esbuild/linux-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.8':
|
||||
'@esbuild/netbsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.8':
|
||||
'@esbuild/netbsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.8':
|
||||
'@esbuild/openbsd-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.8':
|
||||
'@esbuild/openbsd-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.25.8':
|
||||
'@esbuild/openharmony-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.8':
|
||||
'@esbuild/sunos-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.8':
|
||||
'@esbuild/win32-arm64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.8':
|
||||
'@esbuild/win32-ia32@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.23.1':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.8':
|
||||
'@esbuild/win32-x64@0.25.9':
|
||||
optional: true
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
@@ -4917,13 +4937,6 @@ snapshots:
|
||||
|
||||
'@iarna/toml@2.2.5': {}
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/vue@5.0.0(vue@3.5.18(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
vue: 3.5.18(typescript@5.9.2)
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||
@@ -5017,12 +5030,24 @@ snapshots:
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
'@jridgewell/trace-mapping': 0.3.29
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.30
|
||||
optional: true
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.30
|
||||
optional: true
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/set-array@1.2.1': {}
|
||||
@@ -5036,6 +5061,9 @@ snapshots:
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.4': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.25':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
@@ -5046,6 +5074,12 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.4
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.30':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
optional: true
|
||||
|
||||
'@juggle/resize-observer@3.4.0': {}
|
||||
|
||||
'@lokesh.dhakar/quantize@1.4.0': {}
|
||||
@@ -5421,6 +5455,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.7.0
|
||||
|
||||
'@tauri-apps/plugin-global-shortcut@2.3.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.7.0
|
||||
|
||||
'@tauri-apps/plugin-http@2.5.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.7.0
|
||||
@@ -5557,7 +5595,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@unocss/core': 66.4.2
|
||||
|
||||
'@unocss/vite@66.4.2(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))':
|
||||
'@unocss/vite@66.4.2(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@unocss/config': 66.4.2
|
||||
@@ -5568,23 +5606,23 @@ snapshots:
|
||||
pathe: 2.0.3
|
||||
tinyglobby: 0.2.14
|
||||
unplugin-utils: 0.2.5
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.0.1(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))':
|
||||
'@vitejs/plugin-vue-jsx@5.0.1(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.0
|
||||
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
|
||||
'@rolldown/pluginutils': 1.0.0-beta.31
|
||||
'@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.28.0)
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vue: 3.5.18(typescript@5.9.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@6.0.1(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))':
|
||||
'@vitejs/plugin-vue@6.0.1(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))(vue@3.5.18(typescript@5.9.2))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-beta.29
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vue: 3.5.18(typescript@5.9.2)
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4)':
|
||||
@@ -5614,13 +5652,13 @@ snapshots:
|
||||
chai: 5.2.1
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))':
|
||||
'@vitest/mocker@3.2.4(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
optionalDependencies:
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
@@ -6507,34 +6545,34 @@ snapshots:
|
||||
'@esbuild/win32-x64': 0.23.1
|
||||
optional: true
|
||||
|
||||
esbuild@0.25.8:
|
||||
esbuild@0.25.9:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.8
|
||||
'@esbuild/android-arm': 0.25.8
|
||||
'@esbuild/android-arm64': 0.25.8
|
||||
'@esbuild/android-x64': 0.25.8
|
||||
'@esbuild/darwin-arm64': 0.25.8
|
||||
'@esbuild/darwin-x64': 0.25.8
|
||||
'@esbuild/freebsd-arm64': 0.25.8
|
||||
'@esbuild/freebsd-x64': 0.25.8
|
||||
'@esbuild/linux-arm': 0.25.8
|
||||
'@esbuild/linux-arm64': 0.25.8
|
||||
'@esbuild/linux-ia32': 0.25.8
|
||||
'@esbuild/linux-loong64': 0.25.8
|
||||
'@esbuild/linux-mips64el': 0.25.8
|
||||
'@esbuild/linux-ppc64': 0.25.8
|
||||
'@esbuild/linux-riscv64': 0.25.8
|
||||
'@esbuild/linux-s390x': 0.25.8
|
||||
'@esbuild/linux-x64': 0.25.8
|
||||
'@esbuild/netbsd-arm64': 0.25.8
|
||||
'@esbuild/netbsd-x64': 0.25.8
|
||||
'@esbuild/openbsd-arm64': 0.25.8
|
||||
'@esbuild/openbsd-x64': 0.25.8
|
||||
'@esbuild/openharmony-arm64': 0.25.8
|
||||
'@esbuild/sunos-x64': 0.25.8
|
||||
'@esbuild/win32-arm64': 0.25.8
|
||||
'@esbuild/win32-ia32': 0.25.8
|
||||
'@esbuild/win32-x64': 0.25.8
|
||||
'@esbuild/aix-ppc64': 0.25.9
|
||||
'@esbuild/android-arm': 0.25.9
|
||||
'@esbuild/android-arm64': 0.25.9
|
||||
'@esbuild/android-x64': 0.25.9
|
||||
'@esbuild/darwin-arm64': 0.25.9
|
||||
'@esbuild/darwin-x64': 0.25.9
|
||||
'@esbuild/freebsd-arm64': 0.25.9
|
||||
'@esbuild/freebsd-x64': 0.25.9
|
||||
'@esbuild/linux-arm': 0.25.9
|
||||
'@esbuild/linux-arm64': 0.25.9
|
||||
'@esbuild/linux-ia32': 0.25.9
|
||||
'@esbuild/linux-loong64': 0.25.9
|
||||
'@esbuild/linux-mips64el': 0.25.9
|
||||
'@esbuild/linux-ppc64': 0.25.9
|
||||
'@esbuild/linux-riscv64': 0.25.9
|
||||
'@esbuild/linux-s390x': 0.25.9
|
||||
'@esbuild/linux-x64': 0.25.9
|
||||
'@esbuild/netbsd-arm64': 0.25.9
|
||||
'@esbuild/netbsd-x64': 0.25.9
|
||||
'@esbuild/openbsd-arm64': 0.25.9
|
||||
'@esbuild/openbsd-x64': 0.25.9
|
||||
'@esbuild/openharmony-arm64': 0.25.9
|
||||
'@esbuild/sunos-x64': 0.25.9
|
||||
'@esbuild/win32-arm64': 0.25.9
|
||||
'@esbuild/win32-ia32': 0.25.9
|
||||
'@esbuild/win32-x64': 0.25.9
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
@@ -6640,7 +6678,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fdir@6.4.6(picomatch@4.0.3):
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
@@ -8164,7 +8202,7 @@ snapshots:
|
||||
acorn: 8.15.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.17
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.6
|
||||
optional: true
|
||||
|
||||
undici-types@7.8.0: {}
|
||||
@@ -8206,7 +8244,7 @@ snapshots:
|
||||
scule: 1.3.0
|
||||
strip-literal: 3.0.0
|
||||
tinyglobby: 0.2.14
|
||||
unplugin: 2.3.5
|
||||
unplugin: 2.3.6
|
||||
unplugin-utils: 0.2.5
|
||||
optional: true
|
||||
|
||||
@@ -8263,6 +8301,14 @@ snapshots:
|
||||
picomatch: 4.0.2
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
unplugin@2.3.6:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
acorn: 8.15.0
|
||||
picomatch: 4.0.3
|
||||
webpack-virtual-modules: 0.6.2
|
||||
optional: true
|
||||
|
||||
untyped@2.0.0:
|
||||
dependencies:
|
||||
citty: 0.1.6
|
||||
@@ -8311,7 +8357,7 @@ snapshots:
|
||||
debug: 4.4.1
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -8326,16 +8372,16 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-vue-setup-extend@0.4.0(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)):
|
||||
vite-plugin-vue-setup-extend@0.4.0(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)):
|
||||
dependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
magic-string: 0.25.9
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
|
||||
vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1):
|
||||
vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1):
|
||||
dependencies:
|
||||
esbuild: 0.25.8
|
||||
fdir: 6.4.6(picomatch@4.0.3)
|
||||
esbuild: 0.25.9
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
picomatch: 4.0.3
|
||||
postcss: 8.5.6
|
||||
rollup: 4.46.2
|
||||
@@ -8353,7 +8399,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/chai': 5.2.2
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
'@vitest/mocker': 3.2.4(vite@7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -8371,7 +8417,7 @@ snapshots:
|
||||
tinyglobby: 0.2.14
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.1.1(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite: 7.1.2(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
vite-node: 3.2.4(@types/node@24.0.10)(jiti@2.5.1)(sass@1.90.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.6.1)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
|
||||
2
public/icon.js
vendored
2
public/icon.js
vendored
File diff suppressed because one or more lines are too long
4
scripts/check-dependencies.js
vendored
4
scripts/check-dependencies.js
vendored
@@ -103,8 +103,8 @@ const compareVersions = (version1, version2) => {
|
||||
const v2 = version2.replace(/[^0-9.]/g, '').split('.')
|
||||
|
||||
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
|
||||
const num1 = parseInt(v1[i] || '0')
|
||||
const num2 = parseInt(v2[i] || '0')
|
||||
const num1 = parseInt(v1[i] || '0', 10)
|
||||
const num2 = parseInt(v2[i] || '0', 10)
|
||||
if (num1 > num2) return 1
|
||||
if (num1 < num2) return -1
|
||||
}
|
||||
|
||||
3
src-tauri/.env
Normal file
3
src-tauri/.env
Normal file
@@ -0,0 +1,3 @@
|
||||
RUST_BACKTRACE=1
|
||||
# APP_ENVIRONMENT=local
|
||||
APP_ENVIRONMENT=local
|
||||
@@ -6,5 +6,9 @@
|
||||
<string>Request camera access for WebRTC</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Request microphone access for WebRTC</string>
|
||||
<key>NSScreenCaptureUsageDescription</key>
|
||||
<string>HuLa needs screen recording permission to enable screenshot functionality</string>
|
||||
<key>NSAccessibilityUsageDescription</key>
|
||||
<string>HuLa needs accessibility permission to capture screen content</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
14
src-tauri/capabilities/desktop.json
vendored
14
src-tauri/capabilities/desktop.json
vendored
@@ -1,6 +1,16 @@
|
||||
{
|
||||
"identifier": "desktop-capability",
|
||||
"platforms": ["macOS", "windows", "linux"],
|
||||
"windows": ["home", "login", "update", "checkupdate"],
|
||||
"permissions": ["updater:default", "updater:allow-check", "updater:allow-download", "updater:allow-install"]
|
||||
"windows": ["home", "login", "update", "checkupdate", "capture", "settings", "tray"],
|
||||
"permissions": [
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-install",
|
||||
"global-shortcut:allow-is-registered",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-register-all",
|
||||
"global-shortcut:allow-unregister",
|
||||
"global-shortcut:allow-unregister-all"
|
||||
]
|
||||
}
|
||||
|
||||
2
src-tauri/gen/schemas/capabilities.json
vendored
2
src-tauri/gen/schemas/capabilities.json
vendored
File diff suppressed because one or more lines are too long
@@ -157,6 +157,7 @@ pub fn get_configuration(app_handle: &AppHandle) -> Result<Settings, config::Con
|
||||
config::ConfigError::Message(format!("Failed to parse APP_ENVIRONMENT: {:?}", e))
|
||||
})?;
|
||||
|
||||
info!("APP_ENVIRONMENT: {}", environment.as_str());
|
||||
let environment_filename = format!("{}.yaml", environment.as_str());
|
||||
let is_desktop_dev = cfg!(debug_assertions) && cfg!(desktop);
|
||||
|
||||
|
||||
@@ -183,13 +183,18 @@ pub fn set_height(height: u32, handle: AppHandle) -> Result<(), String> {
|
||||
///
|
||||
/// # 参数
|
||||
/// * `window_label` - 窗口的标签名称
|
||||
/// * `hide_close_button` - 可选参数,是否隐藏关闭按钮,默认为false(不隐藏)
|
||||
/// * `handle` - Tauri应用句柄
|
||||
///
|
||||
/// # 返回
|
||||
/// * `Result<(), String>` - 成功返回Ok(()), 失败返回错误信息
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn hide_title_bar_buttons(window_label: &str, handle: AppHandle) -> Result<(), String> {
|
||||
pub fn hide_title_bar_buttons(
|
||||
window_label: &str,
|
||||
hide_close_button: Option<bool>,
|
||||
handle: AppHandle,
|
||||
) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
@@ -219,6 +224,11 @@ pub fn hide_title_bar_buttons(window_label: &str, handle: AppHandle) -> Result<(
|
||||
hide_button(ns_window, NSWindowButton::NSWindowMiniaturizeButton);
|
||||
hide_button(ns_window, NSWindowButton::NSWindowZoomButton);
|
||||
|
||||
// 根据参数决定是否隐藏关闭按钮
|
||||
if hide_close_button.unwrap_or(false) {
|
||||
hide_button(ns_window, NSWindowButton::NSWindowCloseButton);
|
||||
}
|
||||
|
||||
// 设置窗口不可拖动
|
||||
let _: () = msg_send![ns_window, setMovable: NO];
|
||||
}
|
||||
@@ -226,6 +236,54 @@ pub fn hide_title_bar_buttons(window_label: &str, handle: AppHandle) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 恢复Mac窗口的标题栏按钮显示
|
||||
///
|
||||
/// # 参数
|
||||
/// * `window_label` - 窗口的标签名称
|
||||
/// * `handle` - Tauri应用句柄
|
||||
///
|
||||
/// # 返回
|
||||
/// * `Result<(), String>` - 成功返回Ok(()), 失败返回错误信息
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn show_title_bar_buttons(window_label: &str, handle: AppHandle) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
use cocoa::appkit::NSWindowButton;
|
||||
use cocoa::base::{NO, YES, id};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
let webview_window = handle
|
||||
.get_webview_window(window_label)
|
||||
.ok_or_else(|| format!("Window '{}' not found", window_label))?;
|
||||
|
||||
let ns_window = webview_window
|
||||
.ns_window()
|
||||
.map_err(|e| format!("Failed to get NSWindow: {}", e))? as id;
|
||||
|
||||
unsafe {
|
||||
// 显示标题栏按钮的辅助函数
|
||||
let show_button = |window: id, button_type: NSWindowButton| {
|
||||
let btn = window.standardWindowButton_(button_type);
|
||||
if !btn.is_null() {
|
||||
let _: () = msg_send![btn, setHidden: NO];
|
||||
}
|
||||
};
|
||||
|
||||
// 显示所有标题栏按钮
|
||||
show_button(ns_window, NSWindowButton::NSWindowCloseButton);
|
||||
show_button(ns_window, NSWindowButton::NSWindowMiniaturizeButton);
|
||||
show_button(ns_window, NSWindowButton::NSWindowZoomButton);
|
||||
show_button(ns_window, NSWindowButton::NSWindowFullScreenButton);
|
||||
|
||||
// 恢复窗口可拖动
|
||||
let _: () = msg_send![ns_window, setMovable: YES];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn push_window_payload(
|
||||
label: String,
|
||||
@@ -323,3 +381,30 @@ pub fn set_badge_count(count: Option<i64>, handle: AppHandle) -> Result<(), Stri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 macOS 窗口级别为屏幕保护程序级别,以覆盖菜单栏
|
||||
#[tauri::command]
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_window_level_above_menubar(window_label: &str, handle: AppHandle) -> Result<(), String> {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(deprecated)]
|
||||
{
|
||||
use cocoa::base::id;
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
let webview_window = handle
|
||||
.get_webview_window(window_label)
|
||||
.ok_or_else(|| format!("Window '{}' not found", window_label))?;
|
||||
|
||||
let ns_window = webview_window
|
||||
.ns_window()
|
||||
.map_err(|e| format!("Failed to get NSWindow: {}", e))? as id;
|
||||
|
||||
unsafe {
|
||||
// 设置窗口级别为屏幕保护程序级别 (1000),高于菜单栏
|
||||
let screen_saver_level: i32 = 1000;
|
||||
let _: () = msg_send![ns_window, setLevel: screen_saver_level];
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
mod desktops;
|
||||
#[cfg(desktop)]
|
||||
use common::init::CustomInit;
|
||||
#[cfg(target_os = "macos")]
|
||||
use common_cmd::hide_title_bar_buttons;
|
||||
#[cfg(desktop)]
|
||||
use common_cmd::{
|
||||
audio, default_window_icon, get_files_meta, get_window_payload, push_window_payload,
|
||||
screenshot, set_height,
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use common_cmd::{hide_title_bar_buttons, set_window_level_above_menubar, show_title_bar_buttons};
|
||||
#[cfg(target_os = "macos")]
|
||||
use desktops::app_event;
|
||||
#[cfg(desktop)]
|
||||
use desktops::{common_cmd, directory_scanner, init, tray, video_thumbnail::get_video_thumbnail};
|
||||
@@ -239,20 +239,35 @@ fn setup_logout_listener(app_handle: tauri::AppHandle) {
|
||||
app_handle.listen("logout", move |_event| {
|
||||
let app_handle = app_handle_clone.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tracing::info!("[LOGOUT] Starting to close all non-login windows");
|
||||
tracing::info!("[LOGOUT] Starting to close windows and preserve capture/checkupdate windows");
|
||||
|
||||
let all_windows = app_handle.webview_windows();
|
||||
tracing::info!("[LOGOUT] Found {} windows", all_windows.len());
|
||||
|
||||
// 收集需要关闭的窗口
|
||||
// 收集需要关闭的窗口和需要隐藏的窗口
|
||||
let mut windows_to_close = Vec::new();
|
||||
let mut windows_to_hide = Vec::new();
|
||||
for (label, window) in all_windows {
|
||||
// 跳过 login 窗口,不关闭它
|
||||
if label != "login" && label != "tray" {
|
||||
windows_to_close.push((label, window));
|
||||
match label.as_str() {
|
||||
// 这些窗口完全不处理
|
||||
"login" | "tray" => {},
|
||||
// 这些窗口只隐藏,不销毁
|
||||
"capture" | "checkupdate" => {
|
||||
windows_to_hide.push((label, window));
|
||||
},
|
||||
// 其他窗口需要关闭
|
||||
_ => {
|
||||
windows_to_close.push((label, window));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 先隐藏需要保持的窗口
|
||||
for (label, window) in windows_to_hide {
|
||||
tracing::info!("[LOGOUT] Hiding window (preserving): {}", label);
|
||||
let _ = window.hide();
|
||||
}
|
||||
|
||||
// 逐个关闭窗口,添加小延迟以避免并发关闭导致的错误
|
||||
for (label, window) in windows_to_close {
|
||||
tracing::info!("[LOGOUT] Closing window: {}", label);
|
||||
@@ -282,7 +297,7 @@ fn setup_logout_listener(app_handle: tauri::AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("[LOGOUT] All non-login windows closed successfully");
|
||||
tracing::info!("[LOGOUT] Logout completed - windows closed and capture/checkupdate windows preserved");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -369,6 +384,10 @@ fn get_invoke_handlers() -> impl Fn(tauri::ipc::Invoke<tauri::Wry>) -> bool + Se
|
||||
get_video_thumbnail,
|
||||
#[cfg(target_os = "macos")]
|
||||
hide_title_bar_buttons,
|
||||
#[cfg(target_os = "macos")]
|
||||
show_title_bar_buttons,
|
||||
#[cfg(target_os = "macos")]
|
||||
set_window_level_above_menubar,
|
||||
#[cfg(desktop)]
|
||||
push_window_payload,
|
||||
#[cfg(desktop)]
|
||||
|
||||
20
src-tauri/tauri.macos.conf.json
vendored
20
src-tauri/tauri.macos.conf.json
vendored
@@ -10,16 +10,9 @@
|
||||
"devUrl": "http://127.0.0.1:6130"
|
||||
},
|
||||
"bundle": {
|
||||
"resources": [
|
||||
"tray",
|
||||
"configuration",
|
||||
"Info.plist"
|
||||
],
|
||||
"resources": ["tray", "configuration", "Info.plist"],
|
||||
"active": true,
|
||||
"targets": [
|
||||
"app",
|
||||
"dmg"
|
||||
],
|
||||
"targets": ["app", "dmg"],
|
||||
"icon": [
|
||||
"icons/macos/light/32x32.png",
|
||||
"icons/macos/light/128x128.png",
|
||||
@@ -82,9 +75,14 @@
|
||||
"fullscreen": false,
|
||||
"transparent": true,
|
||||
"resizable": false,
|
||||
"skipTaskbar": false,
|
||||
"skipTaskbar": true,
|
||||
"decorations": false,
|
||||
"visible": false
|
||||
"visible": false,
|
||||
"hiddenTitle": true,
|
||||
"alwaysOnTop": true,
|
||||
"focus": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"visibleOnAllWorkspaces": true
|
||||
},
|
||||
{
|
||||
"label": "checkupdate",
|
||||
|
||||
7
src-tauri/tauri.windows.conf.json
vendored
7
src-tauri/tauri.windows.conf.json
vendored
@@ -96,12 +96,13 @@
|
||||
{
|
||||
"label": "capture",
|
||||
"url": "/capture",
|
||||
"fullscreen": false,
|
||||
"fullscreen": true,
|
||||
"transparent": true,
|
||||
"resizable": false,
|
||||
"skipTaskbar": false,
|
||||
"skipTaskbar": true,
|
||||
"decorations": false,
|
||||
"visible": false
|
||||
"visible": false,
|
||||
"hiddenTitle": true
|
||||
},
|
||||
{
|
||||
"label": "checkupdate",
|
||||
|
||||
15
src/App.vue
15
src/App.vue
@@ -13,6 +13,7 @@ import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { MittEnum, StoresEnum, ThemeEnum } from '@/enums'
|
||||
import { useGlobalShortcut } from '@/hooks/useGlobalShortcut.ts'
|
||||
import { useLogin } from '@/hooks/useLogin.ts'
|
||||
import { useMitt } from '@/hooks/useMitt.ts'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
@@ -29,6 +30,8 @@ const { resetLoginState, logout } = useLogin()
|
||||
const token = useStorage<string | null>('TOKEN', null)
|
||||
const refreshToken = useStorage<string | null>('REFRESH_TOKEN', null)
|
||||
const { addListener } = useTauriListener()
|
||||
// 全局快捷键管理
|
||||
const { initializeGlobalShortcut, cleanupGlobalShortcut } = useGlobalShortcut()
|
||||
|
||||
/** 不需要锁屏的页面 */
|
||||
const LockExclusion = new Set(['/login', '/tray', '/qrCode', '/about', '/onlineStatus'])
|
||||
@@ -123,6 +126,11 @@ onMounted(async () => {
|
||||
}
|
||||
document.documentElement.dataset.theme = themes.value.content
|
||||
window.addEventListener('dragstart', preventDrag)
|
||||
|
||||
// 只在主窗口中初始化全局快捷键
|
||||
if (appWindow.label === 'home') {
|
||||
await initializeGlobalShortcut()
|
||||
}
|
||||
/** 开发环境不禁止 */
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
/** 禁用浏览器默认的快捷键 */
|
||||
@@ -171,9 +179,14 @@ onMounted(async () => {
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
onUnmounted(async () => {
|
||||
window.removeEventListener('contextmenu', (e) => e.preventDefault(), false)
|
||||
window.removeEventListener('dragstart', preventDrag)
|
||||
|
||||
// 只在主窗口中清理全局快捷键
|
||||
if (appWindow.label === 'home') {
|
||||
await cleanupGlobalShortcut()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -128,9 +128,9 @@ const isAccepted = (targetId: string) => {
|
||||
const applyList = computed(() => {
|
||||
return contactStore.requestFriendsList.filter((item) => {
|
||||
if (props.type === 'friend') {
|
||||
return item.type === 1
|
||||
} else {
|
||||
return item.type === 2
|
||||
} else {
|
||||
return item.type === 1
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -194,11 +194,16 @@ const loadMoreFriendRequests = async () => {
|
||||
|
||||
const handleAgree = async (applyId: string) => {
|
||||
loadingMap.value[applyId] = true
|
||||
contactStore.onAcceptFriend(applyId).then(() => {
|
||||
setTimeout(() => {
|
||||
loadingMap.value[applyId] = false
|
||||
}, 600)
|
||||
})
|
||||
contactStore
|
||||
.onHandleInvite({
|
||||
applyId,
|
||||
state: 2
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => {
|
||||
loadingMap.value[applyId] = false
|
||||
}, 600)
|
||||
})
|
||||
}
|
||||
|
||||
// 处理好友请求操作(拒绝或忽略)
|
||||
@@ -206,9 +211,15 @@ const handleFriendAction = async (action: string, applyId: string) => {
|
||||
loadingMap.value[applyId] = true
|
||||
try {
|
||||
if (action === 'reject') {
|
||||
await contactStore.onRejectFriend(applyId)
|
||||
await contactStore.onHandleInvite({
|
||||
applyId,
|
||||
state: 0
|
||||
})
|
||||
} else if (action === 'ignore') {
|
||||
await contactStore.onIgnoreFriend(applyId)
|
||||
await contactStore.onHandleInvite({
|
||||
applyId,
|
||||
state: 3
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { emitTo } from '@tauri-apps/api/event'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { LogicalPosition, LogicalSize } from '@tauri-apps/api/dpi'
|
||||
import { join } from '@tauri-apps/api/path'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { open } from '@tauri-apps/plugin-dialog'
|
||||
@@ -530,8 +531,28 @@ const updateRecentEmojis = (emoji: string) => {
|
||||
|
||||
const handleCap = async () => {
|
||||
const captureWindow = await WebviewWindow.getByLabel('capture')
|
||||
captureWindow?.show()
|
||||
await emitTo('capture', 'capture', true)
|
||||
if (!captureWindow) return
|
||||
|
||||
// 设置窗口覆盖整个屏幕(包括菜单栏)
|
||||
// 使用 JavaScript screen API 获取屏幕尺寸
|
||||
const screenWidth = window.screen.width * window.devicePixelRatio
|
||||
const screenHeight = window.screen.height * window.devicePixelRatio
|
||||
|
||||
// 设置窗口覆盖整个屏幕(包括菜单栏),不使用负坐标
|
||||
// 依靠窗口级别设置来确保覆盖菜单栏
|
||||
await captureWindow.setSize(new LogicalSize(screenWidth, screenHeight))
|
||||
await captureWindow.setPosition(new LogicalPosition(0, 0))
|
||||
|
||||
// 在 macOS 上设置窗口级别以覆盖菜单栏
|
||||
try {
|
||||
await invoke('set_window_level_above_menubar', { windowLabel: 'capture' })
|
||||
} catch (error) {
|
||||
console.warn('设置窗口级别失败,但继续执行:', error)
|
||||
}
|
||||
|
||||
await captureWindow.show()
|
||||
await captureWindow.setFocus()
|
||||
await captureWindow.emit('capture', true)
|
||||
}
|
||||
|
||||
const handleVoiceRecord = () => {
|
||||
|
||||
@@ -956,16 +956,20 @@ const startRtcCall = async (callType: CallTypeEnum) => {
|
||||
}
|
||||
|
||||
const createRtcCallWindow = async (isIncoming: boolean, remoteUserId: string, callType: CallTypeEnum) => {
|
||||
// 获取对方用户ID(单聊时使用,群聊时可能需要其他逻辑)
|
||||
// 根据是否来电决定窗口尺寸
|
||||
const windowConfig = isIncoming
|
||||
? { width: 360, height: 90, minWidth: 360, minHeight: 90 } // 来电通知尺寸
|
||||
: { width: 500, height: 650, minWidth: 500, minHeight: 650 } // 正常通话尺寸
|
||||
|
||||
await createWebviewWindow(
|
||||
'视频通话', // 窗口标题
|
||||
'rtcCall', // 窗口标签
|
||||
500, // 宽度
|
||||
650, // 高度
|
||||
windowConfig.width, // 宽度
|
||||
windowConfig.height, // 高度
|
||||
undefined, // 不需要关闭其他窗口
|
||||
false, // 不可调整大小
|
||||
500, // 最小宽度
|
||||
650, // 最小高度
|
||||
windowConfig.minWidth, // 最小宽度
|
||||
windowConfig.minHeight, // 最小高度
|
||||
false, // 不透明
|
||||
false, // 显示窗口
|
||||
{
|
||||
|
||||
@@ -104,8 +104,9 @@
|
||||
:from-user-uid="item.fromUser.uid"
|
||||
:is-group="isGroup" />
|
||||
|
||||
<!-- 消息为系统消息 -->
|
||||
<SystemMessage
|
||||
v-if="item.message.type === MsgEnum.SYSTEM"
|
||||
v-else-if="item.message.type === MsgEnum.SYSTEM"
|
||||
:body="item.message.body"
|
||||
:from-user-uid="item.fromUser.uid" />
|
||||
|
||||
@@ -896,7 +897,7 @@ const handleMacSelect = (event: any) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理聊天区域点击事件,用于清除回复样式
|
||||
// 处理聊天区域点击事件,用于清除回复样式和气泡激活状态
|
||||
const handleChatAreaClick = (event: any) => {
|
||||
// 检查点击目标是否为回复相关元素
|
||||
const isReplyElement =
|
||||
@@ -904,6 +905,10 @@ const handleChatAreaClick = (event: any) => {
|
||||
event.target.matches('.active-reply') ||
|
||||
event.target.closest('.active-reply')
|
||||
|
||||
// 检查点击目标是否为气泡相关元素
|
||||
const isBubbleElement =
|
||||
event.target.closest('.bubble') || event.target.closest('.bubble-oneself') || event.target.closest('[data-key]')
|
||||
|
||||
// 如果点击的不是回复相关元素,清除activeReply样式
|
||||
if (!isReplyElement && activeReply.value) {
|
||||
nextTick(() => {
|
||||
@@ -917,6 +922,11 @@ const handleChatAreaClick = (event: any) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 如果点击的不是气泡相关元素,清除activeBubble状态
|
||||
if (!isBubbleElement && activeBubble.value) {
|
||||
activeBubble.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = (item: any) => {
|
||||
|
||||
@@ -379,7 +379,7 @@ const handleLoadGroupAnnoun = async (roomId: string) => {
|
||||
announList.value = [topAnnouncement, ...announList.value.filter((item: any) => !item.top)]
|
||||
}
|
||||
}
|
||||
announNum.value = parseInt(data.total)
|
||||
announNum.value = parseInt(data.total, 10)
|
||||
}
|
||||
|
||||
// 加载完成后,关闭骨架屏
|
||||
|
||||
@@ -152,40 +152,6 @@ watchEffect(() => {
|
||||
if (alwaysOnTopStatus.value) {
|
||||
appWindow.setAlwaysOnTop(alwaysOnTopStatus.value as boolean)
|
||||
}
|
||||
|
||||
// 添加关闭事件拦截逻辑 - 只拦截 home 窗口
|
||||
// if (appWindow.label === 'home' && !unlistenCloseRequested) {
|
||||
// // 监听原生关闭事件
|
||||
// appWindow
|
||||
// .onCloseRequested((event) => {
|
||||
// // 如果是程序内部触发的关闭操作,不拦截
|
||||
// if (isProgrammaticClose) {
|
||||
// return
|
||||
// }
|
||||
// // 阻止默认关闭行为
|
||||
// event.preventDefault()
|
||||
// if (!tips.value.notTips) {
|
||||
// tipsRef.show = true
|
||||
// } else {
|
||||
// if (tips.value.type === CloseBxEnum.CLOSE) {
|
||||
// // 用户选择直接退出
|
||||
// console.log('用户设置为直接退出应用')
|
||||
// emit(EventEnum.EXIT)
|
||||
// } else {
|
||||
// // 用户选择最小化到托盘
|
||||
// console.log('用户设置为最小化到托盘')
|
||||
// appWindow.hide()
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .then((unlisten) => {
|
||||
// console.log('macOS home窗口关闭按钮事件监听器已设置')
|
||||
// unlistenCloseRequested = unlisten
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error('设置 macOS home窗口关闭按钮监听器失败:', error)
|
||||
// })
|
||||
// }
|
||||
if (escClose.value && type() === 'windows') {
|
||||
window.addEventListener('keydown', (e) => isEsc(e))
|
||||
} else {
|
||||
@@ -249,6 +215,7 @@ const isEsc = (e: KeyboardEvent) => {
|
||||
// 判断当前是否是最大化
|
||||
const handleResize = () => {
|
||||
appWindow.isMaximized().then((res) => {
|
||||
console.log('ActionBar 检测到窗口最大化状态:', res)
|
||||
windowMaximized.value = res
|
||||
})
|
||||
}
|
||||
@@ -321,6 +288,11 @@ onUnmounted(() => {
|
||||
unlistenCloseRequested = null
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露 windowMaximized 状态
|
||||
defineExpose({
|
||||
windowMaximized
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -28,8 +28,6 @@ export enum URLEnum {
|
||||
CHAT = '/im/chat',
|
||||
/**房间*/
|
||||
ROOM = '/im/room',
|
||||
/**oss*/
|
||||
OSS = '/oss',
|
||||
/**系统*/
|
||||
SYSTEM = '/system',
|
||||
/**验证码*/
|
||||
|
||||
@@ -251,6 +251,29 @@ export function useCanvasTool(drawCanvas: any, drawCtx: any, imgCtx: any, screen
|
||||
}
|
||||
}
|
||||
|
||||
// 重置绘图状态,清除所有绘制历史
|
||||
const resetState = () => {
|
||||
drawConfig.value.actions = []
|
||||
drawConfig.value.undoStack = []
|
||||
drawConfig.value.isDrawing = false
|
||||
currentTool.value = ''
|
||||
console.log('🔄 绘图状态已重置,历史记录已清除')
|
||||
}
|
||||
|
||||
// 停止当前绘图操作
|
||||
const stopDrawing = () => {
|
||||
drawConfig.value.isDrawing = false
|
||||
currentTool.value = ''
|
||||
closeListen()
|
||||
console.log('⏹️ 绘图操作已停止')
|
||||
}
|
||||
|
||||
// 清除事件监听
|
||||
const clearEvents = () => {
|
||||
closeListen()
|
||||
console.log('🧹 绘图事件监听已清除')
|
||||
}
|
||||
|
||||
const startListen = () => {
|
||||
document.addEventListener('mousedown', handleMouseDown)
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
@@ -270,6 +293,9 @@ export function useCanvasTool(drawCanvas: any, drawCtx: any, imgCtx: any, screen
|
||||
drawCircle,
|
||||
drawArrow,
|
||||
undo,
|
||||
redo
|
||||
redo,
|
||||
resetState,
|
||||
stopDrawing,
|
||||
clearEvents
|
||||
}
|
||||
}
|
||||
|
||||
331
src/hooks/useGlobalShortcut.ts
Normal file
331
src/hooks/useGlobalShortcut.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { LogicalPosition, LogicalSize } from '@tauri-apps/api/dpi'
|
||||
import { emitTo, listen } from '@tauri-apps/api/event'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { register, unregister } from '@tauri-apps/plugin-global-shortcut'
|
||||
import { useTauriListener } from '@/hooks/useTauriListener'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
|
||||
// 快捷键配置接口
|
||||
type ShortcutConfig = {
|
||||
/** 配置键名,用于从 store 中读取设置 */
|
||||
key: keyof NonNullable<ReturnType<typeof useSettingStore>['shortcuts']>
|
||||
/** 默认快捷键值 */
|
||||
defaultValue: string
|
||||
/** 快捷键处理函数 */
|
||||
handler: () => Promise<void>
|
||||
/** 监听的更新事件名 */
|
||||
updateEventName: string
|
||||
/** 发送注册状态的事件名 */
|
||||
registrationEventName: string
|
||||
}
|
||||
|
||||
// 全局快捷键状态管理
|
||||
const globalShortcutStates = new Map<string, string>()
|
||||
|
||||
// 防抖状态管理
|
||||
let togglePanelTimeout: NodeJS.Timeout | null = null
|
||||
let lastToggleTime = 0
|
||||
|
||||
/**
|
||||
* 全局快捷键管理 Hook
|
||||
* 负责注册、取消注册和管理全局快捷键
|
||||
* 使用配置驱动的方式,方便扩展新快捷键
|
||||
*/
|
||||
export const useGlobalShortcut = () => {
|
||||
const settingStore = useSettingStore()
|
||||
const { addListener } = useTauriListener()
|
||||
|
||||
/**
|
||||
* 确保capture窗口存在
|
||||
* 如果不存在则创建,如果存在则确保设置了关闭拦截
|
||||
*/
|
||||
const ensureCaptureWindow = async () => {
|
||||
const captureWindow = await WebviewWindow.getByLabel('capture')
|
||||
|
||||
if (captureWindow) {
|
||||
// 设置关闭拦截 - 将关闭转为隐藏
|
||||
addListener(
|
||||
captureWindow.onCloseRequested(async (event) => {
|
||||
event.preventDefault()
|
||||
await captureWindow.hide()
|
||||
// 触发重置事件,让Screenshot组件重新初始化
|
||||
await captureWindow.emit('capture-reset', {})
|
||||
}),
|
||||
'capture-close-intercept'
|
||||
)
|
||||
// 初始状态为隐藏
|
||||
await captureWindow.hide()
|
||||
}
|
||||
|
||||
return captureWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* 截图处理函数
|
||||
*/
|
||||
const handleScreenshot = async () => {
|
||||
try {
|
||||
const homeWindow = await WebviewWindow.getByLabel('home')
|
||||
if (!homeWindow) return
|
||||
|
||||
const captureWindow = await WebviewWindow.getByLabel('capture')
|
||||
if (!captureWindow) return
|
||||
|
||||
// 设置窗口覆盖整个屏幕(包括菜单栏)
|
||||
const screenWidth = window.screen.width * window.devicePixelRatio
|
||||
const screenHeight = window.screen.height * window.devicePixelRatio
|
||||
|
||||
// 依靠窗口级别设置来确保覆盖菜单栏
|
||||
await captureWindow.setSize(new LogicalSize(screenWidth, screenHeight))
|
||||
await captureWindow.setPosition(new LogicalPosition(0, 0))
|
||||
|
||||
// 在 macOS 上设置窗口级别以覆盖菜单栏
|
||||
await invoke('set_window_level_above_menubar', { windowLabel: 'capture' })
|
||||
|
||||
await captureWindow.show()
|
||||
await captureWindow.setFocus()
|
||||
await captureWindow.emit('capture', true)
|
||||
|
||||
console.log('📷 截图窗口已启动')
|
||||
} catch (error) {
|
||||
console.error('Failed to open screenshot window:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能切换主面板显示状态(防抖版本)
|
||||
* - 如果窗口已显示,则隐藏
|
||||
* - 如果窗口隐藏或最小化,则显示并聚焦
|
||||
*/
|
||||
const handleOpenMainPanel = async () => {
|
||||
const currentTime = Date.now()
|
||||
|
||||
// 防抖:如果距离上次操作少于500ms,则忽略
|
||||
if (currentTime - lastToggleTime < 500) {
|
||||
return
|
||||
}
|
||||
|
||||
// 清除之前的延时操作
|
||||
if (togglePanelTimeout) {
|
||||
clearTimeout(togglePanelTimeout)
|
||||
togglePanelTimeout = null
|
||||
}
|
||||
|
||||
lastToggleTime = currentTime
|
||||
|
||||
try {
|
||||
const homeWindow = await WebviewWindow.getByLabel('home')
|
||||
if (!homeWindow) {
|
||||
console.warn('Home window not found')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前窗口状态
|
||||
const isVisible = await homeWindow.isVisible()
|
||||
const isMinimized = await homeWindow.isMinimized()
|
||||
|
||||
console.log(`🏠 快捷键触发 - 窗口状态: 可见=${isVisible}, 最小化=${isMinimized}`)
|
||||
|
||||
if (isVisible && !isMinimized) {
|
||||
// 窗口当前可见且未最小化,直接隐藏
|
||||
await homeWindow.hide()
|
||||
} else {
|
||||
// 处理最小化状态
|
||||
if (isMinimized) {
|
||||
await homeWindow.unminimize()
|
||||
}
|
||||
|
||||
// 显示窗口
|
||||
await homeWindow.show()
|
||||
|
||||
// 延迟设置焦点,确保窗口已完全显示
|
||||
togglePanelTimeout = setTimeout(async () => {
|
||||
await homeWindow.setFocus()
|
||||
}, 50)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle main panel:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷键配置数组 - 新增快捷键只需在此处添加配置即可
|
||||
const shortcutConfigs: ShortcutConfig[] = [
|
||||
{
|
||||
key: 'screenshot',
|
||||
defaultValue: 'CmdOrCtrl+Alt+H',
|
||||
handler: handleScreenshot,
|
||||
updateEventName: 'shortcut-updated',
|
||||
registrationEventName: 'shortcut-registration-updated'
|
||||
},
|
||||
{
|
||||
key: 'openMainPanel',
|
||||
defaultValue: 'CmdOrCtrl+Alt+P',
|
||||
handler: handleOpenMainPanel,
|
||||
updateEventName: 'open-main-panel-shortcut-updated',
|
||||
registrationEventName: 'open-main-panel-shortcut-registration-updated'
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* 通用快捷键注册函数
|
||||
* @param config 快捷键配置
|
||||
* @param shortcut 快捷键字符串
|
||||
*/
|
||||
const registerShortcut = async (config: ShortcutConfig, shortcut: string): Promise<boolean> => {
|
||||
try {
|
||||
const currentShortcut = globalShortcutStates.get(config.key)
|
||||
|
||||
// 清理当前快捷键
|
||||
if (currentShortcut) {
|
||||
await unregister(currentShortcut)
|
||||
console.log(`🗑️ 清理快捷键 [${config.key}]: ${currentShortcut}`)
|
||||
}
|
||||
|
||||
// 预防性清理目标快捷键
|
||||
if (!currentShortcut) {
|
||||
try {
|
||||
await unregister(shortcut)
|
||||
console.log(`🗑️ 预清理快捷键 [${config.key}]: ${shortcut}`)
|
||||
} catch (_e) {
|
||||
console.log(`ℹ️ 快捷键 [${config.key}] 未注册: ${shortcut}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册新快捷键
|
||||
await register(shortcut, config.handler)
|
||||
globalShortcutStates.set(config.key, shortcut)
|
||||
console.log(`✅ 快捷键已注册 [${config.key}]: ${shortcut}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`❌ 注册快捷键失败 [${config.key}]:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册快捷键
|
||||
* @param shortcut 要取消注册的快捷键字符串
|
||||
*/
|
||||
const unregisterShortcut = async (shortcut: string) => {
|
||||
try {
|
||||
await unregister(shortcut)
|
||||
console.log(`✅ 成功取消注册快捷键: ${shortcut}`)
|
||||
} catch (error) {
|
||||
console.error(`❌ 取消注册快捷键失败: ${shortcut}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制清理快捷键残留
|
||||
*/
|
||||
const forceCleanupShortcuts = async (shortcuts: string[]) => {
|
||||
for (const shortcut of shortcuts) {
|
||||
try {
|
||||
await unregister(shortcut)
|
||||
} catch (_e) {
|
||||
console.log(`🧹 强制清理 ${shortcut} (可能未注册)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用快捷键更新处理函数
|
||||
* @param config 快捷键配置
|
||||
* @param newShortcut 新快捷键
|
||||
*/
|
||||
const handleShortcutUpdate = async (config: ShortcutConfig, newShortcut: string) => {
|
||||
const oldShortcut = globalShortcutStates.get(config.key)
|
||||
|
||||
// 强制清理旧快捷键
|
||||
const shortcutsToClean = [oldShortcut, newShortcut].filter(Boolean) as string[]
|
||||
await forceCleanupShortcuts(shortcutsToClean)
|
||||
|
||||
// 清除状态,准备重新注册
|
||||
globalShortcutStates.delete(config.key)
|
||||
|
||||
// 尝试注册新快捷键
|
||||
console.log(`🔧 [Home] 开始注册新快捷键 [${config.key}]: ${newShortcut}`)
|
||||
const success = await registerShortcut(config, newShortcut)
|
||||
|
||||
// 如果注册失败且有旧快捷键,尝试回滚
|
||||
if (!success && oldShortcut) {
|
||||
globalShortcutStates.delete(config.key)
|
||||
const rollbackSuccess = await registerShortcut(config, oldShortcut)
|
||||
console.log(`🔄 [Home] 快捷键回滚结果 [${config.key}]: ${rollbackSuccess ? '成功' : '失败'}`)
|
||||
}
|
||||
|
||||
// 通知设置页面注册状态更新
|
||||
await emitTo('settings', config.registrationEventName, {
|
||||
shortcut: newShortcut,
|
||||
registered: success
|
||||
})
|
||||
console.log(`📡 [Home] 已通知 settings 窗口快捷键状态更新 [${config.key}]: ${success ? '已注册' : '未注册'}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化全局快捷键
|
||||
* 根据配置自动注册所有快捷键并监听更新事件
|
||||
*/
|
||||
const initializeGlobalShortcut = async () => {
|
||||
// 确保capture窗口存在
|
||||
await ensureCaptureWindow()
|
||||
|
||||
// 批量注册所有配置的快捷键
|
||||
for (const config of shortcutConfigs) {
|
||||
const savedShortcut = settingStore.shortcuts?.[config.key] || config.defaultValue
|
||||
await registerShortcut(config, savedShortcut)
|
||||
|
||||
// 监听每个快捷键的更新事件
|
||||
addListener(
|
||||
listen(config.updateEventName, (event) => {
|
||||
const newShortcut = (event.payload as any)?.shortcut
|
||||
if (newShortcut) {
|
||||
console.log(`📡 [Home] 收到快捷键更新事件 [${config.key}]: ${newShortcut}`)
|
||||
handleShortcutUpdate(config, newShortcut)
|
||||
} else {
|
||||
console.warn(`📡 [Home] 收到无效的快捷键更新事件 [${config.key}]:`, event.payload)
|
||||
}
|
||||
}),
|
||||
config.updateEventName
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理全局快捷键
|
||||
* 取消注册所有快捷键并清理状态
|
||||
*/
|
||||
const cleanupGlobalShortcut = async () => {
|
||||
// 清理防抖定时器
|
||||
if (togglePanelTimeout) {
|
||||
clearTimeout(togglePanelTimeout)
|
||||
togglePanelTimeout = null
|
||||
}
|
||||
|
||||
// 取消注册所有已注册的快捷键
|
||||
for (const shortcut of globalShortcutStates.values()) {
|
||||
await unregisterShortcut(shortcut)
|
||||
}
|
||||
// 清理状态
|
||||
globalShortcutStates.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
// 处理函数
|
||||
handleScreenshot,
|
||||
handleOpenMainPanel,
|
||||
|
||||
// 核心功能
|
||||
initializeGlobalShortcut,
|
||||
cleanupGlobalShortcut,
|
||||
ensureCaptureWindow,
|
||||
|
||||
// 工具函数(主要用于测试和调试)
|
||||
registerShortcut: (config: ShortcutConfig, shortcut: string) => registerShortcut(config, shortcut),
|
||||
unregisterShortcut,
|
||||
|
||||
// 配置信息(用于外部访问)
|
||||
shortcutConfigs
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { info } from '@tauri-apps/plugin-log'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { EventEnum, RoomTypeEnum, TauriCommand } from '@/enums'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
@@ -37,9 +38,12 @@ export const useLogin = () => {
|
||||
* 登出账号
|
||||
*/
|
||||
const logout = async () => {
|
||||
info('登出账号')
|
||||
const { createWebviewWindow } = useWindow()
|
||||
isTrayMenuShow.value = false
|
||||
try {
|
||||
// ws 退出连接
|
||||
await invokeSilently('ws_disconnect')
|
||||
await invokeSilently(TauriCommand.UPDATE_USER_LAST_OPT_TIME)
|
||||
// 创建登录窗口
|
||||
await createWebviewWindow('登录', 'login', 320, 448, undefined, false, 320, 448)
|
||||
|
||||
@@ -13,6 +13,7 @@ export const useTauriListener = () => {
|
||||
const listeners: Promise<UnlistenFn>[] = []
|
||||
const instance = getCurrentInstance()
|
||||
const windowLabel = WebviewWindow.getCurrent().label
|
||||
let isComponentMounted = true
|
||||
|
||||
/**
|
||||
* 添加事件监听器
|
||||
@@ -59,6 +60,9 @@ export const useTauriListener = () => {
|
||||
* 清理当前组件的监听器
|
||||
*/
|
||||
const cleanup = async () => {
|
||||
// 标记组件为未挂载状态
|
||||
isComponentMounted = false
|
||||
|
||||
// 只有当存在监听器时才打印日志和执行清理
|
||||
if (listeners.length > 0) {
|
||||
const componentName = instance?.type?.name || instance?.type?.__name || '未知组件'
|
||||
@@ -143,7 +147,12 @@ export const useTauriListener = () => {
|
||||
|
||||
// 只在组件实例存在时才注册 onUnmounted 钩子
|
||||
if (instance) {
|
||||
onUnmounted(cleanup)
|
||||
onUnmounted(() => {
|
||||
// 检查组件是否仍然挂载,避免重复执行清理
|
||||
if (isComponentMounted) {
|
||||
cleanup()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -583,8 +583,6 @@ export const useWebRtc = (roomId: string, remoteUserId: string, callType: CallTy
|
||||
info('收到 offer')
|
||||
connectionStatus.value = RTCCallStatus.CALLING
|
||||
await nextTick()
|
||||
// 开启铃声
|
||||
startBell()
|
||||
|
||||
await getDevices()
|
||||
const hasLocalStream = await getLocalStream(video ? CallTypeEnum.VIDEO : CallTypeEnum.AUDIO)
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
:current-label="appWindow.label" />
|
||||
|
||||
<!-- 顶部搜索栏 -->
|
||||
<header
|
||||
class="mt-30px w-full h-40px flex flex-col items-center border-b-(1px solid [--right-chat-footer-line-color])">
|
||||
<header class="mt-30px pb-10px flex-1 flex-col-x-center border-b-(1px solid [--right-chat-footer-line-color])">
|
||||
<div class="flex-center gap-5px w-full pr-16px pl-16px box-border">
|
||||
<n-input
|
||||
id="search"
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
|
||||
<!-- 聊天界面背景图标 -->
|
||||
<div v-else class="flex-center size-full select-none">
|
||||
<img
|
||||
v-if="imgTheme === ThemeEnum.DARK && themes.versatile === 'default' && !isDetails"
|
||||
class="w-110px h-100px"
|
||||
src="@/assets/img/hula_bg_d.svg"
|
||||
alt="" />
|
||||
<img v-else-if="imgTheme === ThemeEnum.DARK" class="w-110px h-100px" src="@/assets/img/hula-bg-h.png" alt="" />
|
||||
<img v-else class="svg-icon w-110px h-100px" src="@/assets/img/hula_bg_l.png" alt="" />
|
||||
<img class="w-150px h-140px" src="/logoD.png" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -11,10 +11,10 @@ export class IosAdapter implements MobileClientInterface {
|
||||
|
||||
// 手动获取其安全区域值
|
||||
const insets: SafeArea = {
|
||||
top: parseInt(rootStyle.getPropertyValue('--safe-area-inset-top') || '0'),
|
||||
bottom: parseInt(rootStyle.getPropertyValue('--safe-area-inset-bottom') || '0'),
|
||||
left: parseInt(rootStyle.getPropertyValue('--safe-area-inset-left') || '0'),
|
||||
right: parseInt(rootStyle.getPropertyValue('--safe-area-inset-right') || '0')
|
||||
top: parseInt(rootStyle.getPropertyValue('--safe-area-inset-top') || '0', 10),
|
||||
bottom: parseInt(rootStyle.getPropertyValue('--safe-area-inset-bottom') || '0', 10),
|
||||
left: parseInt(rootStyle.getPropertyValue('--safe-area-inset-left') || '0', 10),
|
||||
right: parseInt(rootStyle.getPropertyValue('--safe-area-inset-right') || '0', 10)
|
||||
}
|
||||
|
||||
return insets
|
||||
|
||||
@@ -267,6 +267,11 @@ const routes: Array<RouteRecordRaw> = [
|
||||
path: '/manageStore',
|
||||
name: 'manageStore',
|
||||
component: () => import('@/views/moreWindow/settings/ManageStore.vue')
|
||||
},
|
||||
{
|
||||
path: '/shortcut',
|
||||
name: 'shortcut',
|
||||
component: () => import('@/views/moreWindow/settings/Shortcut.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -81,12 +81,8 @@ export default {
|
||||
requestFriendList: (params?: any) => GET<ListResponse<RequestFriendItem>>(urls.requestFriendList, params),
|
||||
/** 发送添加好友请求 */
|
||||
sendAddFriendRequest: (params: { targetUid: string; msg: string }) => POST(urls.sendAddFriendRequest, params),
|
||||
/** 同意好友申请 */
|
||||
applyFriendRequest: (params: { applyId: string }) => PUT(urls.sendAddFriendRequest, params),
|
||||
/** 忽略好友申请 */
|
||||
ignoreFriendRequest: (params: { applyId: string }) => PUT(urls.ignoreFriendRequest, params),
|
||||
/** 拒绝好友申请 */
|
||||
rejectFriendRequest: (params: { applyId: string }) => PUT(urls.rejectFriendRequest, params),
|
||||
/** 同意邀请进群或好友申请,1 同意 2 拒绝 */
|
||||
handleInviteApi: (body: { applyId: string; state: number }) => POST(urls.handleInvite, body),
|
||||
/** 删除好友 */
|
||||
deleteFriend: (params: { targetUid: string }) => DELETE(urls.deleteFriend, params),
|
||||
/** 好友申请未读数 */
|
||||
|
||||
@@ -55,7 +55,7 @@ const signUtils = {
|
||||
*/
|
||||
getTencentSign(text: string, secretId: string, secretKey: string, timestamp: string): string {
|
||||
// 根据时间戳生成UTC日期字符串
|
||||
const date = new Date(parseInt(timestamp) * 1000)
|
||||
const date = new Date(parseInt(timestamp, 10) * 1000)
|
||||
const dateStr = date.toISOString().split('T')[0]
|
||||
const service = 'tmt' // 服务名称
|
||||
|
||||
|
||||
@@ -44,11 +44,8 @@ export default {
|
||||
getContactList: `${prefix + URLEnum.USER}/friend/page`, // 联系人列表
|
||||
requestFriendList: `${prefix + URLEnum.ROOM}/apply/page`, // 好友申请、群聊邀请列表
|
||||
newFriendCount: `${prefix + URLEnum.ROOM}/apply/unread`, // 申请未读数
|
||||
acceptInvite: `${prefix + URLEnum.ROOM}/apply/accept`, // 审批别人邀请的进群
|
||||
handleInvite: `${prefix + URLEnum.ROOM}/apply/handler/apply`, // 审批别人邀请的进群
|
||||
sendAddFriendRequest: `${prefix + URLEnum.ROOM}/apply/apply`, // 申请好友\同意申请
|
||||
rejectFriendRequest: `${prefix + URLEnum.ROOM}/apply/reject`, // 拒绝好友申请
|
||||
ignoreFriendRequest: `${prefix + URLEnum.ROOM}/apply/ignore`, // 忽略好友申请
|
||||
deleteFriendRequest: `${prefix + URLEnum.ROOM}/apply/delete`, // 删除好友、群聊申请
|
||||
deleteFriend: `${prefix + URLEnum.USER}/friend`, // 删除好友
|
||||
modifyFriendRemark: `${prefix + URLEnum.USER}/friend/updateRemark`, // 修改好友备注
|
||||
|
||||
@@ -76,7 +73,7 @@ export default {
|
||||
updateMyRoomInfo: `${prefix + URLEnum.ROOM}/updateMyRoomInfo`, // 修改“我”的群聊名称
|
||||
searchGroup: `${prefix + URLEnum.ROOM}/search`, // 搜索群聊
|
||||
applyGroup: `${prefix + URLEnum.ROOM}/apply/group`, // 申请加群
|
||||
applyHandle: `${prefix + URLEnum.ROOM}/apply/handle`, // 处理加群申请 [仅仅管理员、群主可调用]
|
||||
applyHandle: `${prefix + URLEnum.ROOM}/apply/adminHandleApply`, // 处理加群申请 [仅仅管理员、群主可调用]
|
||||
applyGroupList: `${prefix + URLEnum.ROOM}/apply/group/list`, // 申请加群列表 [仅仅管理员、群主可见]
|
||||
getAnnouncementList: `${prefix + URLEnum.ROOM}/announcement/list`, // 获取群公告
|
||||
pushAnnouncement: `${prefix + URLEnum.ROOM}/announcement/push`, // 发布群公告
|
||||
@@ -88,9 +85,9 @@ export default {
|
||||
sendCaptcha: `${prefix + URLEnum.TOKEN}/anyTenant/sendEmailCode`, // 发送验证码到用户邮箱
|
||||
|
||||
// -------------- 系统相关 ---------------
|
||||
fileUpload: `${prefix + URLEnum.SYSTEM + URLEnum.OSS}/upload/url`, // 文件上传
|
||||
fileUpload: `${prefix + URLEnum.SYSTEM}/upload/url`, // 文件上传
|
||||
initConfig: `${prefix + URLEnum.SYSTEM}/anyTenant/config/init`, // 获取配置文件
|
||||
getQiniuToken: `${prefix + URLEnum.TOKEN}/anyTenant/ossToken`, // 获取七牛云上传token
|
||||
getQiniuToken: `${prefix + URLEnum.SYSTEM}/anyTenant/ossToken`, // 获取七牛云上传token
|
||||
|
||||
// -------------- token相关 ---------------
|
||||
register: `${prefix + URLEnum.TOKEN}/anyTenant/registerByEmail`, // 注册
|
||||
|
||||
@@ -21,6 +21,7 @@ let webSocketService: any
|
||||
|
||||
if (USE_RUST_WEBSOCKET) {
|
||||
// 使用 Rust WebSocket 实现
|
||||
// TODO: 这里会初始化多次,会根据窗口来初始化,需要实现单例模式
|
||||
info('🦀 使用 Rust WebSocket 实现')
|
||||
webSocketService = import('./webSocketRust').then((module) => module.default)
|
||||
} else {
|
||||
|
||||
@@ -41,13 +41,69 @@ export interface WebSocketEvent {
|
||||
* Rust WebSocket 客户端封装
|
||||
* 提供与原始 WebSocket Worker 兼容的接口
|
||||
*/
|
||||
/**
|
||||
* 监听器管理器,类似 AbortController
|
||||
*/
|
||||
class ListenerController {
|
||||
private listeners: Set<UnlistenFn> = new Set()
|
||||
private isAborted = false
|
||||
|
||||
add(unlisten: UnlistenFn): void {
|
||||
if (this.isAborted) {
|
||||
// 如果已经中止,立即清理新添加的监听器
|
||||
unlisten()
|
||||
return
|
||||
}
|
||||
this.listeners.add(unlisten)
|
||||
}
|
||||
|
||||
async abort(): Promise<void> {
|
||||
if (this.isAborted) return
|
||||
|
||||
this.isAborted = true
|
||||
const cleanupPromises: Promise<void>[] = []
|
||||
|
||||
// 并行执行所有清理操作
|
||||
for (const unlisten of this.listeners) {
|
||||
cleanupPromises.push(
|
||||
Promise.resolve()
|
||||
.then(() => unlisten())
|
||||
.catch((err) => {
|
||||
error(`[ListenerController] 清理监听器失败: ${err}`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// 等待所有清理完成(设置超时防止阻塞)
|
||||
try {
|
||||
await Promise.race([
|
||||
Promise.all(cleanupPromises),
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error('清理超时')), 5000))
|
||||
])
|
||||
} catch (err) {
|
||||
warn(`[ListenerController] 部分监听器清理可能未完成: ${err}`)
|
||||
}
|
||||
|
||||
this.listeners.clear()
|
||||
info(`[ListenerController] 已清理所有监听器`)
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.listeners.size
|
||||
}
|
||||
|
||||
get aborted(): boolean {
|
||||
return this.isAborted
|
||||
}
|
||||
}
|
||||
|
||||
class RustWebSocketClient {
|
||||
private eventListener: UnlistenFn | null = null
|
||||
private listenerController: ListenerController = new ListenerController()
|
||||
private isInitialized = false
|
||||
|
||||
constructor() {
|
||||
info('[RustWS] Rust WebSocket 客户端初始化')
|
||||
this.setupEventListener()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,11 +235,20 @@ class RustWebSocketClient {
|
||||
*/
|
||||
private async setupEventListener(): Promise<void> {
|
||||
try {
|
||||
info(`[RustWS] 开始设置事件监听器,当前业务监听器数量: ${this.listenerController.size}`)
|
||||
|
||||
// 清理旧的监听器
|
||||
if (this.eventListener) {
|
||||
this.eventListener()
|
||||
info('[RustWS] 已清理主事件监听器')
|
||||
}
|
||||
|
||||
// 高效清理所有业务监听器(并行 + 超时)
|
||||
const oldListenerCount = this.listenerController.size
|
||||
await this.listenerController.abort()
|
||||
this.listenerController = new ListenerController()
|
||||
info(`[RustWS] 已高效清理 ${oldListenerCount} 个业务监听器`)
|
||||
|
||||
// 监听 WebSocket 事件
|
||||
this.eventListener = await listen<WebSocketEvent>('websocket-event', (event) => {
|
||||
this.handleWebSocketEvent(event.payload)
|
||||
@@ -192,7 +257,7 @@ class RustWebSocketClient {
|
||||
// 设置业务消息监听器
|
||||
await this.setupBusinessMessageListeners()
|
||||
|
||||
info('[RustWS] 事件监听器设置完成')
|
||||
info(`[RustWS] 事件监听器设置完成,新的业务监听器数量: ${this.listenerController.size}`)
|
||||
} catch (err) {
|
||||
error(`[RustWS] 设置事件监听器失败: ${err}`)
|
||||
}
|
||||
@@ -267,168 +332,231 @@ class RustWebSocketClient {
|
||||
*/
|
||||
private async setupBusinessMessageListeners(): Promise<void> {
|
||||
// 连接状态相关事件
|
||||
await listen('ws-connection-lost', (event: any) => {
|
||||
warn(`[RustWS] 收到连接丢失事件: ${JSON.stringify(event.payload)}`)
|
||||
this.handleConnectionLost(event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-connection-lost', (event: any) => {
|
||||
warn(`[RustWS] 收到连接丢失事件: ${JSON.stringify(event.payload)}`)
|
||||
this.handleConnectionLost(event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 登录相关事件
|
||||
await listen('ws-login-qr-code', (event: any) => {
|
||||
info('获取二维码')
|
||||
useMitt.emit(WsResponseMessageType.LOGIN_QR_CODE, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-login-qr-code', (event: any) => {
|
||||
info('获取二维码')
|
||||
useMitt.emit(WsResponseMessageType.LOGIN_QR_CODE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-waiting-authorize', () => {
|
||||
info('等待授权')
|
||||
useMitt.emit(WsResponseMessageType.WAITING_AUTHORIZE)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-waiting-authorize', () => {
|
||||
info('等待授权')
|
||||
useMitt.emit(WsResponseMessageType.WAITING_AUTHORIZE)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-login-success', (event: any) => {
|
||||
info('登录成功')
|
||||
useMitt.emit(WsResponseMessageType.LOGIN_SUCCESS, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-login-success', (event: any) => {
|
||||
info('登录成功')
|
||||
useMitt.emit(WsResponseMessageType.LOGIN_SUCCESS, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 消息相关事件
|
||||
await listen('ws-receive-message', (event: any) => {
|
||||
info(`[ws]收到消息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, event.payload)
|
||||
})
|
||||
const listenerIndex = this.listenerController.size
|
||||
this.listenerController.add(
|
||||
await listen('ws-receive-message', (event: any) => {
|
||||
info(`[ws]收到消息[监听器${listenerIndex}]: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.RECEIVE_MESSAGE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 消息相关事件
|
||||
await listen('ws-join-group', async (event: any) => {
|
||||
info(`[ws]收到消息: ${JSON.stringify(event.payload)}`)
|
||||
// 更新群聊列表
|
||||
await contactStore.getGroupChatList()
|
||||
})
|
||||
// 群组相关事件
|
||||
this.listenerController.add(
|
||||
await listen('ws-join-group', async (event: any) => {
|
||||
info(`[ws]加入群组: ${JSON.stringify(event.payload)}`)
|
||||
// 更新群聊列表
|
||||
await contactStore.getGroupChatList()
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-msg-recall', (event: any) => {
|
||||
info('撤回')
|
||||
useMitt.emit(WsResponseMessageType.MSG_RECALL, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-msg-recall', (event: any) => {
|
||||
info('撤回')
|
||||
useMitt.emit(WsResponseMessageType.MSG_RECALL, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-msg-mark-item', (event: any) => {
|
||||
info(`消息标记: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.MSG_MARK_ITEM, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-msg-mark-item', (event: any) => {
|
||||
info(`消息标记: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.MSG_MARK_ITEM, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 用户状态相关事件
|
||||
await listen('ws-online', (event: any) => {
|
||||
info(`上线: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ONLINE, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-online', (event: any) => {
|
||||
info(`上线: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ONLINE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-offline', () => {
|
||||
info('下线')
|
||||
useMitt.emit(WsResponseMessageType.OFFLINE)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-offline', () => {
|
||||
info('下线')
|
||||
useMitt.emit(WsResponseMessageType.OFFLINE)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-user-state-change', (event: any) => {
|
||||
info(`用户状态改变: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.USER_STATE_CHANGE, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-user-state-change', (event: any) => {
|
||||
info(`用户状态改变: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.USER_STATE_CHANGE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 好友相关事件
|
||||
await listen('ws-request-new-friend', (event: any) => {
|
||||
info('好友申请')
|
||||
useMitt.emit(WsResponseMessageType.REQUEST_NEW_FRIEND, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-request-new-friend', (event: any) => {
|
||||
info('好友申请')
|
||||
useMitt.emit(WsResponseMessageType.REQUEST_NEW_FRIEND, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-request-approval-friend', (event: any) => {
|
||||
info(`同意好友申请: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.REQUEST_APPROVAL_FRIEND, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-request-approval-friend', (event: any) => {
|
||||
info(`同意好友申请: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.REQUEST_APPROVAL_FRIEND, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-new-friend-session', (event: any) => {
|
||||
info('成员变动')
|
||||
useMitt.emit(WsResponseMessageType.NEW_FRIEND_SESSION, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-new-friend-session', (event: any) => {
|
||||
info('成员变动')
|
||||
useMitt.emit(WsResponseMessageType.NEW_FRIEND_SESSION, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 房间/群聊相关事件
|
||||
await listen('ws-room-info-change', (event: any) => {
|
||||
info(`群主修改群聊信息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_INFO_CHANGE, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-room-info-change', (event: any) => {
|
||||
info(`群主修改群聊信息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_INFO_CHANGE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-my-room-info-change', (event: any) => {
|
||||
info(`自己修改我在群里的信息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.MY_ROOM_INFO_CHANGE, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-my-room-info-change', (event: any) => {
|
||||
info(`自己修改我在群里的信息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.MY_ROOM_INFO_CHANGE, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-room-group-notice-msg', (event: any) => {
|
||||
info(`发布群公告: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_GROUP_NOTICE_MSG, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-room-group-notice-msg', (event: any) => {
|
||||
info(`发布群公告: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_GROUP_NOTICE_MSG, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-room-edit-group-notice-msg', (event: any) => {
|
||||
info(`编辑群公告: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_EDIT_GROUP_NOTICE_MSG, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-room-edit-group-notice-msg', (event: any) => {
|
||||
info(`编辑群公告: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_EDIT_GROUP_NOTICE_MSG, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-room-dissolution', (event: any) => {
|
||||
info(`群解散: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_DISSOLUTION, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-room-dissolution', (event: any) => {
|
||||
info(`群解散: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.ROOM_DISSOLUTION, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 视频通话相关事件
|
||||
await listen('ws-video-call-request', (event: any) => {
|
||||
info(`收到通话请求: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.VideoCallRequest, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-video-call-request', (event: any) => {
|
||||
info(`收到通话请求: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.VideoCallRequest, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-call-accepted', (event: any) => {
|
||||
info(`通话被接受: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.CallAccepted, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-call-accepted', (event: any) => {
|
||||
info(`通话被接受: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.CallAccepted, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-call-rejected', (event: any) => {
|
||||
info(`通话被拒绝: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.CallRejected, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-call-rejected', (event: any) => {
|
||||
info(`通话被拒绝: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.CallRejected, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-room-closed', (event: any) => {
|
||||
info(`房间已关闭: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.RoomClosed, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-room-closed', (event: any) => {
|
||||
info(`房间已关闭: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.RoomClosed, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-webrtc-signal', (event: any) => {
|
||||
info(`收到信令消息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.WEBRTC_SIGNAL, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-webrtc-signal', (event: any) => {
|
||||
info(`收到信令消息: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.WEBRTC_SIGNAL, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-join-video', (event: any) => {
|
||||
info(`用户加入房间: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.JoinVideo, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-join-video', (event: any) => {
|
||||
info(`用户加入房间: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.JoinVideo, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-leave-video', (event: any) => {
|
||||
info(`用户离开房间: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.LeaveVideo, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-leave-video', (event: any) => {
|
||||
info(`用户离开房间: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.LeaveVideo, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-dropped', (event: any) => {
|
||||
useMitt.emit(WsResponseMessageType.DROPPED, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-dropped', (event: any) => {
|
||||
useMitt.emit(WsResponseMessageType.DROPPED, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-cancel', (event: any) => {
|
||||
info(`已取消通话: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.CANCEL, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-cancel', (event: any) => {
|
||||
info(`已取消通话: ${JSON.stringify(event.payload)}`)
|
||||
useMitt.emit(WsResponseMessageType.CANCEL, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 系统相关事件
|
||||
await listen('ws-token-expired', (event: any) => {
|
||||
info('账号在其他设备登录')
|
||||
useMitt.emit(WsResponseMessageType.TOKEN_EXPIRED, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-token-expired', (event: any) => {
|
||||
info('账号在其他设备登录')
|
||||
useMitt.emit(WsResponseMessageType.TOKEN_EXPIRED, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
await listen('ws-invalid-user', (event: any) => {
|
||||
info('无效用户')
|
||||
useMitt.emit(WsResponseMessageType.INVALID_USER, event.payload)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-invalid-user', (event: any) => {
|
||||
info('无效用户')
|
||||
useMitt.emit(WsResponseMessageType.INVALID_USER, event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 未知消息类型
|
||||
await listen('ws-unknown-message', (event: any) => {
|
||||
info(`接收到未处理类型的消息: ${JSON.stringify(event.payload)}`)
|
||||
})
|
||||
this.listenerController.add(
|
||||
await listen('ws-unknown-message', (event: any) => {
|
||||
info(`接收到未处理类型的消息: ${JSON.stringify(event.payload)}`)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,6 +631,12 @@ class RustWebSocketClient {
|
||||
|
||||
info(`[RustWS] 当前窗口 [${windowLabel}] 需要 WebSocket 连接`)
|
||||
|
||||
// 首次初始化时设置事件监听器
|
||||
if (this.listenerController.size === 0) {
|
||||
info('[RustWS] 首次初始化,设置事件监听器')
|
||||
await this.setupEventListener()
|
||||
}
|
||||
|
||||
// 检查当前连接状态
|
||||
const isConnected = await this.isConnected()
|
||||
|
||||
@@ -515,6 +649,10 @@ class RustWebSocketClient {
|
||||
} catch (err) {
|
||||
warn(`[RustWS] 检查连接状态失败,尝试智能重连: ${err}`)
|
||||
try {
|
||||
// 确保事件监听器已设置
|
||||
if (this.listenerController.size === 0) {
|
||||
await this.setupEventListener()
|
||||
}
|
||||
await this.initConnect()
|
||||
} catch (reconnectError) {
|
||||
error(`[RustWS] 智能重连失败: ${reconnectError}`)
|
||||
@@ -551,49 +689,65 @@ class RustWebSocketClient {
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy(): void {
|
||||
async destroy(): Promise<void> {
|
||||
if (this.eventListener) {
|
||||
this.eventListener()
|
||||
this.eventListener = null
|
||||
}
|
||||
|
||||
// 高效清理所有业务监听器(并行 + 超时)
|
||||
await this.listenerController.abort()
|
||||
|
||||
if (this.isInitialized) {
|
||||
this.disconnect()
|
||||
await this.disconnect()
|
||||
}
|
||||
|
||||
info('[RustWS] WebSocket 客户端已销毁')
|
||||
}
|
||||
}
|
||||
|
||||
info('创建RustWebSocketClient')
|
||||
// 创建全局实例
|
||||
const rustWebSocketClient = new RustWebSocketClient()
|
||||
|
||||
// 防止重复初始化
|
||||
let isModuleInitialized = false
|
||||
|
||||
// 延迟执行智能初始化,确保页面加载完成
|
||||
setTimeout(() => {
|
||||
rustWebSocketClient.smartInitConnect()
|
||||
}, 100)
|
||||
if (!isModuleInitialized) {
|
||||
isModuleInitialized = true
|
||||
setTimeout(() => {
|
||||
info('[RustWS] 模块级别智能初始化开始')
|
||||
rustWebSocketClient.smartInitConnect()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 使用 Tauri 原生事件监听窗口焦点变化(跨平台兼容)
|
||||
;(async () => {
|
||||
try {
|
||||
const currentWindow = getCurrentWebviewWindow()
|
||||
// 防止重复设置窗口焦点监听器
|
||||
let isWindowListenerInitialized = false
|
||||
|
||||
// 监听窗口获得焦点事件
|
||||
await currentWindow.listen('tauri://focus', () => {
|
||||
info('[RustWS] 窗口获得焦点,设置应用状态为前台')
|
||||
rustWebSocketClient.setAppBackgroundState(false)
|
||||
})
|
||||
if (!isWindowListenerInitialized) {
|
||||
isWindowListenerInitialized = true
|
||||
;(async () => {
|
||||
try {
|
||||
const currentWindow = getCurrentWebviewWindow()
|
||||
|
||||
// 监听窗口失去焦点事件
|
||||
await currentWindow.listen('tauri://blur', () => {
|
||||
info('[RustWS] 窗口失去焦点,设置应用状态为后台')
|
||||
rustWebSocketClient.setAppBackgroundState(true)
|
||||
})
|
||||
// 监听窗口获得焦点事件
|
||||
await currentWindow.listen('tauri://focus', () => {
|
||||
info('[RustWS] 窗口获得焦点,设置应用状态为前台')
|
||||
rustWebSocketClient.setAppBackgroundState(false)
|
||||
})
|
||||
|
||||
info('[RustWS] 窗口焦点事件监听器已设置')
|
||||
} catch (err) {
|
||||
error(`[RustWS] 设置窗口焦点监听器失败: ${err}`)
|
||||
}
|
||||
})()
|
||||
// 监听窗口失去焦点事件
|
||||
await currentWindow.listen('tauri://blur', () => {
|
||||
info('[RustWS] 窗口失去焦点,设置应用状态为后台')
|
||||
rustWebSocketClient.setAppBackgroundState(true)
|
||||
})
|
||||
|
||||
info('[RustWS] 窗口焦点事件监听器已设置')
|
||||
} catch (err) {
|
||||
error(`[RustWS] 设置窗口焦点监听器失败: ${err}`)
|
||||
}
|
||||
})()
|
||||
}
|
||||
|
||||
export default rustWebSocketClient
|
||||
|
||||
@@ -333,7 +333,8 @@ export const useChatStore = defineStore(
|
||||
// 用会话列表第一个去请求消息列表
|
||||
await getMsgList()
|
||||
// 请求第一个群成员列表
|
||||
currentRoomType.value === RoomTypeEnum.GROUP && (await groupStore.getGroupUserList(data[0].roomId))
|
||||
currentRoomType.value === RoomTypeEnum.GROUP &&
|
||||
(await groupStore.getGroupUserList(globalStore.currentSession.roomId))
|
||||
// 初始化所有用户基本信息
|
||||
userStore.isSign && (await cachedStore.initAllUserBaseInfo())
|
||||
// 联系人列表
|
||||
@@ -491,7 +492,7 @@ export const useChatStore = defineStore(
|
||||
const getMsgIndex = (msgId: string) => {
|
||||
if (!msgId) return -1
|
||||
const keys = currentMessageMap.value ? Array.from(currentMessageMap.value.keys()) : []
|
||||
return keys.findIndex((key) => key === msgId)
|
||||
return keys.indexOf(msgId)
|
||||
}
|
||||
|
||||
// 更新所有标记类型的数量
|
||||
|
||||
@@ -135,27 +135,20 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时默认执行一次加载
|
||||
// getContactList()
|
||||
// getRequestFriendsList()
|
||||
|
||||
/**
|
||||
* 接受好友请求
|
||||
* @param applyId 好友申请ID
|
||||
* 处理流程:
|
||||
* 1. 调用接口同意好友申请
|
||||
* 2. 刷新好友申请列表
|
||||
* 3. 刷新好友列表
|
||||
* 4. 更新当前选中联系人的状态
|
||||
* 5. 更新未读数
|
||||
* 处理好友/群申请
|
||||
* @param apply 好友申请信息
|
||||
* @param state 处理状态 0拒绝 2同意 3忽略
|
||||
*/
|
||||
const onAcceptFriend = async (applyId: string) => {
|
||||
const onHandleInvite = async (apply: { applyId: string; state: number }) => {
|
||||
// 同意好友申请
|
||||
apis.applyFriendRequest({ applyId }).then(async () => {
|
||||
apis.handleInviteApi(apply).then(async () => {
|
||||
// 刷新好友申请列表
|
||||
await getRequestFriendsList(true)
|
||||
// 刷新好友列表
|
||||
await getContactList(true)
|
||||
// 获取最新的未读数
|
||||
await getNewFriendCount()
|
||||
// 更新当前选中联系人的状态
|
||||
if (globalStore.currentSelectedContact) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
@@ -167,42 +160,6 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 拒绝好友请求
|
||||
* @param applyId 好友申请ID
|
||||
* 处理流程:
|
||||
* 1. 调用接口拒绝好友申请
|
||||
* 2. 刷新好友申请列表
|
||||
* 3. 更新未读数
|
||||
*/
|
||||
const onRejectFriend = async (applyId: string) => {
|
||||
// 拒绝好友申请
|
||||
apis.rejectFriendRequest({ applyId }).then(async () => {
|
||||
// 刷新好友申请列表
|
||||
await getRequestFriendsList(true)
|
||||
// 获取最新的未读数
|
||||
await getNewFriendCount()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 忽略好友请求
|
||||
* @param applyId 好友申请ID
|
||||
* 处理流程:
|
||||
* 1. 调用接口忽略好友申请
|
||||
* 2. 刷新好友申请列表
|
||||
* 3. 更新未读数
|
||||
*/
|
||||
const onIgnoreFriend = async (applyId: string) => {
|
||||
// 忽略好友申请
|
||||
apis.ignoreFriendRequest({ applyId }).then(async () => {
|
||||
// 刷新好友申请列表
|
||||
await getRequestFriendsList(true)
|
||||
// 获取最新的未读数
|
||||
await getNewFriendCount()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除好友
|
||||
* @param uid 要删除的好友用户ID
|
||||
@@ -230,9 +187,7 @@ export const useContactStore = defineStore(StoresEnum.CONTACTS, () => {
|
||||
requestFriendsList,
|
||||
contactsOptions,
|
||||
requestFriendsOptions,
|
||||
onAcceptFriend,
|
||||
onRejectFriend,
|
||||
onIgnoreFriend,
|
||||
onDeleteContact
|
||||
onDeleteContact,
|
||||
onHandleInvite
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,6 +32,10 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
isDouble: true,
|
||||
translate: 'tencent'
|
||||
},
|
||||
shortcuts: {
|
||||
screenshot: 'CmdOrCtrl+Alt+H',
|
||||
openMainPanel: 'CmdOrCtrl+Alt+P'
|
||||
},
|
||||
page: {
|
||||
shadow: true,
|
||||
fonts: 'PingFang',
|
||||
@@ -69,6 +73,27 @@ export const useSettingStore = defineStore(StoresEnum.SETTING, {
|
||||
/** 设置菜单显示模式 */
|
||||
setShowMode(showMode: ShowModeEnum): void {
|
||||
this.showMode = showMode
|
||||
},
|
||||
/** 设置截图快捷键 */
|
||||
setScreenshotShortcut(shortcut: string) {
|
||||
if (!this.shortcuts) {
|
||||
this.shortcuts = { screenshot: 'CmdOrCtrl+Alt+H', openMainPanel: 'CmdOrCtrl+Alt+P' }
|
||||
}
|
||||
this.shortcuts.screenshot = shortcut
|
||||
},
|
||||
/** 设置打开主面板快捷键 */
|
||||
setOpenMainPanelShortcut(shortcut: string) {
|
||||
if (!this.shortcuts) {
|
||||
this.shortcuts = { screenshot: 'CmdOrCtrl+Alt+H', openMainPanel: 'CmdOrCtrl+Alt+P' }
|
||||
}
|
||||
this.shortcuts.openMainPanel = shortcut
|
||||
},
|
||||
/** 设置发送消息快捷键 */
|
||||
setSendMessageShortcut(shortcut: string): void {
|
||||
if (!this.chat) {
|
||||
this.chat = { sendKey: 'Enter', isDouble: true, translate: 'tencent' }
|
||||
}
|
||||
this.chat.sendKey = shortcut
|
||||
}
|
||||
},
|
||||
share: {
|
||||
|
||||
@@ -70,7 +70,7 @@ export const useVideoViewer = defineStore(
|
||||
|
||||
// 更新视频列表中的特定视频路径(用于下载完成后更新为本地路径)
|
||||
const updateVideoPath = (originalUrl: string, newPath: string) => {
|
||||
const index = videoList.value.findIndex((url) => url === originalUrl)
|
||||
const index = videoList.value.indexOf(originalUrl)
|
||||
if (index !== -1) {
|
||||
videoList.value[index] = newPath
|
||||
}
|
||||
|
||||
@@ -1144,7 +1144,7 @@ class VoiceMessageStrategyImpl extends AbstractMessageStrategy {
|
||||
return {
|
||||
type: MsgEnum.VOICE,
|
||||
url: assetUrl,
|
||||
size: parseInt(lastVoiceDiv.dataset.size || '0'),
|
||||
size: parseInt(lastVoiceDiv.dataset.size || '0', 10),
|
||||
duration: parseFloat(lastVoiceDiv.dataset.duration || '0'),
|
||||
filename: lastVoiceDiv.dataset.filename || 'voice.mp3'
|
||||
}
|
||||
|
||||
7
src/typings/stores.d.ts
vendored
7
src/typings/stores.d.ts
vendored
@@ -52,6 +52,13 @@ declare namespace STO {
|
||||
/** 翻译提供商 */
|
||||
translate: 'youdao' | 'tencent'
|
||||
}
|
||||
/** 快捷键设置 */
|
||||
shortcuts: {
|
||||
/** 截图快捷键 */
|
||||
screenshot: string
|
||||
/** 打开主面板快捷键 */
|
||||
openMainPanel: string
|
||||
}
|
||||
/** 界面设置 */
|
||||
page: {
|
||||
/** 是否开启阴影 */
|
||||
|
||||
@@ -284,7 +284,7 @@ export const getImageDimensions = async (
|
||||
try {
|
||||
// 获取远程图片大小
|
||||
const response = await fetch(input, { method: 'HEAD' })
|
||||
result.size = parseInt(response.headers.get('content-length') || '0')
|
||||
result.size = parseInt(response.headers.get('content-length') || '0', 10)
|
||||
} catch (_error) {
|
||||
// 如果无法获取大小,使用默认值
|
||||
result.size = 0
|
||||
|
||||
@@ -123,6 +123,18 @@ const handleClickMsg = async (group: any) => {
|
||||
// 取消状态栏闪烁
|
||||
const handleTip = async () => {
|
||||
globalStore.setTipVisible(false)
|
||||
// 取消窗口置顶
|
||||
await appWindow?.setAlwaysOnTop(false)
|
||||
|
||||
// 隐藏窗口
|
||||
await appWindow?.hide()
|
||||
|
||||
// 清空消息内容
|
||||
content.value = []
|
||||
msgCount.value = 0
|
||||
|
||||
// 重置窗口高度
|
||||
resizeWindow('notify', 280, 140)
|
||||
}
|
||||
|
||||
const debouncedHandleTip = useDebounceFn(handleTip, 100)
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
<!-- 通知样式窗口 (接收方且未接听) -->
|
||||
<div
|
||||
v-if="isReceiver && !isCallAccepted"
|
||||
data-tauri-drag-region
|
||||
class="w-360px h-100px bg-white dark:bg-gray-900 rounded-12px shadow-2xl border border-gray-200 dark:border-gray-700 flex items-center p-12px select-none backdrop-blur-md">
|
||||
class="w-360px h-full bg-white dark:bg-gray-800 flex-y-center px-12px select-none">
|
||||
<!-- 用户头像 -->
|
||||
<div class="relative mr-12px">
|
||||
<n-avatar
|
||||
@@ -13,18 +12,16 @@
|
||||
:fallback-src="themes.content === ThemeEnum.DARK ? '/logoL.png' : '/logoD.png'"
|
||||
class="rounded-12px shadow-md" />
|
||||
<!-- 通话类型指示器 -->
|
||||
<div
|
||||
class="absolute -bottom-2px -right-2px w-20px h-20px rounded-full bg-blue-500 flex items-center justify-center shadow-lg">
|
||||
<Icon
|
||||
:icon="callType === CallTypeEnum.VIDEO ? 'material-symbols:videocam' : 'material-symbols:call'"
|
||||
:size="12"
|
||||
class="text-white" />
|
||||
<div class="absolute -bottom-2px -right-2px w-20px h-20px rounded-full bg-blue-500 flex-center shadow-lg">
|
||||
<svg class="size-14px color-#fff">
|
||||
<use :href="callType === CallTypeEnum.VIDEO ? '#video-one' : '#phone-telephone'"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息和状态 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-15px font-semibold text-gray-900 dark:text-white mb-2px truncate">
|
||||
<div class="text-15px font-semibold text-gray-900 dark:text-white mb-12px truncate">
|
||||
{{ remoteUserInfo?.name || '未知用户' }}
|
||||
</div>
|
||||
<div class="text-12px text-gray-500 dark:text-gray-400 flex items-center">
|
||||
@@ -34,18 +31,18 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-6px mr-7">
|
||||
<div class="flex gap-16px mr-8">
|
||||
<!-- 拒绝按钮 -->
|
||||
<div
|
||||
@click="rejectCall"
|
||||
class="w-44px h-44px rounded-full bg-#d5304f hover:bg-#d5304f active:bg-#d5304f flex items-center justify-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95">
|
||||
<Icon icon="material-symbols:call-end" :size="20" class="text-white" />
|
||||
class="size-40px rounded-full bg-#d5304f hover:bg-#d5304f flex-center cursor-pointer shadow-lg">
|
||||
<svg class="color-#fff size-20px"><use href="#PhoneHangup"></use></svg>
|
||||
</div>
|
||||
<!-- 接听按钮 -->
|
||||
<div
|
||||
@click="acceptCall"
|
||||
class="w-44px h-44px rounded-full bg-#13987f hover:bg-#13987f active:bg-#13987f flex items-center justify-center cursor-pointer transition-all duration-200 shadow-lg hover:shadow-xl transform hover:scale-105 active:scale-95">
|
||||
<Icon icon="material-symbols:call" :size="20" class="text-white" />
|
||||
class="size-40px rounded-full bg-#13987f hover:bg-#13987f flex-center cursor-pointer shadow-lg">
|
||||
<svg class="color-#fff size-20px"><use href="#phone-telephone-entity"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,24 +63,23 @@
|
||||
|
||||
<!-- 窗口控制栏 -->
|
||||
<ActionBar
|
||||
ref="actionBarRef"
|
||||
class="relative z-10"
|
||||
:top-win-label="WebviewWindow.getCurrent().label"
|
||||
:shrink="false"
|
||||
:min-w="false"
|
||||
:max-w="false" />
|
||||
:shrink="false" />
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<div class="flex-1 flex flex-col items-center justify-center px-32px pt-60px relative z-10">
|
||||
<div class="flex-1 flex-col-center px-8px pt-6px relative z-10 min-h-0">
|
||||
<!-- 视频通话时显示视频 (只有在双方都开启视频时才显示) -->
|
||||
<div
|
||||
v-if="callType === CallTypeEnum.VIDEO && localStream && (isVideoEnabled || hasRemoteVideo)"
|
||||
class="mb-32px relative">
|
||||
class="w-full flex-1 pb-22px relative min-h-0">
|
||||
<!-- 主视频 -->
|
||||
<video
|
||||
ref="mainVideoRef"
|
||||
autoplay
|
||||
playsinline
|
||||
class="w-320px h-240px rounded-8px bg-black object-cover"></video>
|
||||
class="w-full h-full scale-x-[-1] rounded-8px bg-black object-cover"></video>
|
||||
|
||||
<!-- 画中画视频 -->
|
||||
<div class="absolute top-8px right-8px group">
|
||||
@@ -92,12 +88,13 @@
|
||||
v-if="isPipVideoVisible"
|
||||
autoplay
|
||||
playsinline
|
||||
class="w-120px h-90px rounded-4px bg-black object-cover border-2 border-white cursor-pointer hover:border-blue-400 transition-colors"
|
||||
:class="isWindowMaximized ? 'w-320px h-190px' : 'w-120px h-90px'"
|
||||
class="scale-x-[-1] rounded-8px bg-black object-cover border-2 border-white cursor-pointer"
|
||||
@click="toggleVideoLayout"></video>
|
||||
<!-- 切换提示 -->
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity bg-black bg-opacity-30 rounded-4px pointer-events-none">
|
||||
<Icon icon="material-symbols:swap-horiz" class="text-white text-20px" />
|
||||
class="absolute inset-0 flex-center opacity-0 group-hover:opacity-100 transition-opacity bg-black bg-opacity-30 rounded-8px pointer-events-none">
|
||||
<svg class="text-#fff size-20px"><use href="#switch"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,57 +126,108 @@
|
||||
</div>
|
||||
|
||||
<!-- 底部控制按钮 -->
|
||||
<div class="call-controls flex flex-col items-center pb-40px relative z-10">
|
||||
<!-- 上排按钮:静音、扬声器、摄像头 -->
|
||||
<div class="flex items-start justify-center gap-40px mb-32px">
|
||||
<div class="relative z-10">
|
||||
<!-- 视频通话最大化时的一行布局 -->
|
||||
<div v-if="callType === CallTypeEnum.VIDEO && isWindowMaximized" class="pb-10px flex-center gap-32px">
|
||||
<!-- 静音按钮 -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="toggleMute"
|
||||
class="control-btn w-60px h-60px rounded-full flex items-center justify-center cursor-pointer transition-all duration-200 mb-8px"
|
||||
class="size-44px rounded-full flex-center cursor-pointer"
|
||||
:class="isMuted ? 'bg-#d5304f60 hover:bg-#d5304f80' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<Icon :icon="isMuted ? 'material-symbols:mic-off' : 'material-symbols:mic'" :size="24" class="text-white" />
|
||||
<svg class="size-16px color-#fff"><use :href="isMuted ? '#voice-off' : '#voice'"></use></svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 whitespace-nowrap">{{ isMuted ? '无麦克风' : '麦克风' }}</div>
|
||||
<div class="text-12px text-gray-400 text-center">{{ isMuted ? '无麦克风' : '麦克风' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 扬声器按钮 -->
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="toggleSpeaker"
|
||||
class="control-btn w-60px h-60px rounded-full flex items-center justify-center cursor-pointer transition-all duration-200 mb-8px"
|
||||
:class="isSpeakerOn ? 'bg-blue-500 hover:bg-blue-400' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<Icon
|
||||
:icon="isSpeakerOn ? 'material-symbols:volume-up' : 'material-symbols:volume-down'"
|
||||
:size="24"
|
||||
class="text-white" />
|
||||
class="size-44px rounded-full flex-center cursor-pointer"
|
||||
:class="isSpeakerOn ? 'bg-#d5304f60 hover:bg-#d5304f80' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<svg class="size-16px color-#fff"><use :href="isSpeakerOn ? '#volume-mute' : '#volume-notice'"></use></svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 whitespace-nowrap">{{ isSpeakerOn ? '扬声器已开' : '扬声器' }}</div>
|
||||
<div class="text-12px text-gray-400 text-center">{{ isSpeakerOn ? '扬声器已开' : '扬声器' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 摄像头按钮 (仅视频通话时显示) -->
|
||||
<div v-if="callType === CallTypeEnum.VIDEO" class="flex flex-col items-center">
|
||||
<!-- 摄像头按钮 -->
|
||||
<div class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="toggleVideo"
|
||||
class="control-btn w-60px h-60px rounded-full flex items-center justify-center cursor-pointer transition-all duration-200 mb-8px"
|
||||
:class="isVideoEnabled ? 'bg-blue-500 hover:bg-blue-400' : 'bg-#d5304f60 hover:bg-#d5304f80'">
|
||||
<Icon
|
||||
:icon="isVideoEnabled ? 'material-symbols:videocam' : 'material-symbols:videocam-off'"
|
||||
:size="24"
|
||||
class="text-white" />
|
||||
class="size-44px rounded-full flex-center cursor-pointer"
|
||||
:class="isVideoEnabled ? 'bg-#d5304f60 hover:bg-#d5304f80' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<svg class="size-16px color-#fff">
|
||||
<use :href="isVideoEnabled ? '#video-one' : '#monitor-off'"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 whitespace-nowrap">
|
||||
<div class="text-12px text-gray-400 text-center">
|
||||
{{ isVideoEnabled ? '关闭摄像头' : '开启摄像头' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 挂断按钮 -->
|
||||
<div class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="handleCallResponse(CallResponseStatus.DROPPED)"
|
||||
class="size-44px rounded-full bg-#d5304f60 hover:bg-#d5304f80 flex-center cursor-pointer">
|
||||
<svg class="size-16px color-#fff"><use href="#PhoneHangup"></use></svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 text-center">挂断</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下排按钮:挂断 -->
|
||||
<div class="flex justify-center">
|
||||
<div
|
||||
@click="handleCallResponse(CallResponseStatus.DROPPED)"
|
||||
class="control-btn w-70px h-70px rounded-full bg-#d5304f60 hover:bg-#d5304f80 flex items-center justify-center cursor-pointer transition-all duration-200">
|
||||
<Icon icon="material-symbols:call-end" :size="32" class="text-white" />
|
||||
<!-- 非全屏时的两行布局 -->
|
||||
<div v-else class="pb-30px flex-col-x-center">
|
||||
<!-- 上排按钮:静音、扬声器、摄像头 -->
|
||||
<div class="flex-center gap-40px mb-32px">
|
||||
<!-- 静音按钮 -->
|
||||
<div class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="toggleMute"
|
||||
class="size-44px rounded-full flex-center cursor-pointer"
|
||||
:class="isMuted ? 'bg-#d5304f60 hover:bg-#d5304f80' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<svg class="size-16px color-#fff"><use :href="isMuted ? '#voice-off' : '#voice'"></use></svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 text-center">{{ isMuted ? '无麦克风' : '麦克风' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 扬声器按钮 -->
|
||||
<div class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="toggleSpeaker"
|
||||
class="size-44px rounded-full flex-center cursor-pointer"
|
||||
:class="isSpeakerOn ? 'bg-#d5304f60 hover:bg-#d5304f80' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<svg class="size-16px color-#fff">
|
||||
<use :href="isSpeakerOn ? '#volume-mute' : '#volume-notice'"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 text-center">{{ isSpeakerOn ? '扬声器已开' : '扬声器' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 摄像头按钮 (仅视频通话时显示) -->
|
||||
<div v-if="callType === CallTypeEnum.VIDEO" class="flex-col-x-center gap-8px w-80px">
|
||||
<div
|
||||
@click="toggleVideo"
|
||||
class="size-44px rounded-full flex-center cursor-pointer"
|
||||
:class="isVideoEnabled ? 'bg-#d5304f60 hover:bg-#d5304f80' : 'bg-gray-600 hover:bg-gray-500'">
|
||||
<svg class="size-16px color-#fff">
|
||||
<use :href="isVideoEnabled ? '#video-one' : '#monitor-off'"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="text-12px text-gray-400 text-center">
|
||||
{{ isVideoEnabled ? '关闭摄像头' : '开启摄像头' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下排按钮:挂断 -->
|
||||
<div class="flex-x-center">
|
||||
<div
|
||||
@click="handleCallResponse(CallResponseStatus.DROPPED)"
|
||||
class="size-66px rounded-full bg-#d5304f60 hover:bg-#d5304f80 flex-center cursor-pointer">
|
||||
<svg class="size-24px color-#fff"><use href="#PhoneHangup"></use></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,24 +237,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { primaryMonitor } from '@tauri-apps/api/window'
|
||||
import { info } from '@tauri-apps/plugin-log'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ActionBar from '@/components/windows/ActionBar.vue'
|
||||
import type ActionBar from '@/components/windows/ActionBar.vue'
|
||||
import { CallTypeEnum, RTCCallStatus, ThemeEnum } from '@/enums'
|
||||
import { useUserInfo } from '@/hooks/useCached'
|
||||
import { useWebRtc } from '@/hooks/useWebRtc'
|
||||
import { useSettingStore } from '@/stores/setting'
|
||||
import { AvatarUtils } from '@/utils/AvatarUtils'
|
||||
import { invokeSilently } from '@/utils/TauriInvokeHandler'
|
||||
import { CallResponseStatus } from '../../services/wsType'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
const avatarSrc = computed(() => AvatarUtils.getAvatarUrl(remoteUserInfo.value?.avatar as string))
|
||||
// 通过路由参数获取数据
|
||||
const route = useRoute()
|
||||
const remoteUserId = route.query.remoteUserId as string
|
||||
@@ -225,7 +272,9 @@ const {
|
||||
toggleVideo: toggleVideoWebRtc,
|
||||
isVideoEnabled,
|
||||
pauseBell,
|
||||
playBell
|
||||
playBell,
|
||||
stopBell,
|
||||
startBell
|
||||
} = useWebRtc(roomId, remoteUserId, callType, isReceiver)
|
||||
const remoteAudioRef = ref<HTMLAudioElement>()
|
||||
const isMuted = ref(false)
|
||||
@@ -237,6 +286,8 @@ const remoteUserInfo = useUserInfo(remoteUserId)
|
||||
// 视频元素引用
|
||||
const mainVideoRef = ref<HTMLVideoElement>()
|
||||
const pipVideoRef = ref<HTMLVideoElement>()
|
||||
// ActionBar 组件引用
|
||||
const actionBarRef = useTemplateRef<typeof ActionBar>('actionBarRef')
|
||||
// 视频布局状态:false=远程视频主画面,true=本地视频主画面
|
||||
const isLocalVideoMain = ref(true)
|
||||
// 通话接听状态
|
||||
@@ -249,18 +300,11 @@ console.log('当前操作系统:', currentOS)
|
||||
|
||||
const createSize = (width: number, height: number) => {
|
||||
const size = currentOS === 'windows' ? new LogicalSize(width, height) : new PhysicalSize(width, height)
|
||||
console.log(
|
||||
`创建窗口大小 ${width}x${height}:`,
|
||||
size,
|
||||
`类型: ${currentOS === 'windows' ? 'LogicalSize' : 'PhysicalSize'}`
|
||||
)
|
||||
return size
|
||||
}
|
||||
|
||||
const normalSize = createSize(500, 650)
|
||||
const notificationSize = createSize(360, 100)
|
||||
const avatarSrc = computed(() => AvatarUtils.getAvatarUrl(remoteUserInfo.value?.avatar as string))
|
||||
|
||||
// 计算属性
|
||||
const callStatusText = computed(() => {
|
||||
switch (connectionStatus.value) {
|
||||
case RTCCallStatus.CALLING:
|
||||
@@ -304,6 +348,11 @@ const isPipVideoVisible = computed(() => {
|
||||
return (hasLocalVideo.value || hasRemoteVideo.value) && !!remoteStream.value
|
||||
})
|
||||
|
||||
// 获取窗口最大化状态
|
||||
const isWindowMaximized = computed(() => {
|
||||
return actionBarRef.value?.windowMaximized
|
||||
})
|
||||
|
||||
// 视频流分配工具函数
|
||||
const assignVideoStreams = async () => {
|
||||
await nextTick()
|
||||
@@ -431,16 +480,26 @@ const toggleVideoLayout = async () => {
|
||||
|
||||
// 接听通话
|
||||
const acceptCall = async () => {
|
||||
// 立即停止铃声
|
||||
stopBell()
|
||||
isCallAccepted.value = true
|
||||
// 调用接听响应函数
|
||||
sendRtcCall2VideoCallResponse(1)
|
||||
|
||||
// 调整窗口大小为正常大小
|
||||
// 调整窗口大小为正常通话大小
|
||||
try {
|
||||
const currentWindow = WebviewWindow.getCurrent()
|
||||
await currentWindow.setSize(normalSize)
|
||||
await currentWindow.setSize(createSize(500, 650))
|
||||
await currentWindow.center()
|
||||
|
||||
// 取消窗口置顶
|
||||
await currentWindow.setAlwaysOnTop(false)
|
||||
|
||||
// 恢复标题栏按钮显示
|
||||
if (currentOS === 'macos') {
|
||||
await invokeSilently('show_title_bar_buttons', { windowLabel: currentWindow.label })
|
||||
}
|
||||
|
||||
// 确保窗口获得焦点
|
||||
try {
|
||||
await currentWindow.setFocus()
|
||||
@@ -454,6 +513,8 @@ const acceptCall = async () => {
|
||||
|
||||
// 拒绝通话
|
||||
const rejectCall = async () => {
|
||||
// 立即停止铃声
|
||||
stopBell()
|
||||
// 调用拒绝响应函数
|
||||
handleCallResponse(CallResponseStatus.REJECTED)
|
||||
}
|
||||
@@ -499,71 +560,63 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
if (isReceiver && !isCallAccepted.value) {
|
||||
// 设置通知窗口大小
|
||||
await currentWindow.setSize(notificationSize)
|
||||
if (isReceiver && !isCallAccepted.value) {
|
||||
// 接收方立即开始播放铃声
|
||||
startBell()
|
||||
|
||||
// 获取屏幕尺寸并定位到右下角
|
||||
try {
|
||||
const monitor = await primaryMonitor()
|
||||
if (monitor) {
|
||||
const margin = 20
|
||||
const taskbarHeight = 80 // Windows任务栏高度
|
||||
// 设置来电通知窗口的正确大小和位置
|
||||
await currentWindow.setSize(createSize(360, 90))
|
||||
|
||||
let screenWidth: number
|
||||
let screenHeight: number
|
||||
let x: number
|
||||
let y: number
|
||||
// 隐藏标题栏和设置窗口不可移动
|
||||
if (currentOS === 'macos') {
|
||||
await invokeSilently('hide_title_bar_buttons', { windowLabel: currentWindow.label, hideCloseButton: true })
|
||||
}
|
||||
|
||||
if (currentOS === 'windows') {
|
||||
// Windows使用逻辑像素进行计算
|
||||
screenWidth = monitor.size.width / (monitor.scaleFactor || 1)
|
||||
screenHeight = monitor.size.height / (monitor.scaleFactor || 1)
|
||||
x = Math.max(0, screenWidth - notificationSize.width - margin)
|
||||
y = Math.max(0, screenHeight - notificationSize.height - margin - taskbarHeight)
|
||||
await currentWindow.setPosition(new LogicalPosition(x, y))
|
||||
} else {
|
||||
// Mac使用物理像素进行计算
|
||||
screenWidth = monitor.size.width
|
||||
screenHeight = monitor.size.height
|
||||
x = Math.max(0, screenWidth - notificationSize.width - margin)
|
||||
y = Math.max(0, screenHeight - notificationSize.height - margin - taskbarHeight)
|
||||
await currentWindow.setPosition(new PhysicalPosition(x, y))
|
||||
}
|
||||
} else {
|
||||
// 如果无法获取主显示器信息,使用屏幕右下角的估算位置
|
||||
await currentWindow.setPosition(new LogicalPosition(800, 600))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get monitor info:', error)
|
||||
// 如果获取屏幕信息失败,使用更安全的默认位置
|
||||
await currentWindow.setPosition(new LogicalPosition(800, 600))
|
||||
// 获取屏幕尺寸并定位
|
||||
const monitor = await primaryMonitor()
|
||||
if (monitor) {
|
||||
const margin = 20
|
||||
const taskbarHeight = 40
|
||||
|
||||
let screenWidth: number
|
||||
let screenHeight: number
|
||||
let x: number
|
||||
let y: number
|
||||
|
||||
if (currentOS === 'windows') {
|
||||
// Windows使用逻辑像素进行计算,窗口在右下角
|
||||
screenWidth = monitor.size.width / (monitor.scaleFactor || 1)
|
||||
screenHeight = monitor.size.height / (monitor.scaleFactor || 1)
|
||||
x = Math.max(0, screenWidth - 360 - margin)
|
||||
y = Math.max(0, screenHeight - 90 - margin - taskbarHeight)
|
||||
await currentWindow.setPosition(new LogicalPosition(x, y))
|
||||
} else {
|
||||
// Mac使用物理像素进行计算,窗口在右上角
|
||||
screenWidth = monitor.size.width
|
||||
screenHeight = monitor.size.height
|
||||
x = Math.max(0, screenWidth - 360 - margin)
|
||||
y = margin
|
||||
await currentWindow.setPosition(new PhysicalPosition(x, y))
|
||||
}
|
||||
} else {
|
||||
// 正常大小窗口
|
||||
await currentWindow.setSize(normalSize)
|
||||
await currentWindow.center()
|
||||
// 如果无法获取主显示器信息,使用屏幕右下角的估算位置
|
||||
await currentWindow.setPosition(new LogicalPosition(800, 600))
|
||||
}
|
||||
await currentWindow.setAlwaysOnTop(true)
|
||||
} else {
|
||||
// 正常通话窗口设置
|
||||
await currentWindow.center()
|
||||
await currentWindow.setAlwaysOnTop(false)
|
||||
|
||||
// 确保窗口显示
|
||||
await currentWindow.show()
|
||||
|
||||
// 在Windows上,有时需要额外的焦点设置
|
||||
try {
|
||||
await currentWindow.setFocus()
|
||||
} catch (error) {
|
||||
console.warn('Failed to set window focus:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to setup window:', error)
|
||||
// 最后的fallback:至少尝试显示窗口
|
||||
try {
|
||||
await currentWindow.show()
|
||||
} catch (showError) {
|
||||
console.error('Failed to show window:', showError)
|
||||
// 确保标题栏按钮显示(非来电通知状态)
|
||||
if (currentOS === 'macos') {
|
||||
await invokeSilently('show_title_bar_buttons', { windowLabel: currentWindow.label })
|
||||
}
|
||||
}
|
||||
|
||||
// 确保窗口显示
|
||||
await currentWindow.show()
|
||||
await currentWindow.setFocus()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -63,19 +63,6 @@
|
||||
<span class="pl-10px">聊天</span>
|
||||
|
||||
<n-flex class="item" :size="12" vertical>
|
||||
<!-- 发送信息 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<span>发送信息快捷键</span>
|
||||
<n-select
|
||||
class="w-140px"
|
||||
size="small"
|
||||
label-field="value"
|
||||
v-model:value="chat.sendKey"
|
||||
:options="sendOptions" />
|
||||
</n-flex>
|
||||
|
||||
<span class="w-full h-1px bg-[--line-color]"></span>
|
||||
|
||||
<!-- 双击打开独立会话 -->
|
||||
<!-- <n-flex align="center" justify="space-between">
|
||||
<span>双击会话列表打开独立聊天窗口</span>
|
||||
@@ -158,7 +145,7 @@ import { type } from '@tauri-apps/plugin-os'
|
||||
import { NSwitch } from 'naive-ui'
|
||||
import { CloseBxEnum, ShowModeEnum } from '@/enums'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { fontOptions, sendOptions, translateOptions } from './config.ts'
|
||||
import { fontOptions, translateOptions } from './config.ts'
|
||||
import { topicsList } from './model.tsx'
|
||||
|
||||
const appWindow = WebviewWindow.getCurrent()
|
||||
|
||||
411
src/views/moreWindow/settings/Shortcut.vue
Normal file
411
src/views/moreWindow/settings/Shortcut.vue
Normal file
@@ -0,0 +1,411 @@
|
||||
<template>
|
||||
<n-flex vertical :size="40">
|
||||
<!-- 截图快捷键设置 -->
|
||||
<n-flex vertical class="text-(14px [--text-color])" :size="16">
|
||||
<span class="pl-10px">截图快捷键</span>
|
||||
|
||||
<n-flex class="item" :size="12" vertical>
|
||||
<!-- 截图快捷键 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-flex vertical :size="8">
|
||||
<span>{{ shortcutConfigs.screenshot.displayName }}</span>
|
||||
<span class="text-(12px #909090)">按下快捷键即可开始截图</span>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center" :size="12">
|
||||
<n-tag v-if="shortcutRegistered !== null" :type="shortcutRegistered ? 'success' : 'error'" size="small">
|
||||
{{ shortcutRegistered ? '已绑定' : '未绑定' }}
|
||||
</n-tag>
|
||||
<n-input
|
||||
:value="screenshotShortcutDisplay"
|
||||
:placeholder="screenshotShortcutDisplay"
|
||||
style="width: 130px"
|
||||
class="border-(1px solid #90909080)"
|
||||
readonly
|
||||
size="small"
|
||||
@keydown="handleShortcutInput"
|
||||
@focus="handleScreenshotFocus"
|
||||
@blur="handleScreenshotBlur">
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<svg @click="resetScreenshotShortcut" class="size-14px cursor-pointer">
|
||||
<use href="#return"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>重置</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<span class="w-full h-1px bg-[--line-color]"></span>
|
||||
|
||||
<!-- 打开主面板快捷键 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-flex vertical :size="8">
|
||||
<span>{{ shortcutConfigs.openMainPanel.displayName }}</span>
|
||||
<span class="text-(12px #909090)">按下快捷键即可切换主面板显示状态</span>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center" :size="12">
|
||||
<n-tag
|
||||
v-if="openMainPanelShortcutRegistered !== null"
|
||||
:type="openMainPanelShortcutRegistered ? 'success' : 'error'"
|
||||
size="small">
|
||||
{{ openMainPanelShortcutRegistered ? '已绑定' : '未绑定' }}
|
||||
</n-tag>
|
||||
<n-input
|
||||
:value="openMainPanelShortcutDisplay"
|
||||
:placeholder="openMainPanelShortcutDisplay"
|
||||
style="width: 130px"
|
||||
class="border-(1px solid #90909080)"
|
||||
readonly
|
||||
size="small"
|
||||
@keydown="handleOpenMainPanelShortcutInput"
|
||||
@focus="handleOpenMainPanelFocus"
|
||||
@blur="handleOpenMainPanelBlur">
|
||||
<template #suffix>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<svg @click="resetOpenMainPanelShortcut" class="size-14px cursor-pointer">
|
||||
<use href="#return"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>重置</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<span class="w-full h-1px bg-[--line-color]"></span>
|
||||
|
||||
<!-- 发送消息快捷键 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<n-flex vertical :size="8">
|
||||
<span>发送消息快捷键</span>
|
||||
<span class="text-(12px #909090)">在聊天输入框中按下快捷键发送消息</span>
|
||||
</n-flex>
|
||||
|
||||
<n-flex align="center" :size="12">
|
||||
<n-select
|
||||
v-model:value="sendMessageShortcut"
|
||||
class="w-200px"
|
||||
size="small"
|
||||
label-field="label"
|
||||
:options="sendOptions"
|
||||
@blur="handleSendMessageBlur" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { isRegistered } from '@tauri-apps/plugin-global-shortcut'
|
||||
import { type } from '@tauri-apps/plugin-os'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { sendOptions } from './config.ts'
|
||||
|
||||
const message = useMessage()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
// 快捷键配置管理
|
||||
type ShortcutConfig = {
|
||||
key: 'screenshot' | 'openMainPanel'
|
||||
value: Ref<string>
|
||||
isCapturing: Ref<boolean>
|
||||
isRegistered: Ref<boolean | null>
|
||||
original: Ref<string>
|
||||
defaultValue: string
|
||||
eventName: string
|
||||
registrationEventName: string
|
||||
displayName: string
|
||||
}
|
||||
|
||||
// 统一的快捷键配置
|
||||
const shortcutConfigs: Record<string, ShortcutConfig> = {
|
||||
screenshot: {
|
||||
key: 'screenshot',
|
||||
value: ref(settingStore.shortcuts?.screenshot),
|
||||
isCapturing: ref(false),
|
||||
isRegistered: ref<boolean | null>(null),
|
||||
original: ref(settingStore.shortcuts?.screenshot),
|
||||
defaultValue: 'CmdOrCtrl+Alt+H',
|
||||
eventName: 'shortcut-updated',
|
||||
registrationEventName: 'shortcut-registration-updated',
|
||||
displayName: '截图快捷键'
|
||||
},
|
||||
openMainPanel: {
|
||||
key: 'openMainPanel',
|
||||
value: ref(settingStore.shortcuts?.openMainPanel),
|
||||
isCapturing: ref(false),
|
||||
isRegistered: ref<boolean | null>(null),
|
||||
original: ref(settingStore.shortcuts?.openMainPanel),
|
||||
defaultValue: 'CmdOrCtrl+Alt+P',
|
||||
eventName: 'open-main-panel-shortcut-updated',
|
||||
registrationEventName: 'open-main-panel-shortcut-registration-updated',
|
||||
displayName: '切换主面板快捷键'
|
||||
}
|
||||
}
|
||||
|
||||
// 向后兼容的别名(仅保留模板中使用的)
|
||||
const screenshotShortcut = shortcutConfigs.screenshot.value
|
||||
const openMainPanelShortcut = shortcutConfigs.openMainPanel.value
|
||||
const shortcutRegistered = shortcutConfigs.screenshot.isRegistered
|
||||
const openMainPanelShortcutRegistered = shortcutConfigs.openMainPanel.isRegistered
|
||||
|
||||
// 发送消息快捷键单独处理
|
||||
const sendMessageShortcut = ref(settingStore.chat?.sendKey)
|
||||
|
||||
// 将快捷键转换为平台对应的显示文本
|
||||
const formatShortcutDisplay = (shortcut: string) => {
|
||||
const isWindows = type() === 'windows'
|
||||
return shortcut
|
||||
.replace('CmdOrCtrl', isWindows ? 'Ctrl' : 'Command')
|
||||
.replace('Cmd', 'Command')
|
||||
.split('+')
|
||||
.map((key) => key.trim())
|
||||
.map((key) => {
|
||||
// 将按键名称格式化为小写(除了特殊键)
|
||||
if (['Ctrl', 'Command', 'Alt', 'Shift'].includes(key)) {
|
||||
return key.toLowerCase()
|
||||
}
|
||||
return key.toLowerCase()
|
||||
})
|
||||
.join(' + ')
|
||||
}
|
||||
|
||||
// 输入框显示的快捷键文本(用于显示,不用于绑定)
|
||||
const screenshotShortcutDisplay = computed(() => {
|
||||
return formatShortcutDisplay(screenshotShortcut.value)
|
||||
})
|
||||
|
||||
const openMainPanelShortcutDisplay = computed(() => {
|
||||
return formatShortcutDisplay(openMainPanelShortcut.value)
|
||||
})
|
||||
|
||||
// 通用的store变化监听
|
||||
const createShortcutWatcher = (config: ShortcutConfig, storeGetter: () => string | undefined) => {
|
||||
return watch(
|
||||
storeGetter,
|
||||
(newValue) => {
|
||||
if (newValue && !config.isCapturing.value) {
|
||||
config.value.value = newValue
|
||||
config.original.value = newValue
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
|
||||
// 监听 store 变化,确保数据同步
|
||||
createShortcutWatcher(shortcutConfigs.screenshot, () => settingStore.shortcuts?.screenshot)
|
||||
createShortcutWatcher(shortcutConfigs.openMainPanel, () => settingStore.shortcuts?.openMainPanel)
|
||||
|
||||
watch(
|
||||
() => settingStore.chat?.sendKey,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
sendMessageShortcut.value = newValue
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 通用的快捷键绑定检查
|
||||
const checkShortcutRegistration = async (config: ShortcutConfig) => {
|
||||
config.isRegistered.value = await isRegistered(config.value.value)
|
||||
}
|
||||
|
||||
// 通用的快捷键输入处理
|
||||
const createShortcutInputHandler = (config: ShortcutConfig) => {
|
||||
return (event: KeyboardEvent) => {
|
||||
if (!config.isCapturing.value) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const keys: string[] = []
|
||||
|
||||
// 检查修饰键 - 统一使用CmdOrCtrl以保证跨平台兼容性
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
keys.push('CmdOrCtrl')
|
||||
}
|
||||
if (event.altKey) {
|
||||
keys.push('Alt')
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
keys.push('Shift')
|
||||
}
|
||||
|
||||
// 获取主键
|
||||
const mainKey = event.key
|
||||
if (mainKey && !['Control', 'Alt', 'Shift', 'Meta', 'Cmd'].includes(mainKey)) {
|
||||
keys.push(mainKey.toUpperCase())
|
||||
}
|
||||
|
||||
// 至少需要一个修饰键和一个主键
|
||||
if (keys.length >= 2) {
|
||||
// 更新内部存储值(使用CmdOrCtrl格式,显示会通过computed自动格式化)
|
||||
config.value.value = keys.join('+')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建具体的处理函数
|
||||
const handleShortcutInput = createShortcutInputHandler(shortcutConfigs.screenshot)
|
||||
const handleOpenMainPanelShortcutInput = createShortcutInputHandler(shortcutConfigs.openMainPanel)
|
||||
|
||||
// 通用的焦点处理
|
||||
const createFocusHandler = (config: ShortcutConfig) => {
|
||||
return async () => {
|
||||
config.isCapturing.value = true
|
||||
config.original.value = config.value.value
|
||||
console.log(`🎯 开始编辑${config.displayName}`)
|
||||
}
|
||||
}
|
||||
|
||||
const createBlurHandler = (config: ShortcutConfig, saveFunction: () => Promise<void>) => {
|
||||
return async () => {
|
||||
config.isCapturing.value = false
|
||||
console.log(`✅ 结束编辑${config.displayName}`)
|
||||
|
||||
// 如果快捷键有变化,则保存
|
||||
if (config.value.value !== config.original.value) {
|
||||
await saveFunction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建具体的焦点处理函数
|
||||
const handleScreenshotFocus = createFocusHandler(shortcutConfigs.screenshot)
|
||||
const handleOpenMainPanelFocus = createFocusHandler(shortcutConfigs.openMainPanel)
|
||||
|
||||
// 处理发送消息快捷键失去焦点事件(自动保存)
|
||||
const handleSendMessageBlur = async () => {
|
||||
// 如果快捷键有变化,则保存
|
||||
const currentSendKey = settingStore.chat?.sendKey || 'Enter'
|
||||
if (sendMessageShortcut.value !== currentSendKey) {
|
||||
await saveSendMessageShortcut()
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的保存快捷键方法
|
||||
const createSaveShortcutFunction = (config: ShortcutConfig) => {
|
||||
return async () => {
|
||||
try {
|
||||
console.log(`💾 [Settings] 开始保存${config.displayName}: ${config.value.value}`)
|
||||
|
||||
// 根据快捷键类型调用对应的store方法
|
||||
if (config.key === 'screenshot') {
|
||||
settingStore.setScreenshotShortcut(config.value.value)
|
||||
} else if (config.key === 'openMainPanel') {
|
||||
settingStore.setOpenMainPanelShortcut(config.value.value)
|
||||
}
|
||||
|
||||
config.original.value = config.value.value
|
||||
console.log(`💾 [Settings] 已保存到 Pinia store`)
|
||||
|
||||
// 通知主窗口更新快捷键(跨窗口事件)
|
||||
console.log(`📡 [Settings] 发送 ${config.eventName} 事件到主窗口`)
|
||||
await emit(config.eventName, { shortcut: config.value.value })
|
||||
console.log(`📡 [Settings] ${config.eventName} 事件已发送`)
|
||||
|
||||
message.success(`${config.displayName}已更新`)
|
||||
} catch (error) {
|
||||
console.error(`Failed to save ${config.key} shortcut:`, error)
|
||||
message.error(`${config.displayName}设置失败`)
|
||||
|
||||
// 恢复原来的快捷键
|
||||
config.value.value = config.original.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的重置快捷键方法
|
||||
const createResetShortcutFunction = (config: ShortcutConfig, saveFunction: () => Promise<void>) => {
|
||||
return async () => {
|
||||
config.value.value = config.defaultValue
|
||||
await saveFunction()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建具体的保存函数
|
||||
const saveScreenshotShortcut = createSaveShortcutFunction(shortcutConfigs.screenshot)
|
||||
const saveOpenMainPanelShortcut = createSaveShortcutFunction(shortcutConfigs.openMainPanel)
|
||||
|
||||
// 创建具体的重置函数
|
||||
const resetScreenshotShortcut = createResetShortcutFunction(shortcutConfigs.screenshot, saveScreenshotShortcut)
|
||||
const resetOpenMainPanelShortcut = createResetShortcutFunction(shortcutConfigs.openMainPanel, saveOpenMainPanelShortcut)
|
||||
|
||||
// 创建失焦处理函数
|
||||
const handleScreenshotBlur = createBlurHandler(shortcutConfigs.screenshot, saveScreenshotShortcut)
|
||||
const handleOpenMainPanelBlur = createBlurHandler(shortcutConfigs.openMainPanel, saveOpenMainPanelShortcut)
|
||||
|
||||
// 保存发送消息快捷键设置
|
||||
const saveSendMessageShortcut = async () => {
|
||||
try {
|
||||
// 保存到 pinia store
|
||||
settingStore.setSendMessageShortcut(sendMessageShortcut.value)
|
||||
|
||||
message.success('发送消息快捷键已更新')
|
||||
} catch (error) {
|
||||
console.error('Failed to save send message shortcut:', error)
|
||||
message.error('发送消息快捷键设置失败')
|
||||
|
||||
// 恢复原来的值
|
||||
sendMessageShortcut.value = settingStore.chat?.sendKey || 'Enter'
|
||||
}
|
||||
}
|
||||
|
||||
// 通用的事件监听器创建
|
||||
const createRegistrationListener = (config: ShortcutConfig) => {
|
||||
return listen(config.registrationEventName, (event: any) => {
|
||||
const { shortcut, registered } = event.payload
|
||||
console.log(`📡 [Settings] 收到${config.displayName}状态更新: ${shortcut} -> ${registered ? '已绑定' : '未绑定'}`)
|
||||
|
||||
// 只有当前快捷键匹配时才更新状态
|
||||
if (shortcut === config.value.value) {
|
||||
console.log(`📡 [Settings] ${config.displayName}匹配,更新状态为: ${registered ? '已绑定' : '未绑定'}`)
|
||||
config.isRegistered.value = registered
|
||||
} else {
|
||||
console.log(`📡 [Settings] ${config.displayName}不匹配,忽略状态更新`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 检查所有快捷键的绑定状态
|
||||
await Promise.all([
|
||||
checkShortcutRegistration(shortcutConfigs.screenshot),
|
||||
checkShortcutRegistration(shortcutConfigs.openMainPanel)
|
||||
])
|
||||
|
||||
// 创建事件监听器
|
||||
const unlistenScreenshot = await createRegistrationListener(shortcutConfigs.screenshot)
|
||||
const unlistenOpenMainPanel = await createRegistrationListener(shortcutConfigs.openMainPanel)
|
||||
|
||||
// 组件卸载时取消监听
|
||||
onUnmounted(() => {
|
||||
unlistenScreenshot()
|
||||
unlistenOpenMainPanel()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
@apply bg-[--bg-setting-item] rounded-12px size-full p-12px box-border border-(solid 1px [--line-color]) custom-shadow;
|
||||
}
|
||||
|
||||
:deep(.n-input.n-input--focus) {
|
||||
border-width: 2px;
|
||||
border-color: #13987f !important;
|
||||
}
|
||||
</style>
|
||||
@@ -16,6 +16,11 @@ const sideOptions = ref<OPT.L.SettingSide[]>([
|
||||
label: '存储管理',
|
||||
icon: 'mini-sd-card'
|
||||
},
|
||||
{
|
||||
url: '/shortcut',
|
||||
label: '快捷键管理',
|
||||
icon: 'enter-the-keyboard'
|
||||
},
|
||||
{
|
||||
url: '/loginSetting',
|
||||
label: '登录设置',
|
||||
|
||||
@@ -275,7 +275,7 @@ const previousVideo = async () => {
|
||||
// 如果是本地视频,从文件夹获取视频列表
|
||||
const folderVideos = await getVideosFromCurrentFolder(currentVideoPath)
|
||||
if (folderVideos.length > 0) {
|
||||
const currentVideoIndex = folderVideos.findIndex((path) => path === currentVideoPath)
|
||||
const currentVideoIndex = folderVideos.indexOf(currentVideoPath)
|
||||
if (currentVideoIndex > 0) {
|
||||
const previousVideoPath = folderVideos[currentVideoIndex - 1]
|
||||
videoList.value[currentIndex.value] = previousVideoPath
|
||||
@@ -323,7 +323,7 @@ const nextVideo = async () => {
|
||||
// 如果是本地视频,从文件夹获取视频列表
|
||||
const folderVideos = await getVideosFromCurrentFolder(currentVideoPath)
|
||||
if (folderVideos.length > 0) {
|
||||
const currentVideoIndex = folderVideos.findIndex((path) => path === currentVideoPath)
|
||||
const currentVideoIndex = folderVideos.indexOf(currentVideoPath)
|
||||
if (currentVideoIndex < folderVideos.length - 1) {
|
||||
const nextVideoPath = folderVideos[currentVideoIndex + 1]
|
||||
videoList.value[currentIndex.value] = nextVideoPath
|
||||
|
||||
Reference in New Issue
Block a user