mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/lyswhut/lx-music-desktop.git
synced 2026-09-20 16:14:05 +08:00
Compare commits
93 Commits
v2.1.2
...
api-source
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fe34545b9 | ||
|
|
da4be33a04 | ||
|
|
47e2a5a460 | ||
|
|
04337ef4b8 | ||
|
|
df58c8eb80 | ||
|
|
b0bf1bf4be | ||
|
|
1a3d84d4c4 | ||
|
|
735f629ca1 | ||
|
|
9dc927b50b | ||
|
|
47a336eb64 | ||
|
|
33b66aaea0 | ||
|
|
7b9eefdf2d | ||
|
|
ea5e8b56dd | ||
|
|
e52fff4d0e | ||
|
|
8d108509c9 | ||
|
|
d3cc630501 | ||
|
|
e74f297f9e | ||
|
|
8be9c75734 | ||
|
|
851851a7e6 | ||
|
|
1cbe46dc3b | ||
|
|
675a943d59 | ||
|
|
908815def3 | ||
|
|
200ca1eeda | ||
|
|
b81cc139b3 | ||
|
|
f8ae906326 | ||
|
|
e6f55804f4 | ||
|
|
1432a19d7d | ||
|
|
74fbfb130c | ||
|
|
3cf45f602f | ||
|
|
7a9dcc71fd | ||
|
|
fb5249a979 | ||
|
|
3f694b75f8 | ||
|
|
0e1ee66dd6 | ||
|
|
920e002247 | ||
|
|
ab0b352d36 | ||
|
|
aa4b08a156 | ||
|
|
66514908d2 | ||
|
|
60f0801a7c | ||
|
|
7149d0d43f | ||
|
|
d970687ebe | ||
|
|
d0425fc0d6 | ||
|
|
bdd5b46708 | ||
|
|
6d949d7e4d | ||
|
|
5d72d092fc | ||
|
|
667fc8c8a6 | ||
|
|
f7a2d9fd06 | ||
|
|
85946efd0b | ||
|
|
f6a853bfe9 | ||
|
|
4bfbdeb9a5 | ||
|
|
1e53508d67 | ||
|
|
b8fca59cbb | ||
|
|
9ada6fe4a7 | ||
|
|
b1593a2053 | ||
|
|
22a5125a11 | ||
|
|
dfd9a0019b | ||
|
|
e1d81c199b | ||
|
|
e8f574ee9c | ||
|
|
472af1bca6 | ||
|
|
671346bcdd | ||
|
|
9132abb3fe | ||
|
|
1c681e9f82 | ||
|
|
19286ef99d | ||
|
|
37ad2f522a | ||
|
|
1e1aa65fc0 | ||
|
|
ec6e067938 | ||
|
|
ea515fdd85 | ||
|
|
c11c202248 | ||
|
|
1cfee31483 | ||
|
|
00a5aa01c7 | ||
|
|
fc61cf16ed | ||
|
|
0684e29098 | ||
|
|
66d29293cd | ||
|
|
e9fd657852 | ||
|
|
bd7f51358c | ||
|
|
371b8fad05 | ||
|
|
fe259747dc | ||
|
|
d4d94ddbee | ||
|
|
f62d773246 | ||
|
|
20294233e6 | ||
|
|
cb4ab411f9 | ||
|
|
7f008d1f9c | ||
|
|
fe0487c3f9 | ||
|
|
a54e810af1 | ||
|
|
5cc1f20fac | ||
|
|
da804e1f69 | ||
|
|
3399333360 | ||
|
|
e75ab3a7d7 | ||
|
|
3b70acc3ca | ||
|
|
b42092884e | ||
|
|
fea2db0e29 | ||
|
|
fe7bc924da | ||
|
|
60b357e6f9 | ||
|
|
5544a469c4 |
6
.github/workflows/beta-pack.yml
vendored
6
.github/workflows/beta-pack.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
- name: Build src code
|
||||
run: npm run build:src
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
- name: Build src code
|
||||
run: npm run build:src
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
- name: Build src code
|
||||
run: npm run build:src
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
- name: Build src code
|
||||
run: npm run build:src
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
- name: Build src code
|
||||
run: npm run build:src
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
npm install
|
||||
npm ci
|
||||
|
||||
- name: Build src code
|
||||
run: npm run build:src
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -12,5 +12,6 @@
|
||||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"vue.codeActions.enabled": false
|
||||
}
|
||||
|
||||
67
CHANGELOG.md
67
CHANGELOG.md
@@ -6,6 +6,73 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
|
||||
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
|
||||
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
|
||||
|
||||
## [2.2.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.1...v2.2.2) - 2023-05-01
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复在低版本Linux amd64系统上无法启动的问题(glibc版本要求过高导致的,采用内置预编译二进制文件的方式解决)
|
||||
- 修复添加歌曲弹窗默认列表名字显示问题
|
||||
|
||||
## [2.2.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.2.0...v2.2.1) - 2023-05-01
|
||||
|
||||
### 优化
|
||||
|
||||
- 优化对系统Media Session的支持,现在切歌不会再会导致信息丢失的问题了
|
||||
- 启用桌面歌词时,取消对歌词窗口的聚焦
|
||||
- 增加kg歌单歌曲flac24bit显示(@helloplhm-qwq)
|
||||
- 增加tx源热门评论图片显示(@Folltoshe)
|
||||
- 优化更新弹窗弹出时机
|
||||
- 优化搜索框背景配色,使其适应高透明主题
|
||||
- 支持wy热门评论翻页
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复启用全局快捷键时与Media Session注册冲突的问题,启用全局快捷键时,不再注册媒体控制快捷键
|
||||
- 修复mg搜索不显示时长的问题(@Folltoshe)
|
||||
- 修复mg评论加载失败的问题(@Folltoshe)
|
||||
- 修复对存在错误时间标签的歌词的解析
|
||||
|
||||
### 其他
|
||||
|
||||
- 自定义源API utils对象新增`zlib.inflate`与`zlib.deflate`方法,API版本更新到 v1.3.0
|
||||
- 更新kg、tx、wy等平台排行榜列表
|
||||
- 更新 electron 到 v22.3.7
|
||||
|
||||
## [2.2.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.2...v2.2.0) - 2023-03-26
|
||||
|
||||
从v2.2.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明
|
||||
|
||||
### 不兼容性变更说明
|
||||
|
||||
- 同步功能,从这个版本起,数据同步功能至少需要移动端v1.0.0的版本才能连接,连接的地址格式也略有改变,详情看[文档说明](https://lyswhut.github.io/lx-music-doc/desktop/faq/sync)
|
||||
|
||||
### 新增
|
||||
|
||||
- 重构数据同步功能,新增客户端模式
|
||||
- 新增全屏时自动关闭歌词设置,默认开启,可以去设置-桌面歌词设置更改
|
||||
- 新增设置-桌面歌词设置-重置窗口设置功能,点击时会重置桌面歌词窗口大小及位置
|
||||
- 新增设置-其他-列表数据清理功能,点击时会清空已创建的所有列表及所有收藏的歌曲
|
||||
|
||||
### 优化
|
||||
|
||||
- 支持wy源flac hires歌曲类型的显示
|
||||
- 快捷键调整音量时每次加减2%音量改为4%(#1220)
|
||||
- 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容,保留点击切换显示/隐藏
|
||||
- 支持kg源搜索列表、排行榜flac hires歌曲类型的显示(#1231, #1238 By @helloplhm-qwq, @Folltoshe)
|
||||
- 播放速率的粒度调整为0.01,范围0.6-2.0x
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复同步连接的处理问题
|
||||
- 修复记住播放进度的情况下,使用Scheme URL打开应用播放的歌曲进度没有被重置的问题
|
||||
- 修复使用酷狗码无法打开某些类型的歌单的问题
|
||||
- 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题
|
||||
- 修复连续选择时的初始选择歌曲位置被意外改变的问题
|
||||
|
||||
### 其他
|
||||
|
||||
- 更新 Electron 到v22.3.4
|
||||
|
||||
## [2.1.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.1...v2.1.2) - 2023-02-18
|
||||
|
||||
|
||||
|
||||
3
FAQ.md
3
FAQ.md
@@ -2,6 +2,7 @@
|
||||
|
||||
本文档已迁移到:<https://lyswhut.github.io/lx-music-doc/desktop/faq>
|
||||
|
||||
<!--
|
||||
在阅读本常见问题后,仍然无法解决你的问题,请提交issue或者加企鹅群`830125506`反馈(无事勿加,入群先看群公告),反馈时请**注明**已阅读常见问题!
|
||||
|
||||
## ~~软件为什么没有桌面歌词与自定义列表功能~~
|
||||
@@ -562,4 +563,4 @@ const cancelHttp = window.lx.request(url, options, callback)
|
||||
- `window.lx.utils.crypto.randomBytes`:生成随机字符串 `randomBytes(size)`
|
||||
- `window.lx.utils.crypto.rsaEncrypt`:RSA加密 `rsaEncrypt(buffer, key)`
|
||||
|
||||
目前仅提供以上工具方法,如果需要其他方法可以开issue讨论。
|
||||
目前仅提供以上工具方法,如果需要其他方法可以开issue讨论。 -->
|
||||
|
||||
23
README.md
23
README.md
@@ -50,6 +50,8 @@
|
||||
或者到网盘下载(网盘内有MAC、windows版):`https://www.lanzoui.com/b0bf2cfa/` 密码:`glqw`(若链接无法打开请百度:蓝奏云链接打不开)<br>
|
||||
使用常见问题请转至:[常见问题](https://lyswhut.github.io/lx-music-doc/desktop/faq)
|
||||
|
||||
目前本项目的原始发布地址只有**GitHub**及**蓝奏网盘**,其他渠道均为第三方转载发布,与本项目无关!
|
||||
|
||||
#### Scheme URL支持
|
||||
|
||||
从v1.17.0起支持 Scheme URL,可以使用此功能从浏览器等场景下调用LX Music,我们开发了一个[油猴脚本](https://github.com/lyswhut/lx-music-script#readme)配套使用,<br>
|
||||
@@ -57,6 +59,10 @@
|
||||
|
||||
若你想自己调用LX Music,可以看[Scheme URL支持](https://lyswhut.github.io/lx-music-doc/desktop/scheme-url)
|
||||
|
||||
#### 数据同步服务
|
||||
|
||||
从v2.2.0起,我们发布了一个独立版的[数据同步服务](https://github.com/lyswhut/lx-music-sync-server#readme),如果你有服务器,可以将其部署到服务器上作为私人多端同步服务使用,详情看该项目说明
|
||||
|
||||
#### 启动参数
|
||||
|
||||
目前软件已支持的启动参数如下:
|
||||
@@ -111,6 +117,20 @@ npm run pack:linux
|
||||
|
||||
常见问题已移至:<https://lyswhut.github.io/lx-music-doc/desktop/faq>
|
||||
|
||||
### 贡献代码
|
||||
|
||||
本项目欢迎PR,但为了PR能顺利合并,需要注意以下几点:
|
||||
|
||||
- 对于添加新功能的PR,建议在PR前先创建issue说明,以确认该功能是否确实需要
|
||||
- 对于修复Bug PR,请提供修复前后的说明及重现方式
|
||||
- 其他类型的PR则适当附上说明
|
||||
|
||||
贡献代码步骤:
|
||||
|
||||
1. 参照[源码使用方法](https://lyswhut.github.io/lx-music-doc/desktop/use-source-code)设置开发环境
|
||||
2. 克隆本仓库代码并切换到`dev`分支开发
|
||||
3. 提交PR
|
||||
|
||||
### 项目协议
|
||||
|
||||
本项目基于 [Apache License 2.0](https://github.com/lyswhut/lx-music-desktop/blob/master/LICENSE) 许可证发行,以下协议是对于 Apache License 2.0 的补充,如有冲突,以以下协议为准。
|
||||
@@ -127,4 +147,5 @@ npm run pack:linux
|
||||
若你使用了本项目,将代表你接受以上协议。
|
||||
|
||||
音乐平台不易,请尊重版权,支持正版。<br>
|
||||
若对此有疑问请 mail to: lyswhut+qq.com (请将`+`替换成`@`)
|
||||
本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。<br>
|
||||
若对此有疑问请 mail to: lyswhut+qq.com (请将`+`替换成`@`)<br>
|
||||
|
||||
@@ -4,6 +4,7 @@ const path = require('path')
|
||||
const { Arch } = require('electron-builder')
|
||||
|
||||
const better_sqlite3_fileNameMap = {
|
||||
[Arch.x64]: 'electron-v110-linux-x64',
|
||||
[Arch.arm64]: 'electron-v110-linux-arm64',
|
||||
[Arch.armv7l]: 'electron-v110-linux-arm',
|
||||
}
|
||||
@@ -51,10 +52,11 @@ const replaceQrcDecodeLib = async(platform, arch) => {
|
||||
module.exports = async(context) => {
|
||||
const { electronPlatformName, arch } = context
|
||||
await replaceQrcDecodeLib(electronPlatformName, arch)
|
||||
if (electronPlatformName !== 'linux') return
|
||||
if (electronPlatformName !== 'linux' || process.env.FORCE) return
|
||||
const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')
|
||||
const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')
|
||||
switch (arch) {
|
||||
case Arch.x64:
|
||||
case Arch.arm64:
|
||||
case Arch.armv7l:
|
||||
if (fs.existsSync(bindingFilePath)) {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
build-config/lib/better_sqlite3_electron-v110-linux-x64.node
Normal file
BIN
build-config/lib/better_sqlite3_electron-v110-linux-x64.node
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -104,7 +104,7 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
|
||||
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
|
||||
5477
package-lock.json
generated
5477
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lx-music-desktop",
|
||||
"version": "2.1.2",
|
||||
"version": "2.3.0-beta.3",
|
||||
"description": "一个免费的音乐查找助手",
|
||||
"main": "./dist/main.js",
|
||||
"productName": "lx-music-desktop",
|
||||
@@ -205,98 +205,96 @@
|
||||
},
|
||||
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/core": "^7.21.8",
|
||||
"@babel/eslint-parser": "^7.21.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-modules-umd": "^7.18.6",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@types/better-sqlite3": "^7.6.3",
|
||||
"@babel/plugin-transform-runtime": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.5",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@types/better-sqlite3": "^7.6.4",
|
||||
"@types/needle": "^3.2.0",
|
||||
"@types/tunnel": "^0.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||
"@typescript-eslint/parser": "^5.52.0",
|
||||
"@volar/vue-language-plugin-pug": "^1.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"@volar/vue-language-plugin-pug": "^1.6.4",
|
||||
"babel-loader": "^9.1.2",
|
||||
"browserslist": "^4.21.5",
|
||||
"chalk": "^4.1.2",
|
||||
"changelog-parser": "^3.0.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"core-js": "^3.28.0",
|
||||
"core-js": "^3.30.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^6.7.3",
|
||||
"css-minimizer-webpack-plugin": "^4.2.2",
|
||||
"css-minimizer-webpack-plugin": "^5.0.0",
|
||||
"del": "^6.1.1",
|
||||
"electron": "^22.3.0",
|
||||
"electron-builder": "^24.0.0-alpha.12",
|
||||
"electron": "^22.3.8",
|
||||
"electron-builder": "^24.3.0",
|
||||
"electron-debug": "^3.2.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-to-chromium": "^1.4.301",
|
||||
"electron-updater": "^6.0.0-alpha.9",
|
||||
"eslint": "^8.34.0",
|
||||
"electron-to-chromium": "^1.4.385",
|
||||
"electron-updater": "^6.1.0",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.1",
|
||||
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
|
||||
"eslint-plugin-html": "^7.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^15.6.1",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"eslint-webpack-plugin": "^4.0.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"eslint-plugin-vue": "^9.11.1",
|
||||
"eslint-webpack-plugin": "^4.0.1",
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"less": "^4.1.3",
|
||||
"less-loader": "^11.1.0",
|
||||
"mini-css-extract-plugin": "^2.7.2",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"node-loader": "^2.0.0",
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-loader": "^7.0.2",
|
||||
"postcss": "^8.4.23",
|
||||
"postcss-loader": "^7.3.0",
|
||||
"postcss-pxtorem": "^6.0.0",
|
||||
"pug": "^3.0.2",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"rimraf": "^4.1.2",
|
||||
"rimraf": "^5.0.0",
|
||||
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"svg-transform-loader": "^2.0.13",
|
||||
"svgo-loader": "^4.0.0",
|
||||
"terser": "^5.16.4",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"terser": "^5.17.1",
|
||||
"terser-webpack-plugin": "^5.3.8",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.5",
|
||||
"vue-eslint-parser": "^9.1.0",
|
||||
"vue-loader": "^17.0.1",
|
||||
"typescript": "^5.0.4",
|
||||
"vue-eslint-parser": "^9.2.1",
|
||||
"vue-loader": "^17.1.0",
|
||||
"vue-template-compiler": "^2.7.14",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack": "^5.82.0",
|
||||
"webpack-cli": "^5.1.0",
|
||||
"webpack-dev-server": "^4.15.0",
|
||||
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simonwep/pickr": "^1.8.2",
|
||||
"better-sqlite3": "^8.1.0",
|
||||
"better-sqlite3": "^8.3.0",
|
||||
"bufferutil": "^4.0.7",
|
||||
"comlink": "~4.3.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"electron-log": "^4.4.8",
|
||||
"electron-store": "^8.1.0",
|
||||
"font-list": "^1.4.5",
|
||||
"http-terminator": "^3.2.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"image-size": "^1.0.2",
|
||||
"jschardet": "^3.0.0",
|
||||
"koa": "^2.14.1",
|
||||
"long": "^5.2.1",
|
||||
"music-metadata": "^8.1.3",
|
||||
"long": "^5.2.3",
|
||||
"music-metadata": "^8.1.4",
|
||||
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
|
||||
"node-id3": "^0.2.5",
|
||||
"socket.io": "^4.6.0",
|
||||
"node-id3": "^0.2.6",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"utf-8-validate": "^6.0.2",
|
||||
"utf-8-validate": "^6.0.3",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6"
|
||||
"vue-router": "^4.1.6",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"overrides": {
|
||||
"got": "^11",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
### 新增
|
||||
|
||||
### 修复
|
||||
- 新增音效设置(实验性功能),支持10段均衡器设置、内置的一些环境混响音效、3D立体环绕音效
|
||||
|
||||
- 修复处于最新版本时更新弹窗日志内容显示异常的问题
|
||||
- 修复更新到最新版本后的首次启动时的更新日志未显示的问题
|
||||
### 其他
|
||||
|
||||
- 更新 electron 到 v22.3.8
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -10,6 +10,7 @@ export const STORE_NAMES = {
|
||||
LRC_RAW: 'lyrics',
|
||||
LRC_EDITED: 'lyrics_edited',
|
||||
THEME: 'theme',
|
||||
SOUND_EFFECT: 'sound_effect',
|
||||
} as const
|
||||
|
||||
export const APP_EVENT_NAMES = {
|
||||
@@ -75,3 +76,24 @@ export const DOWNLOAD_STATUS = {
|
||||
} as const
|
||||
|
||||
export const QUALITYS = ['flac24bit', 'flac', 'wav', 'ape', '320k', '192k', '128k'] as const
|
||||
|
||||
export const SYNC_CODE = {
|
||||
helloMsg: 'Hello~::^-^::~v3~',
|
||||
idPrefix: 'OjppZDo6',
|
||||
authMsg: 'lx-music auth::',
|
||||
authFailed: 'Auth failed',
|
||||
missingAuthCode: 'Missing auth code',
|
||||
getServiceIdFailed: 'Get service id failed',
|
||||
connectServiceFailed: 'Connect service failed',
|
||||
connecting: 'Connecting...',
|
||||
unknownServiceAddress: 'Unknown service address',
|
||||
msgBlockedIp: 'Blocked IP',
|
||||
msgConnect: 'lx-music connect',
|
||||
msgAuthFailed: 'Auth failed',
|
||||
} as const
|
||||
|
||||
export const SYNC_CLOSE_CODE = {
|
||||
normal: 1000,
|
||||
failed: 4100,
|
||||
} as const
|
||||
|
||||
|
||||
@@ -29,21 +29,21 @@ const local: LX.HotKeyConfig = {
|
||||
const global: LX.HotKeyConfig = {
|
||||
enable: false,
|
||||
keys: {
|
||||
MediaPlayPause: {
|
||||
type: HOTKEY_PLAYER.toggle_play.type,
|
||||
name: '',
|
||||
action: HOTKEY_PLAYER.toggle_play.action,
|
||||
},
|
||||
MediaPreviousTrack: {
|
||||
type: HOTKEY_PLAYER.prev.type,
|
||||
name: '',
|
||||
action: HOTKEY_PLAYER.prev.action,
|
||||
},
|
||||
MediaNextTrack: {
|
||||
type: HOTKEY_PLAYER.next.type,
|
||||
name: '',
|
||||
action: HOTKEY_PLAYER.next.action,
|
||||
},
|
||||
// MediaPlayPause: {
|
||||
// type: HOTKEY_PLAYER.toggle_play.type,
|
||||
// name: '',
|
||||
// action: HOTKEY_PLAYER.toggle_play.action,
|
||||
// },
|
||||
// MediaPreviousTrack: {
|
||||
// type: HOTKEY_PLAYER.prev.type,
|
||||
// name: '',
|
||||
// action: HOTKEY_PLAYER.prev.action,
|
||||
// },
|
||||
// MediaNextTrack: {
|
||||
// type: HOTKEY_PLAYER.next.type,
|
||||
// name: '',
|
||||
// action: HOTKEY_PLAYER.next.action,
|
||||
// },
|
||||
'mod+alt+f5': {
|
||||
type: HOTKEY_PLAYER.toggle_play.type,
|
||||
name: HOTKEY_PLAYER.toggle_play.name,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { join } from 'path'
|
||||
import { homedir } from 'os'
|
||||
|
||||
const isMac = process.platform == 'darwin'
|
||||
const isWin = process.platform == 'win32'
|
||||
|
||||
const defaultSetting: LX.AppSetting = {
|
||||
version: '2.0.0',
|
||||
version: '2.1.0',
|
||||
|
||||
'common.windowSizeId': 3,
|
||||
'common.fontSize': 16,
|
||||
@@ -14,7 +17,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
'common.isShowAnimation': true,
|
||||
'common.randomAnimate': true,
|
||||
'common.isAgreePact': false,
|
||||
'common.controlBtnPosition': process.platform === 'darwin' ? 'left' : 'right',
|
||||
'common.controlBtnPosition': isMac ? 'left' : 'right',
|
||||
'common.playBarProgressStyle': 'mini',
|
||||
'common.tryAutoUpdate': true,
|
||||
'common.showChangeLog': true,
|
||||
@@ -31,12 +34,28 @@ const defaultSetting: LX.AppSetting = {
|
||||
'player.isShowLyricTranslation': false,
|
||||
'player.isShowLyricRoma': false,
|
||||
'player.isS2t': false,
|
||||
'player.isPlayLxlrc': false,
|
||||
'player.isPlayLxlrc': isWin,
|
||||
'player.isSavePlayTime': false,
|
||||
'player.audioVisualization': false,
|
||||
'player.waitPlayEndStop': true,
|
||||
'player.waitPlayEndStopTime': '',
|
||||
'player.autoSkipOnError': true,
|
||||
'player.soundEffect.convolution.fileName': '',
|
||||
'player.soundEffect.convolution.mainGain': 10,
|
||||
'player.soundEffect.convolution.sendGain': 0,
|
||||
'player.soundEffect.biquadFilter.hz31': 0,
|
||||
'player.soundEffect.biquadFilter.hz62': 0,
|
||||
'player.soundEffect.biquadFilter.hz125': 0,
|
||||
'player.soundEffect.biquadFilter.hz250': 0,
|
||||
'player.soundEffect.biquadFilter.hz500': 0,
|
||||
'player.soundEffect.biquadFilter.hz1000': 0,
|
||||
'player.soundEffect.biquadFilter.hz2000': 0,
|
||||
'player.soundEffect.biquadFilter.hz4000': 0,
|
||||
'player.soundEffect.biquadFilter.hz8000': 0,
|
||||
'player.soundEffect.biquadFilter.hz16000': 0,
|
||||
'player.soundEffect.panner.enable': false,
|
||||
'player.soundEffect.panner.soundR': 5,
|
||||
'player.soundEffect.panner.speed': 25,
|
||||
|
||||
'playDetail.isZoomActiveLrc': false,
|
||||
'playDetail.isShowLyricProgressSetting': false,
|
||||
@@ -49,6 +68,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
'desktopLyric.isAlwaysOnTopLoop': false,
|
||||
'desktopLyric.isShowTaskbar': false,
|
||||
'desktopLyric.audioVisualization': false,
|
||||
'desktopLyric.fullscreenHide': true,
|
||||
'desktopLyric.width': 450,
|
||||
'desktopLyric.height': 300,
|
||||
'desktopLyric.x': null,
|
||||
@@ -68,10 +88,10 @@ const defaultSetting: LX.AppSetting = {
|
||||
// 'desktopLyric.style.fontWeight': false,
|
||||
'desktopLyric.style.opacity': 95,
|
||||
'desktopLyric.style.ellipsis': false,
|
||||
'desktopLyric.style.isZoomActiveLrc': true,
|
||||
'desktopLyric.style.isZoomActiveLrc': false,
|
||||
'desktopLyric.style.isFontWeightFont': true,
|
||||
'desktopLyric.style.isFontWeightLine': false,
|
||||
'desktopLyric.style.isFontWeightExtended': false,
|
||||
'desktopLyric.style.isFontWeightLine': true,
|
||||
'desktopLyric.style.isFontWeightExtended': true,
|
||||
|
||||
'list.isClickPlayList': false,
|
||||
'list.isShowSource': true,
|
||||
@@ -108,8 +128,11 @@ const defaultSetting: LX.AppSetting = {
|
||||
// 'tray.isToTray': false,
|
||||
'tray.themeId': 0,
|
||||
|
||||
'sync.mode': 'server',
|
||||
'sync.enable': false,
|
||||
'sync.port': '23332',
|
||||
'sync.server.port': '23332',
|
||||
'sync.server.maxSsnapshotNum': 5,
|
||||
'sync.client.host': '',
|
||||
|
||||
'theme.id': 'blue_plus',
|
||||
// 'theme.id': 'green',
|
||||
|
||||
@@ -90,6 +90,10 @@ const modules = {
|
||||
get_other_source_count: 'get_other_source_count',
|
||||
get_data: 'get_data',
|
||||
save_data: 'save_data',
|
||||
get_sound_effect_eq_preset: 'get_sound_effect_eq_preset',
|
||||
save_sound_effect_eq_preset: 'save_sound_effect_eq_preset',
|
||||
get_sound_effect_convolution_preset: 'get_sound_effect_convolution_preset',
|
||||
save_sound_effect_convolution_preset: 'save_sound_effect_convolution_preset',
|
||||
get_hot_key: 'get_hot_key',
|
||||
|
||||
import_user_api: 'import_user_api',
|
||||
|
||||
102
src/common/types/app_setting.d.ts
vendored
102
src/common/types/app_setting.d.ts
vendored
@@ -163,6 +163,86 @@ declare global {
|
||||
*/
|
||||
'player.waitPlayEndStopTime': string
|
||||
|
||||
/**
|
||||
* 环境音效文件名
|
||||
*/
|
||||
'player.soundEffect.convolution.fileName': string | null
|
||||
|
||||
/**
|
||||
* 环境音效原始输出增益
|
||||
*/
|
||||
'player.soundEffect.convolution.mainGain': number
|
||||
|
||||
/**
|
||||
* 环境音效输出增益
|
||||
*/
|
||||
'player.soundEffect.convolution.sendGain': number
|
||||
|
||||
/**
|
||||
* 均衡器 31hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz31': number
|
||||
|
||||
/**
|
||||
* 均衡器 62hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz62': number
|
||||
|
||||
/**
|
||||
* 均衡器 125hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz125': number
|
||||
|
||||
/**
|
||||
* 均衡器 250hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz250': number
|
||||
|
||||
/**
|
||||
* 均衡器 500hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz500': number
|
||||
|
||||
/**
|
||||
* 均衡器 1000hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz1000': number
|
||||
|
||||
/**
|
||||
* 均衡器 2000hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz2000': number
|
||||
|
||||
/**
|
||||
* 均衡器 4000hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz4000': number
|
||||
|
||||
/**
|
||||
* 均衡器 8000hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz8000': number
|
||||
|
||||
/**
|
||||
* 均衡器 16000hz 值
|
||||
*/
|
||||
'player.soundEffect.biquadFilter.hz16000': number
|
||||
|
||||
/**
|
||||
* 3D立体环绕是否启用
|
||||
*/
|
||||
'player.soundEffect.panner.enable': boolean
|
||||
|
||||
/**
|
||||
* 3D立体环绕声音距离
|
||||
*/
|
||||
'player.soundEffect.panner.soundR': number
|
||||
|
||||
/**
|
||||
* 3D立体环绕速度
|
||||
*/
|
||||
'player.soundEffect.panner.speed': number
|
||||
|
||||
/**
|
||||
* 是否启用音频加载失败时自动切歌
|
||||
*/
|
||||
@@ -219,6 +299,11 @@ declare global {
|
||||
*/
|
||||
'desktopLyric.audioVisualization': boolean
|
||||
|
||||
/**
|
||||
* 是否在全屏时隐藏歌词
|
||||
*/
|
||||
'desktopLyric.fullscreenHide': boolean
|
||||
|
||||
/**
|
||||
* 桌面歌词窗口宽度
|
||||
*/
|
||||
@@ -499,6 +584,11 @@ declare global {
|
||||
*/
|
||||
'tray.themeId': number
|
||||
|
||||
/**
|
||||
* 同步服务模式
|
||||
*/
|
||||
'sync.mode': 'server' | 'client'
|
||||
|
||||
/**
|
||||
* 是否启用同步服务
|
||||
*/
|
||||
@@ -507,7 +597,17 @@ declare global {
|
||||
/**
|
||||
* 同步服务端口号
|
||||
*/
|
||||
'sync.port': '23332' | string
|
||||
'sync.server.port': '23332' | string
|
||||
|
||||
/**
|
||||
* 最大备份快照数
|
||||
*/
|
||||
'sync.server.maxSsnapshotNum': number
|
||||
|
||||
/**
|
||||
* 同步服务地址
|
||||
*/
|
||||
'sync.client.host': string
|
||||
|
||||
/**
|
||||
* 是否在离开搜索界面时自动清空搜索框
|
||||
|
||||
6
src/common/types/list.d.ts
vendored
6
src/common/types/list.d.ts
vendored
@@ -12,13 +12,15 @@ declare namespace LX {
|
||||
|
||||
interface MyDefaultListInfo {
|
||||
id: 'default'
|
||||
name: '试听列表'
|
||||
name: 'list__name_default'
|
||||
// name: '试听列表'
|
||||
// list: LX.Music.MusicInfo[]
|
||||
}
|
||||
|
||||
interface MyLoveListInfo {
|
||||
id: 'love'
|
||||
name: '我的收藏'
|
||||
name: 'list__name_love'
|
||||
// name: '我的收藏'
|
||||
// list: LX.Music.MusicInfo[]
|
||||
}
|
||||
|
||||
|
||||
25
src/common/types/sound_effect.d.ts
vendored
Normal file
25
src/common/types/sound_effect.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
declare namespace LX {
|
||||
namespace SoundEffect {
|
||||
interface EQPreset {
|
||||
id: string
|
||||
name: string
|
||||
hz31: number
|
||||
hz62: number
|
||||
hz125: number
|
||||
hz250: number
|
||||
hz500: number
|
||||
hz1000: number
|
||||
hz2000: number
|
||||
hz4000: number
|
||||
hz8000: number
|
||||
hz16000: number
|
||||
}
|
||||
interface ConvolutionPreset {
|
||||
id: string
|
||||
name: string
|
||||
source: string
|
||||
mainGain: number
|
||||
sendGain: number
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/common/types/sync.d.ts
vendored
58
src/common/types/sync.d.ts
vendored
@@ -1,10 +1,15 @@
|
||||
declare namespace LX {
|
||||
namespace Sync {
|
||||
|
||||
interface Enable {
|
||||
interface EnableServer {
|
||||
enable: boolean
|
||||
port: string
|
||||
}
|
||||
interface EnableClient {
|
||||
enable: boolean
|
||||
host: string
|
||||
authCode?: string
|
||||
}
|
||||
|
||||
interface SyncActionBase <A> {
|
||||
action: A
|
||||
@@ -14,14 +19,17 @@ declare namespace LX {
|
||||
}
|
||||
type SyncAction<A, D = undefined> = D extends undefined ? SyncActionBase<A> : SyncActionData<A, D>
|
||||
|
||||
type SyncMainWindowActions = SyncAction<'select_mode', KeyInfo>
|
||||
type SyncMainWindowActions = SyncAction<'select_mode', string>
|
||||
| SyncAction<'close_select_mode'>
|
||||
| SyncAction<'status', Status>
|
||||
| SyncAction<'client_status', ClientStatus>
|
||||
| SyncAction<'server_status', ServerStatus>
|
||||
|
||||
type SyncServiceActions = SyncAction<'select_mode', Mode>
|
||||
| SyncAction<'get_status'>
|
||||
| SyncAction<'get_server_status'>
|
||||
| SyncAction<'get_client_status'>
|
||||
| SyncAction<'generate_code'>
|
||||
| SyncAction<'enable', Enable>
|
||||
| SyncAction<'enable_server', EnableServer>
|
||||
| SyncAction<'enable_client', EnableClient>
|
||||
|
||||
type ActionList = SyncAction<'list_data_overwrite', LX.List.ListActionDataOverwrite>
|
||||
| SyncAction<'list_create', LX.List.ListActionAdd>
|
||||
@@ -36,25 +44,55 @@ declare namespace LX {
|
||||
| SyncAction<'list_music_overwrite', LX.List.ListActionMusicOverwrite>
|
||||
| SyncAction<'list_music_clear', LX.List.ListActionMusicClear>
|
||||
|
||||
type ActionSync = SyncAction<'list:sync:list_sync_get_md5', string>
|
||||
| SyncAction<'list:sync:list_sync_get_list_data', ListData>
|
||||
| SyncAction<'list:sync:list_sync_get_sync_mode', Mode>
|
||||
| SyncAction<'list:sync:action', ActionList>
|
||||
// | SyncAction<'finished'>
|
||||
|
||||
type ActionSyncType = Actions<ActionSync>
|
||||
|
||||
type ActionSyncSend = SyncAction<'list:sync:list_sync_get_md5'>
|
||||
| SyncAction<'list:sync:list_sync_get_list_data'>
|
||||
| SyncAction<'list:sync:list_sync_get_sync_mode'>
|
||||
| SyncAction<'list:sync:list_sync_set_data', LX.Sync.ListData>
|
||||
| SyncAction<'list:sync:action', ActionList>
|
||||
| SyncAction<'list:sync:finished'>
|
||||
|
||||
type ActionSyncSendType = Actions<ActionSyncSend>
|
||||
|
||||
interface List {
|
||||
action: string
|
||||
data: any
|
||||
}
|
||||
|
||||
interface Status {
|
||||
interface ServerStatus {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
code: string
|
||||
devices: KeyInfo[]
|
||||
devices: ServerKeyInfo[]
|
||||
}
|
||||
|
||||
interface KeyInfo {
|
||||
interface ClientStatus {
|
||||
status: boolean
|
||||
message: string
|
||||
address: string[]
|
||||
}
|
||||
|
||||
interface ClientKeyInfo {
|
||||
clientId: string
|
||||
key: string
|
||||
serverName: string
|
||||
}
|
||||
|
||||
interface ServerKeyInfo {
|
||||
clientId: string
|
||||
key: string
|
||||
deviceName: string
|
||||
connectionTime?: number
|
||||
lastSyncDate?: number
|
||||
snapshotKey: string
|
||||
isMobile: boolean
|
||||
}
|
||||
|
||||
type ListData = Omit<LX.List.ListDataFull, 'tempList'>
|
||||
@@ -65,7 +103,7 @@ declare namespace LX {
|
||||
| 'overwrite_remote_local'
|
||||
| 'overwrite_local_remote_full'
|
||||
| 'overwrite_remote_local_full'
|
||||
| 'none'
|
||||
// | 'none'
|
||||
| 'cancel'
|
||||
|
||||
}
|
||||
|
||||
5
src/common/types/utils.d.ts
vendored
5
src/common/types/utils.d.ts
vendored
@@ -5,3 +5,8 @@ type DeepPartial<T> = {
|
||||
}
|
||||
|
||||
type Modify<T, R> = Omit<T, keyof R> & R
|
||||
|
||||
// type UndefinedOrNever = undefined
|
||||
type Actions<T extends { action: string, data?: any }> = {
|
||||
[U in T as U['action']]: 'data' extends keyof U ? U['data'] : undefined
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
|
||||
const timeExp = /[\d:.]+/g
|
||||
const timeLabelRxp = /^(\[[\d:]+\.)0+(\d+\])/
|
||||
const timeLabelFixRxp = /(?:\.0+|0+)$/
|
||||
const timeExp = /\d{1,3}(:\d{1,3}){0,2}(?:\.\d{1,3})/g
|
||||
const tagRegMap = {
|
||||
title: 'ti',
|
||||
artist: 'ar',
|
||||
@@ -14,6 +12,15 @@ const tagRegMap = {
|
||||
|
||||
const timeoutTools = new TimeoutTools()
|
||||
|
||||
const t_rxp_1 = /^0+(\d+)/
|
||||
const t_rxp_2 = /:0+(\d+)/g
|
||||
const t_rxp_3 = /\.0+(\d+)/
|
||||
const formatTimeLabel = (label) => {
|
||||
return label.replace(t_rxp_1, '$1')
|
||||
.replace(t_rxp_2, ':$1')
|
||||
.replace(t_rxp_3, '.$1')
|
||||
}
|
||||
|
||||
const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
const extendedLines = extendedLyric.split(/\r\n|\n|\r/)
|
||||
for (let i = 0; i < extendedLines.length; i++) {
|
||||
@@ -26,9 +33,7 @@ const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (let time of times) {
|
||||
if (time.includes('.')) time = time.replace(timeLabelRxp, '$1$2')
|
||||
else time += '.0'
|
||||
const timeStr = time.replace(timeLabelFixRxp, '')
|
||||
const timeStr = formatTimeLabel(time)
|
||||
const targetLine = lrcLinesMap[timeStr]
|
||||
if (targetLine) targetLine.extendedLyrics.push(text)
|
||||
}
|
||||
@@ -88,19 +93,16 @@ module.exports = class LinePlayer {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (let time of times) {
|
||||
if (time.includes('.')) time = time.replace(timeLabelRxp, '$1$2')
|
||||
else time += '.0'
|
||||
const timeStr = time.replace(timeLabelFixRxp, '')
|
||||
const timeStr = formatTimeLabel(time)
|
||||
if (linesMap[timeStr]) {
|
||||
linesMap[timeStr].extendedLyrics.push(text)
|
||||
continue
|
||||
}
|
||||
const timeArr = timeStr.split(':')
|
||||
if (timeArr.length < 3) timeArr.unshift(0)
|
||||
if (timeArr[2].indexOf('.') > -1) {
|
||||
timeArr.push(...timeArr[2].split('.'))
|
||||
timeArr.splice(2, 1)
|
||||
} else if (!timeArr[2]) timeArr[2] = '0'
|
||||
if (timeArr.length > 3) continue
|
||||
else if (timeArr.length < 3) for (let i = 3 - timeArr.length; i--;) timeArr.unshift('0')
|
||||
if (timeArr[2].indexOf('.') > -1) timeArr.splice(2, 1, ...timeArr[2].split('.'))
|
||||
|
||||
linesMap[timeStr] = {
|
||||
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
|
||||
text,
|
||||
|
||||
@@ -20,7 +20,7 @@ const oldThemeMap = {
|
||||
export default (setting: any): Partial<LX.AppSetting> => {
|
||||
setting = { ...setting }
|
||||
|
||||
// 迁移 v2 之前的配置
|
||||
// 迁移 v2.0.0 之前的配置
|
||||
if (compareVer(setting.version, '2.0.0') < 0) {
|
||||
// 迁移列表滚动位置设置 ~0.18.3
|
||||
if (setting.list?.scroll) {
|
||||
@@ -133,7 +133,16 @@ export default (setting: any): Partial<LX.AppSetting> => {
|
||||
|
||||
setting['odc.isAutoClearSearchInput'] = setting.odc?.isAutoClearSearchInput
|
||||
setting['odc.isAutoClearSearchList'] = setting.odc?.isAutoClearSearchList
|
||||
|
||||
setting.version = '2.0.0'
|
||||
}
|
||||
|
||||
// 迁移 v2.2.0 之前的设置数据
|
||||
if (compareVer(setting.version, '2.1.0') < 0) {
|
||||
setting['sync.erver.port'] = setting['sync.port']
|
||||
setting.version = '2.1.0'
|
||||
}
|
||||
|
||||
|
||||
return setting
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
|
||||
const meta: Record<string, any> = {
|
||||
songId: oldMusicInfo.songmid, // 歌曲ID,mg源为copyrightId,local为文件路径
|
||||
songId: oldMusicInfo.songmid, // 歌曲ID,local为文件路径
|
||||
albumName: oldMusicInfo.albumName, // 歌曲专辑名称
|
||||
picUrl: oldMusicInfo.img, // 歌曲图片链接
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"comment__hot_load_error": "Hot comments failed to load, click to try to reload",
|
||||
"comment__hot_loading": "Hot comments are loading",
|
||||
"comment__hot_title": "Hot Comment",
|
||||
"comment__location": "From{location}",
|
||||
"comment__new_load_error": "The latest comment failed to load, click to try to reload",
|
||||
"comment__new_loading": "Latest comments are loading",
|
||||
"comment__new_title": "Latest comment",
|
||||
@@ -92,6 +93,8 @@
|
||||
"list__move_to": "Move to ...",
|
||||
"list__movedown": "Movedown",
|
||||
"list__moveup": "Move up",
|
||||
"list__name_default": "Default",
|
||||
"list__name_love": "Love",
|
||||
"list__new_list_btn": "New List",
|
||||
"list__new_list_input": "New list...",
|
||||
"list__pause": "Pause Task",
|
||||
@@ -205,6 +208,7 @@
|
||||
"player__end": "Stopped",
|
||||
"player__error": "Error loading music. Switch to next song after 5 seconds",
|
||||
"player__geting_url": "Getting music link...",
|
||||
"player__geting_url_delay_retry": "The service is busy, try again in {time} seconds...",
|
||||
"player__hide_detail_tip": "Hide detail page (Right-click in the view to quickly hide the details page)",
|
||||
"player__loading": "Music loading...",
|
||||
"player__music_album": "Album: ",
|
||||
@@ -224,6 +228,41 @@
|
||||
"player__playing": "Now playing...",
|
||||
"player__prev": "Prev",
|
||||
"player__refresh_url": "Music URL expired, refreshing...",
|
||||
"player__sound_effect": "Sound settings (experimental)",
|
||||
"player__sound_effect_biquad_filter": "Equalizer",
|
||||
"player__sound_effect_biquad_filter_preset_classical": "Classical",
|
||||
"player__sound_effect_biquad_filter_preset_dance": "Dance",
|
||||
"player__sound_effect_biquad_filter_preset_electronic": "Electronic",
|
||||
"player__sound_effect_biquad_filter_preset_pop": "Pop",
|
||||
"player__sound_effect_biquad_filter_preset_rock": "Rock",
|
||||
"player__sound_effect_biquad_filter_preset_slow": "Slow",
|
||||
"player__sound_effect_biquad_filter_preset_soft": "Soft",
|
||||
"player__sound_effect_biquad_filter_preset_subwoofer": "Subwoofer",
|
||||
"player__sound_effect_biquad_filter_preset_vocal": "Vocal",
|
||||
"player__sound_effect_biquad_filter_reset_btn": "Reset",
|
||||
"player__sound_effect_biquad_filter_save_btn": "Save preset as",
|
||||
"player__sound_effect_biquad_filter_save_input": "New presets...",
|
||||
"player__sound_effect_convolution": "Ambient reverb sound effect",
|
||||
"player__sound_effect_convolution_file_bright_hall": "Hall",
|
||||
"player__sound_effect_convolution_file_cardiod_35_10_spread": "Rock",
|
||||
"player__sound_effect_convolution_file_cinema_diningroom": "Cinema",
|
||||
"player__sound_effect_convolution_file_dining_living_true_stereo": "Dining Room",
|
||||
"player__sound_effect_convolution_file_feedback_spring": "Feedback Spring",
|
||||
"player__sound_effect_convolution_file_living_bedroom_leveled": "Bathroom",
|
||||
"player__sound_effect_convolution_file_matrix_1": "Matrix",
|
||||
"player__sound_effect_convolution_file_matrix_2": "Matrix 2",
|
||||
"player__sound_effect_convolution_file_s2_r4_bd": "Church",
|
||||
"player__sound_effect_convolution_file_s3_r1_bd": "Stereo",
|
||||
"player__sound_effect_convolution_file_spreader25_125ms": "Indoor 2",
|
||||
"player__sound_effect_convolution_file_spreader50_65ms": "Indoor",
|
||||
"player__sound_effect_convolution_file_telephone": "Telephone",
|
||||
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "Rock 2",
|
||||
"player__sound_effect_convolution_main_gain": "Original Audio Gain",
|
||||
"player__sound_effect_convolution_send_gain": "Ambient Sound Effect Gain",
|
||||
"player__sound_effect_panner": "3D stereo surround (need to use headphones)",
|
||||
"player__sound_effect_panner_enabled": "enable",
|
||||
"player__sound_effect_panner_sound_r": "Sound distance",
|
||||
"player__sound_effect_panner_sound_speed": "Surround speed",
|
||||
"player__stop": "Paused",
|
||||
"player__volume": "Volume: ",
|
||||
"player__volume_mute_label": "Mute",
|
||||
@@ -310,15 +349,18 @@
|
||||
"setting__desktop_lyric_always_on_top": "Make the lyrics always above other windows",
|
||||
"setting__desktop_lyric_always_on_top_loop": "Automatically refresh the top of the lyrics (try to enable this setting when the lyrics are still blocked by some programs)",
|
||||
"setting__desktop_lyric_audio_visualization": "Audio Visualization (Experimental)",
|
||||
"setting__desktop_lyric_color": "Lyric font color",
|
||||
"setting__desktop_lyric_color_reset": "Reset color",
|
||||
"setting__desktop_lyric_delay_scroll": "Delayed lyrics scroll",
|
||||
"setting__desktop_lyric_direction_horizontal": "horizontal direction",
|
||||
"setting__desktop_lyric_direction_vertical": "vertical direction",
|
||||
"setting__desktop_lyric_direction": "Lyrics Display Direction",
|
||||
"setting__desktop_lyric_direction_horizontal": "Horizontal direction",
|
||||
"setting__desktop_lyric_direction_vertical": "Vertical direction",
|
||||
"setting__desktop_lyric_ellipsis": "Lyrics are not allowed to wrap",
|
||||
"setting__desktop_lyric_enable": "Display lyrics",
|
||||
"setting__desktop_lyric_font": "Lyric font",
|
||||
"setting__desktop_lyric_font_default": "Default",
|
||||
"setting__desktop_lyric_font_weight": "Bold font",
|
||||
"setting__desktop_lyric_fullscreen_hide": "Automatically close lyrics when full screen",
|
||||
"setting__desktop_lyric_hover_hide": "Reduce the transparency of lyrics when the mouse moves into the lyrics area (this feature has platform compatibility issues)",
|
||||
"setting__desktop_lyric_line_gap": "Lyric spacing ({num})",
|
||||
"setting__desktop_lyric_line_gap_add": "Increase spacing",
|
||||
@@ -326,7 +368,9 @@
|
||||
"setting__desktop_lyric_lock": "Lock lyrics",
|
||||
"setting__desktop_lyric_lock_screen": "It is not allowed to drag the lyrics window out of the main screen",
|
||||
"setting__desktop_lyric_played_color": "color played",
|
||||
"setting__desktop_lyric_scroll_align": "now playing lyrics scroll position",
|
||||
"setting__desktop_lyric_reset": "Reset",
|
||||
"setting__desktop_lyric_reset_window": "Reset window settings",
|
||||
"setting__desktop_lyric_scroll_align": "Now playing lyrics scroll position",
|
||||
"setting__desktop_lyric_scroll_align_center": "Center",
|
||||
"setting__desktop_lyric_scroll_align_top": "Top",
|
||||
"setting__desktop_lyric_shadow_color": "Shadow color",
|
||||
@@ -336,6 +380,7 @@
|
||||
"setting__download_data_embed": "Whether to embed the following content in the audio file",
|
||||
"setting__download_embed_lyric": "Embedding lyric",
|
||||
"setting__download_embed_pic": "Embedding cover",
|
||||
"setting__download_embed_rlyric": "Also embed Roman accent lyrics (if available)",
|
||||
"setting__download_embed_tlyric": "Also embed translated lyrics (if available)",
|
||||
"setting__download_enable": "Whether to enable download function",
|
||||
"setting__download_lyric": "Lyrics download",
|
||||
@@ -398,6 +443,9 @@
|
||||
"setting__odc_clear_search_input": "Clear the search box when you are not searching",
|
||||
"setting__odc_clear_search_list": "Clear the search list when you are not searching",
|
||||
"setting__other": "Extras",
|
||||
"setting__other_listdata": "List Data Cleanup",
|
||||
"setting__other_listdata_clear_btn": "Clear my list data",
|
||||
"setting__other_listdata_clear_tip_confirm": "This will clear all lists you have created and all songs in your favourites, do you really want to continue?",
|
||||
"setting__other_lyric_edited_cache": "Lyric management with adjusted offset time",
|
||||
"setting__other_lyric_edited_clear_btn": "Clean up time-adjusted lyrics",
|
||||
"setting__other_lyric_edited_clear_tip_confirm": "This will clear all the lyrics that you have adjusted the offset time before, confirm to clear? \n(Hand shaking to confirm 🤪)",
|
||||
@@ -438,7 +486,7 @@
|
||||
"setting__play_mediaDevice": "Audio output",
|
||||
"setting__play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
|
||||
"setting__play_mediaDevice_title": "Select a media device for audio output",
|
||||
"setting__play_media_device_error_tip": "This function conflicts with the audio visualization function. You have enabled audio visualization when you started the software this time. This setting is temporarily unavailable. Please restart the software and then modify this setting.",
|
||||
"setting__play_media_device_error_tip": "This function conflicts with advanced audio functions (audio visualization, sound effect settings). These functions have been enabled when you start the software this time. This setting is not available for now. Please close these functions and restart the software before modifying this setting.",
|
||||
"setting__play_media_device_tip": "This feature conflicts with Audio Visualization, both cannot be enabled at the same time, would you like to turn Audio Visualization off and apply the selected audio output settings?",
|
||||
"setting__play_quality": "Priority playback of 320K quality songs (if available)",
|
||||
"setting__play_save_play_time": "Remember playback progress",
|
||||
@@ -454,13 +502,24 @@
|
||||
"setting__setting__desktop_lyric_font_weight_font": "verbatim lyrics",
|
||||
"setting__setting__desktop_lyric_font_weight_line": "progressive lyrics",
|
||||
"setting__sync": "Data synchronization",
|
||||
"setting__sync_address": "Synchronization service address: {address}",
|
||||
"setting__sync_auth_code": "Connection code: {code}",
|
||||
"setting__sync_device": "Connected devices: {devices}",
|
||||
"setting__sync_enable": "Enable the synchronization function (because the data is transmitted in clear text, please use it under a trusted network)",
|
||||
"setting__sync_port": "Sync port settings",
|
||||
"setting__sync_port_tip": "Please enter the synchronization service port number",
|
||||
"setting__sync_refresh_code": "Refresh the connection code",
|
||||
"setting__sync_client_address": "Current device address: {address}",
|
||||
"setting__sync_client_host": "Synchronization service address",
|
||||
"setting__sync_client_host_tip": "http://IP:Port",
|
||||
"setting__sync_client_mode": "client mode",
|
||||
"setting__sync_client_status": "Status: {status}",
|
||||
"setting__sync_code_blocked_ip": "The IP of the current device has been blocked by the server!",
|
||||
"setting__sync_code_fail": "Invalid connection code",
|
||||
"setting__sync_enable": "Enable sync",
|
||||
"setting__sync_mode": "Synchronous mode",
|
||||
"setting__sync_mode_client": "client mode",
|
||||
"setting__sync_mode_server": "server mode",
|
||||
"setting__sync_server_address": "Synchronization service address: {address}",
|
||||
"setting__sync_server_auth_code": "Connection code: {code}",
|
||||
"setting__sync_server_device": "Connected devices: {devices}",
|
||||
"setting__sync_server_mode": "Server mode (since the data is transmitted in clear text, please use it under a trusted network)",
|
||||
"setting__sync_server_port": "Sync port settings",
|
||||
"setting__sync_server_port_tip": "Please enter the synchronization service port number",
|
||||
"setting__sync_server_refresh_code": "Refresh the connection code",
|
||||
"setting__sync_tip": "For how to use it, please see the \"Sync function\" section of the FAQ",
|
||||
"setting__update": "Update",
|
||||
"setting__update_checking": "Checking for updates...",
|
||||
@@ -475,6 +534,7 @@
|
||||
"setting__update_show_change_log": "Show changelog on first boot after version update",
|
||||
"setting__update_try_auto_update": "Attempt to download updates automatically when a new version is found",
|
||||
"setting__update_unknown": "Unknown",
|
||||
"setting_sync_status_enabled": "connected",
|
||||
"song_list": "Playlists",
|
||||
"songlist__import_input_btn_confirm": "Open",
|
||||
"songlist__import_input_show_btn": "Open Playlist",
|
||||
@@ -502,6 +562,8 @@
|
||||
"source_tx": "Tencent",
|
||||
"source_wy": "Netease",
|
||||
"source_xm": "Xiami",
|
||||
"sync__auth_code_input_tip": "Please enter the connection code",
|
||||
"sync__auth_code_title": "Need to enter the connection code",
|
||||
"sync__merge_btn_local_remote": "Local list merge remote list",
|
||||
"sync__merge_btn_remote_local": "Remote list merge local list",
|
||||
"sync__merge_label": "Merge",
|
||||
@@ -509,16 +571,17 @@
|
||||
"sync__merge_tip_desc": "Merge the two lists together, the same song will be removed (the song of the merged person is removed), and different songs will be added.",
|
||||
"sync__other_label": "Other",
|
||||
"sync__other_tip": "Other: ",
|
||||
"sync__other_tip_desc": "\"Only use real-time synchronization function\" will not modify the lists of both parties, only real-time synchronization operations; \"Cancel synchronization\" will directly disconnect the two parties.",
|
||||
"sync__other_tip_desc": "\"Cancel Sync\" will directly disconnect the two parties.",
|
||||
"sync__overwrite": "Full coverage",
|
||||
"sync__overwrite_btn_cancel": "Cancel sync",
|
||||
"sync__overwrite_btn_local_remote": "Local list Overwrite remote list",
|
||||
"sync__overwrite_btn_none": "Only use real-time synchronization",
|
||||
"sync__overwrite_btn_remote_local": "Remote list Overwrite local list",
|
||||
"sync__overwrite_label": "Cover",
|
||||
"sync__overwrite_tip": "Cover: ",
|
||||
"sync__overwrite_tip": "Over: ",
|
||||
"sync__overwrite_tip_desc": "The list with the same ID of the covered person and the covered list will be deleted and replaced with the list of the covered person (lists with different list IDs will be merged together). If you check Complete coverage, all lists of the covered person will be moved. \nDivide, and then replace with a list of overriders.",
|
||||
"sync__title": "Choose how to synchronize the list with {name}",
|
||||
"sync_status_disabled": "not connected",
|
||||
"tag__high_quality": "HQ",
|
||||
"tag__lossless": "SQ",
|
||||
"tag__lossless_24bit": "24bit",
|
||||
|
||||
@@ -72,7 +72,7 @@ const createI18n = (): I18n => {
|
||||
return message
|
||||
},
|
||||
getMessage(key: keyof Message, val?: TranslateValues): string {
|
||||
let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? ''
|
||||
let targetMessage = this.message[key] ?? this.messages[this.fallbackLocale][key] ?? key
|
||||
return val ? this.fillMessage(targetMessage, val) : targetMessage
|
||||
},
|
||||
t(key: keyof Message, val?: TranslateValues): string {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"comment__hot_load_error": "热门评论加载失败,点击尝试重新加载",
|
||||
"comment__hot_loading": "热门评论加载中",
|
||||
"comment__hot_title": "热门评论",
|
||||
"comment__location": "来自{location}",
|
||||
"comment__new_load_error": "最新评论加载失败,点击尝试重新加载",
|
||||
"comment__new_loading": "最新评论加载中",
|
||||
"comment__new_title": "最新评论",
|
||||
@@ -92,6 +93,8 @@
|
||||
"list__move_to": "移动到...",
|
||||
"list__movedown": "下移",
|
||||
"list__moveup": "上移",
|
||||
"list__name_default": "试听列表",
|
||||
"list__name_love": "我的收藏",
|
||||
"list__new_list_btn": "新建列表",
|
||||
"list__new_list_input": "新列表...",
|
||||
"list__pause": "暂停任务",
|
||||
@@ -205,6 +208,7 @@
|
||||
"player__end": "播放完毕",
|
||||
"player__error": "音频加载出错,5 秒后切换下一首",
|
||||
"player__geting_url": "歌曲链接获取中...",
|
||||
"player__geting_url_delay_retry": "服务繁忙,{time}秒后重试...",
|
||||
"player__hide_detail_tip": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
|
||||
"player__loading": "音乐加载中...",
|
||||
"player__music_album": "专辑名:",
|
||||
@@ -224,6 +228,40 @@
|
||||
"player__playing": "播放中...",
|
||||
"player__prev": "上一首",
|
||||
"player__refresh_url": "URL过期,正在刷新URL...",
|
||||
"player__sound_effect": "音效设置(实验性)",
|
||||
"player__sound_effect_biquad_filter": "均衡器",
|
||||
"player__sound_effect_biquad_filter_preset_classical": "古典",
|
||||
"player__sound_effect_biquad_filter_preset_dance": "舞曲",
|
||||
"player__sound_effect_biquad_filter_preset_electronic": "电子乐",
|
||||
"player__sound_effect_biquad_filter_preset_pop": "流行",
|
||||
"player__sound_effect_biquad_filter_preset_rock": "摇滚",
|
||||
"player__sound_effect_biquad_filter_preset_slow": "慢歌",
|
||||
"player__sound_effect_biquad_filter_preset_soft": "柔和",
|
||||
"player__sound_effect_biquad_filter_preset_subwoofer": "重低音",
|
||||
"player__sound_effect_biquad_filter_preset_vocal": "人声",
|
||||
"player__sound_effect_biquad_filter_reset_btn": "重置",
|
||||
"player__sound_effect_biquad_filter_save_btn": "另存预设",
|
||||
"player__sound_effect_biquad_filter_save_input": "新预设...",
|
||||
"player__sound_effect_convolution": "环境混响音效",
|
||||
"player__sound_effect_convolution_file_bright_hall": "大厅",
|
||||
"player__sound_effect_convolution_file_cardiod_35_10_spread": "心形扩散",
|
||||
"player__sound_effect_convolution_file_cinema_diningroom": "电影院",
|
||||
"player__sound_effect_convolution_file_dining_living_true_stereo": "餐厅",
|
||||
"player__sound_effect_convolution_file_feedback_spring": "反馈弹簧",
|
||||
"player__sound_effect_convolution_file_living_bedroom_leveled": "卫生间",
|
||||
"player__sound_effect_convolution_file_matrix_1": "矩阵混响",
|
||||
"player__sound_effect_convolution_file_matrix_2": "矩阵混响2",
|
||||
"player__sound_effect_convolution_file_s2_r4_bd": "教堂",
|
||||
"player__sound_effect_convolution_file_s3_r1_bd": "立体声",
|
||||
"player__sound_effect_convolution_file_spreader50_65ms": "室内",
|
||||
"player__sound_effect_convolution_file_telephone": "电话",
|
||||
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "磁性立体声",
|
||||
"player__sound_effect_convolution_main_gain": "原始音频增益",
|
||||
"player__sound_effect_convolution_send_gain": "环境音效增益",
|
||||
"player__sound_effect_panner": "3D立体环绕(需使用耳机)",
|
||||
"player__sound_effect_panner_enabled": "启用",
|
||||
"player__sound_effect_panner_sound_r": "声音距离",
|
||||
"player__sound_effect_panner_sound_speed": "环绕速度",
|
||||
"player__stop": "暂停播放",
|
||||
"player__volume": "当前音量:",
|
||||
"player__volume_mute_label": "静音",
|
||||
@@ -321,6 +359,7 @@
|
||||
"setting__desktop_lyric_font": "歌词字体",
|
||||
"setting__desktop_lyric_font_default": "默认",
|
||||
"setting__desktop_lyric_font_weight": "加粗字体",
|
||||
"setting__desktop_lyric_fullscreen_hide": "全屏时自动关闭歌词",
|
||||
"setting__desktop_lyric_hover_hide": "鼠标移入歌词区域时降低歌词透明度(此功能存在平台兼容性问题)",
|
||||
"setting__desktop_lyric_line_gap": "歌词间距({num})",
|
||||
"setting__desktop_lyric_line_gap_add": "加大间距",
|
||||
@@ -328,6 +367,8 @@
|
||||
"setting__desktop_lyric_lock": "锁定歌词",
|
||||
"setting__desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外",
|
||||
"setting__desktop_lyric_played_color": "已播放颜色",
|
||||
"setting__desktop_lyric_reset": "重置",
|
||||
"setting__desktop_lyric_reset_window": "重置窗口设置",
|
||||
"setting__desktop_lyric_scroll_align": "正在播放歌词滚动位置",
|
||||
"setting__desktop_lyric_scroll_align_center": "中心",
|
||||
"setting__desktop_lyric_scroll_align_top": "顶部",
|
||||
@@ -401,6 +442,9 @@
|
||||
"setting__odc_clear_search_input": "离开搜索界面时清空搜索框",
|
||||
"setting__odc_clear_search_list": "离开搜索界面时清空搜索列表",
|
||||
"setting__other": "其他",
|
||||
"setting__other_listdata": "列表数据清理",
|
||||
"setting__other_listdata_clear_btn": "清空我的列表数据",
|
||||
"setting__other_listdata_clear_tip_confirm": "这将清理你创建的 所有列表 及收藏的 所有歌曲,是否真的要继续?",
|
||||
"setting__other_lyric_edited_cache": "已调整过偏移时间的歌词管理",
|
||||
"setting__other_lyric_edited_clear_btn": "清理已调整过时间的歌词",
|
||||
"setting__other_lyric_edited_clear_tip_confirm": "这将清理所有你之前已调整过偏移时间的歌词,是否确认清理?(手抖确认🤪)",
|
||||
@@ -441,7 +485,7 @@
|
||||
"setting__play_mediaDevice": "音频输出",
|
||||
"setting__play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
|
||||
"setting__play_mediaDevice_title": "选择声音输出的媒体设备",
|
||||
"setting__play_media_device_error_tip": "此功能与音频可视化功能冲突,你本次启动软件时已启用过音频可视化,此设置暂不可用,请 重启 软件后,再来修改此设置。",
|
||||
"setting__play_media_device_error_tip": "此功能与高级音频功能(音频可视化、音效设置)冲突,你本次启动软件时已启用这些功能,此设置暂不可用,请 关闭这些功能 并 重启 软件后,再来修改此设置。",
|
||||
"setting__play_media_device_tip": "此功能与音频可视化功能冲突,两者无法同时启用,是否将音频可视化关闭 并 应用所选音频输出设置?",
|
||||
"setting__play_quality": "优先播放320K品质的歌曲(如果可用)",
|
||||
"setting__play_save_play_time": "记住播放进度",
|
||||
@@ -457,13 +501,24 @@
|
||||
"setting__setting__desktop_lyric_font_weight_font": "逐字歌词",
|
||||
"setting__setting__desktop_lyric_font_weight_line": "逐行歌词",
|
||||
"setting__sync": "数据同步",
|
||||
"setting__sync_address": "同步服务地址:{address}",
|
||||
"setting__sync_auth_code": "连接码:{code}",
|
||||
"setting__sync_device": "已连接的设备:{devices}",
|
||||
"setting__sync_enable": "启用同步功能(由于数据是明文传输,请在受信任的网络下使用)",
|
||||
"setting__sync_port": "同步端口设置",
|
||||
"setting__sync_port_tip": "请输入同步服务端口号",
|
||||
"setting__sync_refresh_code": "刷新连接码",
|
||||
"setting__sync_client_address": "当前设备地址:{address}",
|
||||
"setting__sync_client_host": "同步服务地址",
|
||||
"setting__sync_client_host_tip": "http://IP地址:端口号",
|
||||
"setting__sync_client_mode": "客户端模式",
|
||||
"setting__sync_client_status": "状态:{status}",
|
||||
"setting__sync_code_blocked_ip": "当前设备的IP已被服务端封禁!",
|
||||
"setting__sync_code_fail": "连接码无效",
|
||||
"setting__sync_enable": "启用同步功能",
|
||||
"setting__sync_mode": "同步模式",
|
||||
"setting__sync_mode_client": "客户端模式",
|
||||
"setting__sync_mode_server": "服务端模式",
|
||||
"setting__sync_server_address": "同步服务地址:{address}",
|
||||
"setting__sync_server_auth_code": "连接码:{code}",
|
||||
"setting__sync_server_device": "已连接的设备:{devices}",
|
||||
"setting__sync_server_mode": "服务端模式(由于数据是明文传输,请在受信任的网络下使用)",
|
||||
"setting__sync_server_port": "同步端口设置",
|
||||
"setting__sync_server_port_tip": "请输入同步服务端口号",
|
||||
"setting__sync_server_refresh_code": "刷新连接码",
|
||||
"setting__sync_tip": "使用方式请看常见问题“同步功能”部分",
|
||||
"setting__update": "软件更新",
|
||||
"setting__update_checking": "检查更新中...",
|
||||
@@ -478,6 +533,7 @@
|
||||
"setting__update_show_change_log": "更新版本后的首次启动时显示更新日志",
|
||||
"setting__update_try_auto_update": "发现新版本时尝试自动下载更新",
|
||||
"setting__update_unknown": "未知",
|
||||
"setting_sync_status_enabled": "已连接",
|
||||
"song_list": "歌单",
|
||||
"songlist__import_input_btn_confirm": "打开",
|
||||
"songlist__import_input_show_btn": "打开歌单",
|
||||
@@ -505,6 +561,8 @@
|
||||
"source_tx": "企鹅音乐",
|
||||
"source_wy": "网易音乐",
|
||||
"source_xm": "虾米音乐",
|
||||
"sync__auth_code_input_tip": "请输入连接码",
|
||||
"sync__auth_code_title": "需要输入连接码",
|
||||
"sync__merge_btn_local_remote": "本机列表 合并 远程列表",
|
||||
"sync__merge_btn_remote_local": "远程列表 合并 本机列表",
|
||||
"sync__merge_label": "合并",
|
||||
@@ -512,7 +570,7 @@
|
||||
"sync__merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。",
|
||||
"sync__other_label": "其他",
|
||||
"sync__other_tip": "其他:",
|
||||
"sync__other_tip_desc": "“仅使用实时同步功能”将不修改双方的列表,仅实时同步操作;“取消同步”将直接断开双方的连接。",
|
||||
"sync__other_tip_desc": "“取消同步”将直接断开双方的连接。",
|
||||
"sync__overwrite": "完全覆盖",
|
||||
"sync__overwrite_btn_cancel": "取消同步",
|
||||
"sync__overwrite_btn_local_remote": "本机列表 覆盖 远程列表",
|
||||
@@ -522,6 +580,7 @@
|
||||
"sync__overwrite_tip": "覆盖:",
|
||||
"sync__overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表(列表ID不同的列表将被合并到一起),若勾选完全覆盖,则被覆盖者的所有列表将被移除,然后替换成覆盖者的列表。",
|
||||
"sync__title": "选择与 {name} 的列表同步方式",
|
||||
"sync_status_disabled": "未连接",
|
||||
"tag__high_quality": "HQ",
|
||||
"tag__lossless": "SQ",
|
||||
"tag__lossless_24bit": "24bit",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"comment__hot_load_error": "熱門評論加載失敗,點擊嘗試重新加載",
|
||||
"comment__hot_loading": "熱門評論加載中",
|
||||
"comment__hot_title": "熱門評論",
|
||||
"comment__location": "來自{location}",
|
||||
"comment__new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
|
||||
"comment__new_loading": "最新評論加載中",
|
||||
"comment__new_title": "最新評論",
|
||||
@@ -92,6 +93,8 @@
|
||||
"list__move_to": "移動到...",
|
||||
"list__movedown": "下移",
|
||||
"list__moveup": "上移",
|
||||
"list__name_default": "試聽清單",
|
||||
"list__name_love": "我的收藏",
|
||||
"list__new_list_btn": "新建列表",
|
||||
"list__new_list_input": "新列表...",
|
||||
"list__pause": "暫停任務",
|
||||
@@ -206,6 +209,7 @@
|
||||
"player__end": "播放完畢",
|
||||
"player__error": "音頻加載出錯,5 秒後切換下一首",
|
||||
"player__geting_url": "歌曲鏈接獲取中...",
|
||||
"player__geting_url_delay_retry": "服務繁忙,{time}秒後重試...",
|
||||
"player__hide_detail_tip": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
|
||||
"player__loading": "音樂加載中...",
|
||||
"player__music_name": "歌曲名:",
|
||||
@@ -224,6 +228,41 @@
|
||||
"player__playing": "播放中...",
|
||||
"player__prev": "上一首",
|
||||
"player__refresh_url": "URL過期,正在刷新URL...",
|
||||
"player__sound_effect": "音效設置(實驗性)",
|
||||
"player__sound_effect_biquad_filter": "均衡器",
|
||||
"player__sound_effect_biquad_filter_preset_classical": "古典",
|
||||
"player__sound_effect_biquad_filter_preset_dance": "舞曲",
|
||||
"player__sound_effect_biquad_filter_preset_electronic": "電子樂",
|
||||
"player__sound_effect_biquad_filter_preset_pop": "流行",
|
||||
"player__sound_effect_biquad_filter_preset_rock": "搖滾",
|
||||
"player__sound_effect_biquad_filter_preset_slow": "慢歌",
|
||||
"player__sound_effect_biquad_filter_preset_soft": "柔和",
|
||||
"player__sound_effect_biquad_filter_preset_subwoofer": "重低音",
|
||||
"player__sound_effect_biquad_filter_preset_vocal": "人聲",
|
||||
"player__sound_effect_biquad_filter_reset_btn": "重置",
|
||||
"player__sound_effect_biquad_filter_save_btn": "另存預設",
|
||||
"player__sound_effect_biquad_filter_save_input": "新預設...",
|
||||
"player__sound_effect_convolution": "環境混響音效",
|
||||
"player__sound_effect_convolution_file_bright_hall": "大廳",
|
||||
"player__sound_effect_convolution_file_cardiod_35_10_spread": "搖滾",
|
||||
"player__sound_effect_convolution_file_cinema_diningroom": "電影院",
|
||||
"player__sound_effect_convolution_file_dining_living_true_stereo": "餐廳",
|
||||
"player__sound_effect_convolution_file_feedback_spring": "反饋彈簧",
|
||||
"player__sound_effect_convolution_file_living_bedroom_leveled": "衛生間",
|
||||
"player__sound_effect_convolution_file_matrix_1": "矩陣",
|
||||
"player__sound_effect_convolution_file_matrix_2": "矩陣2",
|
||||
"player__sound_effect_convolution_file_s2_r4_bd": "教堂",
|
||||
"player__sound_effect_convolution_file_s3_r1_bd": "立體聲",
|
||||
"player__sound_effect_convolution_file_spreader25_125ms": "室內2",
|
||||
"player__sound_effect_convolution_file_spreader50_65ms": "室內",
|
||||
"player__sound_effect_convolution_file_telephone": "電話",
|
||||
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "搖滾2",
|
||||
"player__sound_effect_convolution_main_gain": "原始音頻增益",
|
||||
"player__sound_effect_convolution_send_gain": "環境音效增益",
|
||||
"player__sound_effect_panner": "3D立體環繞(需使用耳機)",
|
||||
"player__sound_effect_panner_enabled": "啟用",
|
||||
"player__sound_effect_panner_sound_r": "聲音距離",
|
||||
"player__sound_effect_panner_sound_speed": "環繞速度",
|
||||
"player__stop": "暫停播放",
|
||||
"player__volume": "當前音量:",
|
||||
"player__volume_mute_label": "靜音",
|
||||
@@ -310,6 +349,7 @@
|
||||
"setting__desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
|
||||
"setting__desktop_lyric_always_on_top_loop": "自動刷新歌詞置頂(當歌詞置頂後仍被某些程序遮擋時可嘗試啟用此設置)",
|
||||
"setting__desktop_lyric_audio_visualization": "音頻可視化(實驗性)",
|
||||
"setting__desktop_lyric_color": "歌詞字體顏色",
|
||||
"setting__desktop_lyric_color_reset": "重置顏色",
|
||||
"setting__desktop_lyric_delay_scroll": "延遲歌詞滾動",
|
||||
"setting__desktop_lyric_direction": "歌詞顯示方向",
|
||||
@@ -320,6 +360,7 @@
|
||||
"setting__desktop_lyric_font": "歌詞字體",
|
||||
"setting__desktop_lyric_font_default": "默認",
|
||||
"setting__desktop_lyric_font_weight": "加粗字體",
|
||||
"setting__desktop_lyric_fullscreen_hide": "全屏時自動關閉歌詞",
|
||||
"setting__desktop_lyric_hover_hide": "鼠標移入歌詞區域時降低歌詞透明度(此功能存在平台兼容性問題)",
|
||||
"setting__desktop_lyric_line_gap": "歌詞間距({num})",
|
||||
"setting__desktop_lyric_line_gap_add": "加大間距",
|
||||
@@ -327,6 +368,8 @@
|
||||
"setting__desktop_lyric_lock": "鎖定歌詞",
|
||||
"setting__desktop_lyric_lock_screen": "不允許歌詞窗口拖出主屏幕之外",
|
||||
"setting__desktop_lyric_played_color": "已播放顏色",
|
||||
"setting__desktop_lyric_reset": "重置",
|
||||
"setting__desktop_lyric_reset_window": "重置窗口設置",
|
||||
"setting__desktop_lyric_scroll_align": "正在播放歌詞滾動位置",
|
||||
"setting__desktop_lyric_scroll_align_center": "中心",
|
||||
"setting__desktop_lyric_scroll_align_top": "頂部",
|
||||
@@ -337,6 +380,7 @@
|
||||
"setting__download_data_embed": "是否將以下內容嵌入到音頻文件中",
|
||||
"setting__download_embed_lyric": "歌詞嵌入",
|
||||
"setting__download_embed_pic": "封面嵌入",
|
||||
"setting__download_embed_rlyric": "同時嵌入羅馬音歌詞(如果有)",
|
||||
"setting__download_embed_tlyric": "同時嵌入翻譯歌詞(如果有)",
|
||||
"setting__download_enable": "是否啟用下載功能",
|
||||
"setting__download_lyric": "歌詞下載",
|
||||
@@ -399,6 +443,9 @@
|
||||
"setting__odc_clear_search_input": "離開搜索界面時清空搜索框",
|
||||
"setting__odc_clear_search_list": "離開搜索界面時清空搜索列表",
|
||||
"setting__other": "其他",
|
||||
"setting__other_listdata": "列表數據清理",
|
||||
"setting__other_listdata_clear_btn": "清空我的列表數據",
|
||||
"setting__other_listdata_clear_tip_confirm": "這將清理你創建的 所有列表 及收藏的 所有歌曲,是否真的要繼續?",
|
||||
"setting__other_lyric_edited_cache": "已調整過偏移時間的歌詞管理",
|
||||
"setting__other_lyric_edited_clear_btn": "清理已調整過時間的歌詞",
|
||||
"setting__other_lyric_edited_clear_tip_confirm": "這將清理所有你之前已調整過偏移時間的歌詞,是否確認清理? \n(手抖確認🤪)",
|
||||
@@ -439,7 +486,7 @@
|
||||
"setting__play_mediaDevice": "音頻輸出",
|
||||
"setting__play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
|
||||
"setting__play_mediaDevice_title": "選擇聲音輸出的媒體設備",
|
||||
"setting__play_media_device_error_tip": "此功能與音頻可視化功能衝突,你本次啟動軟件時已啟用過音頻可視化,此設置暫不可用,請 重啟 軟件後,再來修改此設置。",
|
||||
"setting__play_media_device_error_tip": "此功能與高級音頻功能(音頻可視化、音效設置)衝突,你本次啟動軟件時已啟用這些功能,此設置暫不可用,請 關閉這些功能 並 重啟 軟件後,再來修改此設置。",
|
||||
"setting__play_media_device_tip": "此功能與音頻可視化功能衝突,兩者無法同時啟用,是否將音頻可視化關閉 並 應用所選音頻輸出設置?",
|
||||
"setting__play_quality": "優先播放320K品質的歌曲(如果可用)",
|
||||
"setting__play_save_play_time": "記住播放進度",
|
||||
@@ -455,13 +502,24 @@
|
||||
"setting__setting__desktop_lyric_font_weight_font": "逐字歌詞",
|
||||
"setting__setting__desktop_lyric_font_weight_line": "逐行歌詞",
|
||||
"setting__sync": "數據同步",
|
||||
"setting__sync_address": "同步服務地址:{address}",
|
||||
"setting__sync_auth_code": "連接碼:{code}",
|
||||
"setting__sync_device": "已連接的設備:{devices}",
|
||||
"setting__sync_enable": "啟用同步功能(由於數據是明文傳輸,請在受信任的網絡下使用)",
|
||||
"setting__sync_port": "同步端口設置",
|
||||
"setting__sync_port_tip": "請輸入同步服務端口號",
|
||||
"setting__sync_refresh_code": "刷新連接碼",
|
||||
"setting__sync_client_address": "當前設備地址:{address}",
|
||||
"setting__sync_client_host": "同步服務地址",
|
||||
"setting__sync_client_host_tip": "http://IP地址:端口號",
|
||||
"setting__sync_client_mode": "客戶端模式",
|
||||
"setting__sync_client_status": "狀態:{status}",
|
||||
"setting__sync_code_blocked_ip": "當前設備的IP已被服務端封禁!",
|
||||
"setting__sync_code_fail": "連接碼無效",
|
||||
"setting__sync_enable": "啟用同步功能",
|
||||
"setting__sync_mode": "同步模式",
|
||||
"setting__sync_mode_client": "客戶端模式",
|
||||
"setting__sync_mode_server": "服務端模式",
|
||||
"setting__sync_server_address": "同步服務地址:{address}",
|
||||
"setting__sync_server_auth_code": "連接碼:{code}",
|
||||
"setting__sync_server_device": "已連接的設備:{devices}",
|
||||
"setting__sync_server_mode": "服務端模式(由於數據是明文傳輸,請在受信任的網絡下使用)",
|
||||
"setting__sync_server_port": "同步端口設置",
|
||||
"setting__sync_server_port_tip": "請輸入同步服務端口號",
|
||||
"setting__sync_server_refresh_code": "刷新連接碼",
|
||||
"setting__sync_tip": "使用方式請看常見問題“同步功能”部分",
|
||||
"setting__update": "軟件更新",
|
||||
"setting__update_checking": "檢查更新中...",
|
||||
@@ -476,6 +534,7 @@
|
||||
"setting__update_show_change_log": "更新版本後的首次啟動時顯示更新日誌",
|
||||
"setting__update_try_auto_update": "發現新版本時嘗試自動下載更新",
|
||||
"setting__update_unknown": "未知",
|
||||
"setting_sync_status_enabled": "已連接",
|
||||
"song_list": "歌單",
|
||||
"songlist__import_input_show_btn": "打開歌單",
|
||||
"songlist__import_input_tip": "輸入歌單鏈接或歌單ID",
|
||||
@@ -502,6 +561,8 @@
|
||||
"source_tx": "企鵝音樂",
|
||||
"source_wy": "網易音樂",
|
||||
"source_xm": "蝦米音樂",
|
||||
"sync__auth_code_input_tip": "請輸入連接碼",
|
||||
"sync__auth_code_title": "需要輸入連接碼",
|
||||
"sync__merge_btn_local_remote": "本機列表 合併 遠程列表",
|
||||
"sync__merge_btn_remote_local": "遠程列表 合併 本機列表",
|
||||
"sync__merge_label": "合併",
|
||||
@@ -509,7 +570,7 @@
|
||||
"sync__merge_tip_desc": "將兩邊的列表合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。",
|
||||
"sync__other_label": "其他",
|
||||
"sync__other_tip": "其他:",
|
||||
"sync__other_tip_desc": "“僅使用實時同步功能”將不修改雙方的列表,僅實時同步操作;“取消同步”將直接斷開雙方的連接。",
|
||||
"sync__other_tip_desc": "“取消同步”將直接斷開雙方的連接。",
|
||||
"sync__overwrite": "完全覆蓋",
|
||||
"sync__overwrite_btn_cancel": "取消同步",
|
||||
"sync__overwrite_btn_local_remote": "本機列表 覆蓋 遠程列表",
|
||||
@@ -519,6 +580,7 @@
|
||||
"sync__overwrite_tip": "覆蓋:",
|
||||
"sync__overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表(列表ID不同的列表將被合併到一起),若勾選完全覆蓋,則被覆蓋者的所有列表將被移除,然後替換成覆蓋者的列表。",
|
||||
"sync__title": "選擇與 {name} 的列表同步方式",
|
||||
"sync_status_disabled": "未連接",
|
||||
"tag__high_quality": "HQ",
|
||||
"tag__lossless": "SQ",
|
||||
"tag__lossless_24bit": "24bit",
|
||||
|
||||
@@ -88,6 +88,10 @@ export class Event extends EventEmitter {
|
||||
main_window_close() {
|
||||
this.emit('main_window_close')
|
||||
}
|
||||
|
||||
main_window_fullscreen(isFullscreen: boolean) {
|
||||
this.emit('main_window_fullscreen', isFullscreen)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
93
src/main/modules/sync/client/auth.ts
Normal file
93
src/main/modules/sync/client/auth.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { request, generateRsaKey } from './utils'
|
||||
import { getSyncAuthKey, setSyncAuthKey } from '../data'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
import log from '../log'
|
||||
import { aesDecrypt, aesEncrypt, getComputerName, rsaDecrypt } from '../utils'
|
||||
|
||||
|
||||
const hello = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/hello`)
|
||||
.then(({ text }) => text == SYNC_CODE.helloMsg)
|
||||
.catch((err: any) => {
|
||||
log.error('[auth] hello', err.message)
|
||||
console.log(err)
|
||||
return false
|
||||
})
|
||||
|
||||
const getServerId = async(urlInfo: LX.Sync.Client.UrlInfo) => request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/id`)
|
||||
.then(({ text }) => {
|
||||
if (!text.startsWith(SYNC_CODE.idPrefix)) return ''
|
||||
return text.replace(SYNC_CODE.idPrefix, '')
|
||||
})
|
||||
.catch((err: any) => {
|
||||
log.error('[auth] getServerId', err.message)
|
||||
console.log(err)
|
||||
throw err
|
||||
})
|
||||
|
||||
const codeAuth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode: string) => {
|
||||
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
|
||||
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||
key = Buffer.from(key).toString('base64')
|
||||
let { publicKey, privateKey } = await generateRsaKey()
|
||||
publicKey = publicKey.replace(/\n/g, '')
|
||||
.replace('-----BEGIN PUBLIC KEY-----', '')
|
||||
.replace('-----END PUBLIC KEY-----', '')
|
||||
const msg = aesEncrypt(`${SYNC_CODE.authMsg}\n${publicKey}\n${getComputerName()}\nlx_music_desktop`, key)
|
||||
// console.log(msg, key)
|
||||
return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { m: msg } }).then(async({ text, code }) => {
|
||||
// console.log(text)
|
||||
switch (text) {
|
||||
case SYNC_CODE.msgBlockedIp:
|
||||
throw new Error(SYNC_CODE.msgBlockedIp)
|
||||
case SYNC_CODE.authFailed:
|
||||
throw new Error(SYNC_CODE.authFailed)
|
||||
default:
|
||||
if (code != 200) throw new Error(SYNC_CODE.authFailed)
|
||||
}
|
||||
let msg
|
||||
try {
|
||||
msg = rsaDecrypt(Buffer.from(text, 'base64'), privateKey).toString()
|
||||
} catch (err: any) {
|
||||
log.error('[auth] codeAuth decryptMsg error', err.message)
|
||||
throw new Error(SYNC_CODE.authFailed)
|
||||
}
|
||||
// console.log(msg)
|
||||
if (!msg) return Promise.reject(new Error(SYNC_CODE.authFailed))
|
||||
const info = JSON.parse(msg) as LX.Sync.ClientKeyInfo
|
||||
void setSyncAuthKey(serverId, info)
|
||||
return info
|
||||
})
|
||||
}
|
||||
|
||||
const keyAuth = async(urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
|
||||
const msg = aesEncrypt(SYNC_CODE.authMsg + getComputerName(), keyInfo.key)
|
||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
return request(`${urlInfo.httpProtocol}//${urlInfo.hostPath}/ah`, { headers: { i: keyInfo.clientId, m: msg } }).then(({ text, code }) => {
|
||||
if (code != 200) throw new Error(SYNC_CODE.authFailed)
|
||||
|
||||
let msg
|
||||
try {
|
||||
msg = aesDecrypt(text, keyInfo.key)
|
||||
} catch (err: any) {
|
||||
log.error('[auth] keyAuth decryptMsg error', err.message)
|
||||
throw new Error(SYNC_CODE.authFailed)
|
||||
}
|
||||
if (msg != SYNC_CODE.helloMsg) return Promise.reject(new Error(SYNC_CODE.authFailed))
|
||||
})
|
||||
}
|
||||
|
||||
const auth = async(urlInfo: LX.Sync.Client.UrlInfo, serverId: string, authCode?: string) => {
|
||||
if (authCode) return codeAuth(urlInfo, serverId, authCode)
|
||||
const keyInfo = await getSyncAuthKey(serverId)
|
||||
if (!keyInfo) throw new Error(SYNC_CODE.missingAuthCode)
|
||||
await keyAuth(urlInfo, keyInfo)
|
||||
return keyInfo
|
||||
}
|
||||
|
||||
export default async(urlInfo: LX.Sync.Client.UrlInfo, authCode?: string) => {
|
||||
console.log('connect: ', urlInfo.href, authCode)
|
||||
if (!await hello(urlInfo)) throw new Error(SYNC_CODE.connectServiceFailed)
|
||||
const serverId = await getServerId(urlInfo)
|
||||
if (!serverId) throw new Error(SYNC_CODE.getServiceIdFailed)
|
||||
return auth(urlInfo, serverId, authCode)
|
||||
}
|
||||
240
src/main/modules/sync/client/client.ts
Normal file
240
src/main/modules/sync/client/client.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import WebSocket from 'ws'
|
||||
import { encryptMsg, decryptMsg } from './utils'
|
||||
import * as modules from './modules'
|
||||
// import { action as commonAction } from '@/store/modules/common'
|
||||
// import { getStore } from '@/store'
|
||||
import registerSyncListHandler from './syncList'
|
||||
import log from '../log'
|
||||
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
|
||||
import { dateFormat } from '@common/utils/common'
|
||||
import { aesEncrypt, getAddress } from '../utils'
|
||||
import { sendClientStatus } from '@main/modules/winMain'
|
||||
|
||||
let status: LX.Sync.ClientStatus = {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
}
|
||||
|
||||
export const sendSyncStatus = (newStatus: Omit<LX.Sync.ClientStatus, 'address'>) => {
|
||||
status.status = newStatus.status
|
||||
status.message = newStatus.message
|
||||
if (status.status) {
|
||||
status.address = getAddress()
|
||||
}
|
||||
sendClientStatus(status)
|
||||
}
|
||||
|
||||
export const sendSyncMessage = (message: string) => {
|
||||
status.message = message
|
||||
sendClientStatus(status)
|
||||
}
|
||||
|
||||
const handleConnection = (socket: LX.Sync.Client.Socket) => {
|
||||
for (const moduleInit of Object.values(modules)) {
|
||||
moduleInit(socket)
|
||||
}
|
||||
}
|
||||
|
||||
const heartbeatTools = {
|
||||
failedNum: 0,
|
||||
maxTryNum: 100000,
|
||||
stepMs: 3000,
|
||||
pingTimeout: null as NodeJS.Timeout | null,
|
||||
delayRetryTimeout: null as NodeJS.Timeout | null,
|
||||
handleOpen() {
|
||||
console.log('open')
|
||||
// this.failedNum = 0
|
||||
this.heartbeat()
|
||||
},
|
||||
heartbeat() {
|
||||
if (this.pingTimeout) clearTimeout(this.pingTimeout)
|
||||
|
||||
// Use `WebSocket#terminate()`, which immediately destroys the connection,
|
||||
// instead of `WebSocket#close()`, which waits for the close timer.
|
||||
// Delay should be equal to the interval at which your server
|
||||
// sends out pings plus a conservative assumption of the latency.
|
||||
this.pingTimeout = setTimeout(() => {
|
||||
client?.terminate()
|
||||
}, 30000 + 1000)
|
||||
},
|
||||
reConnnect() {
|
||||
if (this.pingTimeout) {
|
||||
clearTimeout(this.pingTimeout)
|
||||
this.pingTimeout = null
|
||||
}
|
||||
// client = null
|
||||
if (!client) return
|
||||
|
||||
if (++this.failedNum > this.maxTryNum) {
|
||||
this.failedNum = 0
|
||||
throw new Error('connect error')
|
||||
}
|
||||
|
||||
const waitTime = Math.min(2000 + Math.floor(this.failedNum / 2) * this.stepMs, 15000)
|
||||
|
||||
// sendSyncStatus({
|
||||
// status: false,
|
||||
// message: `Waiting ${waitTime / 1000}s reconnnect...`,
|
||||
// })
|
||||
|
||||
this.delayRetryTimeout = setTimeout(() => {
|
||||
this.delayRetryTimeout = null
|
||||
if (!client) return
|
||||
console.log(dateFormat(new Date()), 'reconnnect...')
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: `Try reconnnect... (${this.failedNum})`,
|
||||
})
|
||||
connect(client.data.urlInfo, client.data.keyInfo)
|
||||
}, waitTime)
|
||||
},
|
||||
clearTimeout() {
|
||||
if (this.delayRetryTimeout) {
|
||||
clearTimeout(this.delayRetryTimeout)
|
||||
this.delayRetryTimeout = null
|
||||
}
|
||||
if (this.pingTimeout) {
|
||||
clearTimeout(this.pingTimeout)
|
||||
this.pingTimeout = null
|
||||
}
|
||||
},
|
||||
connect(socket: LX.Sync.Client.Socket) {
|
||||
console.log('heartbeatTools connect')
|
||||
socket.on('open', () => {
|
||||
this.handleOpen()
|
||||
})
|
||||
socket.on('ping', () => {
|
||||
this.heartbeat()
|
||||
})
|
||||
socket.on('close', (code) => {
|
||||
console.log(code)
|
||||
switch (code) {
|
||||
case SYNC_CLOSE_CODE.normal:
|
||||
case SYNC_CLOSE_CODE.failed:
|
||||
return
|
||||
}
|
||||
this.reConnnect()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
let client: LX.Sync.Client.Socket | null
|
||||
// let listSyncPromise: Promise<void>
|
||||
export const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
|
||||
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, {
|
||||
}) as LX.Sync.Client.Socket
|
||||
client.data = {
|
||||
keyInfo,
|
||||
urlInfo,
|
||||
}
|
||||
heartbeatTools.connect(client)
|
||||
|
||||
// listSyncPromise = registerSyncListHandler(socket)
|
||||
let events: Partial<{ [K in keyof LX.Sync.ActionSyncSendType]: Array<(data: LX.Sync.ActionSyncSendType[K]) => (void | Promise<void>)> }> = {}
|
||||
let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []
|
||||
client.addEventListener('message', ({ data }) => {
|
||||
if (data == 'ping') return
|
||||
if (typeof data === 'string') {
|
||||
let syncData: LX.Sync.ActionSync
|
||||
try {
|
||||
syncData = JSON.parse(decryptMsg(keyInfo, data))
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const handlers = events[syncData.action]
|
||||
if (handlers) {
|
||||
// @ts-expect-error
|
||||
for (const handler of handlers) void handler(syncData.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
client.onRemoteEvent = function(eventName, handler) {
|
||||
let eventArr = events[eventName]
|
||||
if (!eventArr) events[eventName] = eventArr = []
|
||||
// let eventArr = events.get(eventName)
|
||||
// if (!eventArr) events.set(eventName, eventArr = [])
|
||||
eventArr.push(handler)
|
||||
|
||||
return () => {
|
||||
eventArr!.splice(eventArr!.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
client.sendData = function(eventName, data, callback) {
|
||||
client?.send(encryptMsg(keyInfo, JSON.stringify({ action: eventName, data })), callback)
|
||||
}
|
||||
client.onClose = function(handler: typeof closeEvents[number]) {
|
||||
closeEvents.push(handler)
|
||||
return () => {
|
||||
closeEvents.splice(closeEvents.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
|
||||
client.addEventListener('open', () => {
|
||||
log.info('connect')
|
||||
// const store = getStore()
|
||||
// global.lx.syncKeyInfo = keyInfo
|
||||
client!.isReady = false
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: 'Wait syncing...',
|
||||
})
|
||||
void registerSyncListHandler(client as LX.Sync.Client.Socket).then(() => {
|
||||
log.info('sync list success')
|
||||
handleConnection(client as LX.Sync.Client.Socket)
|
||||
log.info('register list sync service success')
|
||||
client!.isReady = true
|
||||
heartbeatTools.failedNum = 0
|
||||
sendSyncStatus({
|
||||
status: true,
|
||||
message: '',
|
||||
})
|
||||
}).catch(err => {
|
||||
if (err.message == 'closed') {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: '',
|
||||
})
|
||||
} else {
|
||||
console.log(err)
|
||||
log.r_error(err.stack)
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: err.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
client.addEventListener('close', ({ code }) => {
|
||||
const err = new Error('closed')
|
||||
for (const handler of closeEvents) void handler(err)
|
||||
closeEvents = []
|
||||
events = {}
|
||||
switch (code) {
|
||||
case SYNC_CLOSE_CODE.normal:
|
||||
// case SYNC_CLOSE_CODE.failed:
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
client.addEventListener('error', ({ message }) => {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const disconnect = async() => {
|
||||
if (!client) return
|
||||
log.info('disconnecting...')
|
||||
client.close(SYNC_CLOSE_CODE.normal)
|
||||
client = null
|
||||
heartbeatTools.clearTimeout()
|
||||
heartbeatTools.failedNum = 0
|
||||
}
|
||||
|
||||
export const getStatus = (): LX.Sync.ClientStatus => status
|
||||
72
src/main/modules/sync/client/index.ts
Normal file
72
src/main/modules/sync/client/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import handleAuth from './auth'
|
||||
import { connect as socketConnect, disconnect as socketDisconnect, sendSyncStatus, sendSyncMessage } from './client'
|
||||
// import { getSyncHost } from '@/utils/data'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
import log from '../log'
|
||||
import { parseUrl } from './utils'
|
||||
|
||||
let connectId = 0
|
||||
|
||||
const handleConnect = async(host: string, authCode?: string) => {
|
||||
// const hostInfo = await getSyncHost()
|
||||
// console.log(hostInfo)
|
||||
// if (!hostInfo || !hostInfo.host || !hostInfo.port) throw new Error(SYNC_CODE.unknownServiceAddress)
|
||||
const id = connectId
|
||||
const urlInfo = parseUrl(host)
|
||||
await disconnectServer(false)
|
||||
if (id != connectId) return
|
||||
const keyInfo = await handleAuth(urlInfo, authCode)
|
||||
if (id != connectId) return
|
||||
socketConnect(urlInfo, keyInfo)
|
||||
}
|
||||
const handleDisconnect = async() => {
|
||||
await socketDisconnect()
|
||||
}
|
||||
|
||||
const connectServer = async(host: string, authCode?: string) => {
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: SYNC_CODE.connecting,
|
||||
})
|
||||
const id = connectId
|
||||
return handleConnect(host, authCode).catch(async err => {
|
||||
if (id != connectId) return
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: err.message,
|
||||
})
|
||||
switch (err.message) {
|
||||
case SYNC_CODE.connectServiceFailed:
|
||||
case SYNC_CODE.missingAuthCode:
|
||||
break
|
||||
default:
|
||||
log.r_warn(err.message)
|
||||
break
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
const disconnectServer = async(isResetStatus = true) => handleDisconnect().then(() => {
|
||||
log.info('disconnect...')
|
||||
if (isResetStatus) {
|
||||
connectId++
|
||||
sendSyncStatus({
|
||||
status: false,
|
||||
message: '',
|
||||
})
|
||||
}
|
||||
}).catch((err: any) => {
|
||||
log.error(`disconnect error: ${err.message as string}`)
|
||||
sendSyncMessage(err.message)
|
||||
})
|
||||
|
||||
export {
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
}
|
||||
|
||||
export {
|
||||
getStatus,
|
||||
} from './client'
|
||||
1
src/main/modules/sync/client/modules/index.ts
Normal file
1
src/main/modules/sync/client/modules/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as list } from './list'
|
||||
7
src/main/modules/sync/client/modules/list/index.ts
Normal file
7
src/main/modules/sync/client/modules/list/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import initOn from './on'
|
||||
import initSend from './send'
|
||||
|
||||
export default (socket: LX.Sync.Client.Socket) => {
|
||||
initOn(socket)
|
||||
initSend(socket)
|
||||
}
|
||||
9
src/main/modules/sync/client/modules/list/on.ts
Normal file
9
src/main/modules/sync/client/modules/list/on.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { handleRemoteListAction } from '@main/modules/sync/utils'
|
||||
|
||||
export default (socket: LX.Sync.Client.Socket) => {
|
||||
socket.onRemoteEvent('list:sync:action', (action) => {
|
||||
if (!socket.isReady) return
|
||||
void handleRemoteListAction(action)
|
||||
})
|
||||
}
|
||||
|
||||
21
src/main/modules/sync/client/modules/list/send.ts
Normal file
21
src/main/modules/sync/client/modules/list/send.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { registerListActionEvent } from '@main/modules/sync/utils'
|
||||
|
||||
let socket: LX.Sync.Client.Socket | null
|
||||
let unregisterLocalListAction: (() => void) | null
|
||||
|
||||
|
||||
const sendListAction = (action: LX.Sync.ActionList) => {
|
||||
// console.log('sendListAction', action.action)
|
||||
if (!socket?.isReady) return
|
||||
socket.sendData('list:sync:action', action)
|
||||
}
|
||||
|
||||
export default (_socket: LX.Sync.Client.Socket) => {
|
||||
socket = _socket
|
||||
socket.onClose(() => {
|
||||
socket = null
|
||||
unregisterLocalListAction?.()
|
||||
unregisterLocalListAction = null
|
||||
})
|
||||
unregisterLocalListAction = registerListActionEvent(sendListAction)
|
||||
}
|
||||
73
src/main/modules/sync/client/syncList.ts
Normal file
73
src/main/modules/sync/client/syncList.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import log from '../log'
|
||||
import { getLocalListData, setLocalListData } from '../utils'
|
||||
import { toMD5 } from '@common/utils/nodejs'
|
||||
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||
|
||||
const logInfo = (eventName: keyof LX.Sync.ActionSyncSendType, success = false) => {
|
||||
log.info(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')}${success ? ' success' : ''}`)
|
||||
}
|
||||
const logError = (eventName: keyof LX.Sync.ActionSyncSendType, err: Error) => {
|
||||
log.error(`[${eventName as string}]${eventName.replace('list:sync:list_sync_', '').replaceAll('_', ' ')} error: ${err.message}`)
|
||||
}
|
||||
|
||||
export default async(socket: LX.Sync.Client.Socket) => new Promise<void>((resolve, reject) => {
|
||||
let listenEvents: Array<() => void> = []
|
||||
const unregisterEvents = () => {
|
||||
while (listenEvents.length) listenEvents.shift()?.()
|
||||
}
|
||||
|
||||
socket.onClose(() => {
|
||||
unregisterEvents()
|
||||
sendCloseSelectMode()
|
||||
removeSelectModeListener()
|
||||
reject(new Error('closed'))
|
||||
})
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_md5', async() => {
|
||||
logInfo('list:sync:list_sync_get_md5')
|
||||
const md5 = toMD5(JSON.stringify(await getLocalListData()))
|
||||
socket?.sendData('list:sync:list_sync_get_md5', md5, (err) => {
|
||||
if (err) {
|
||||
logError('list:sync:list_sync_get_md5', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
logInfo('list:sync:list_sync_get_md5', true)
|
||||
})
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_list_data', async() => {
|
||||
logInfo('list:sync:list_sync_get_list_data')
|
||||
socket?.sendData('list:sync:list_sync_get_list_data', await getLocalListData(), (err) => {
|
||||
if (err) {
|
||||
logError('list:sync:list_sync_get_list_data', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
logInfo('list:sync:list_sync_get_list_data', true)
|
||||
})
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_get_sync_mode', async() => {
|
||||
logInfo('list:sync:list_sync_get_sync_mode')
|
||||
sendSelectMode(socket.data.keyInfo.serverName, (mode) => {
|
||||
removeSelectModeListener()
|
||||
socket?.sendData('list:sync:list_sync_get_sync_mode', mode, (err) => {
|
||||
if (err) {
|
||||
logError('list:sync:list_sync_get_sync_mode', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
logInfo('list:sync:list_sync_get_sync_mode', true)
|
||||
})
|
||||
})
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:list_sync_set_data', async(data) => {
|
||||
logInfo('list:sync:list_sync_set_data')
|
||||
await setLocalListData(data)
|
||||
logInfo('list:sync:list_sync_set_data', true)
|
||||
}))
|
||||
listenEvents.push(socket.onRemoteEvent('list:sync:finished', async() => {
|
||||
unregisterEvents()
|
||||
resolve()
|
||||
logInfo('list:sync:finished', true)
|
||||
}))
|
||||
})
|
||||
115
src/main/modules/sync/client/utils.ts
Normal file
115
src/main/modules/sync/client/utils.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { generateKeyPair } from 'node:crypto'
|
||||
import { httpFetch, type RequestOptions } from '@main/utils/request'
|
||||
|
||||
export const request = async(url: string, options: RequestOptions = { }) => {
|
||||
return httpFetch(url, {
|
||||
...options,
|
||||
timeout: options.timeout ?? 10000,
|
||||
}).then(response => {
|
||||
return {
|
||||
text: response.body,
|
||||
code: response.statusCode,
|
||||
}
|
||||
})
|
||||
// const controller = new AbortController()
|
||||
// let id: number | null = setTimeout(() => {
|
||||
// id = null
|
||||
// controller.abort()
|
||||
// }, timeout)
|
||||
// return fetch(url, {
|
||||
// ...options,
|
||||
// signal: controller.signal,
|
||||
// // eslint-disable-next-line @typescript-eslint/promise-function-async
|
||||
// }).then(async(response) => {
|
||||
// const text = await response.text()
|
||||
// return {
|
||||
// text,
|
||||
// code: response.status,
|
||||
// }
|
||||
// }).catch(err => {
|
||||
// // console.log(err, err.code, err.message)
|
||||
// throw err
|
||||
// }).finally(() => {
|
||||
// if (id == null) return
|
||||
// clearTimeout(id)
|
||||
// })
|
||||
}
|
||||
|
||||
// export const aesEncrypt = (text: string, key: string, iv: string) => {
|
||||
// const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
|
||||
// return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')
|
||||
// }
|
||||
|
||||
// export const aesDecrypt = (text: string, key: string, iv: string) => {
|
||||
// const decipher = createDecipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
|
||||
// return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
// }
|
||||
|
||||
export const generateRsaKey = async() => new Promise<{ publicKey: string, privateKey: string }>((resolve, reject) => {
|
||||
generateKeyPair(
|
||||
'rsa',
|
||||
{
|
||||
modulusLength: 2048, // It holds a number. It is the key size in bits and is applicable for RSA, and DSA algorithm only.
|
||||
publicKeyEncoding: {
|
||||
type: 'spki', // Note the type is pkcs1 not spki
|
||||
format: 'pem',
|
||||
},
|
||||
privateKeyEncoding: {
|
||||
type: 'pkcs8', // Note again the type is set to pkcs1
|
||||
format: 'pem',
|
||||
// cipher: "aes-256-cbc", //Optional
|
||||
// passphrase: "", //Optional
|
||||
},
|
||||
},
|
||||
(err, publicKey, privateKey) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve({
|
||||
publicKey,
|
||||
privateKey,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
export const encryptMsg = (keyInfo: LX.Sync.ClientKeyInfo, msg: string): string => {
|
||||
return msg
|
||||
// if (!keyInfo) return ''
|
||||
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
|
||||
}
|
||||
|
||||
export const decryptMsg = (keyInfo: LX.Sync.ClientKeyInfo, enMsg: string): string => {
|
||||
return enMsg
|
||||
// if (!keyInfo) return ''
|
||||
// let msg = ''
|
||||
// try {
|
||||
// msg = aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
|
||||
// } catch (err) {
|
||||
// console.log(err)
|
||||
// }
|
||||
// return msg
|
||||
}
|
||||
|
||||
|
||||
export const parseUrl = (host: string): LX.Sync.Client.UrlInfo => {
|
||||
const url = new URL(host)
|
||||
let hostPath = url.host + url.pathname
|
||||
let href = url.href
|
||||
if (hostPath.endsWith('/')) hostPath = hostPath.replace(/\/$/, '')
|
||||
if (href.endsWith('/')) href = href.replace(/\/$/, '')
|
||||
|
||||
return {
|
||||
wsProtocol: url.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||
httpProtocol: url.protocol,
|
||||
hostPath,
|
||||
href,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const sendStatus = (status: LX.Sync.ClientStatus) => {
|
||||
// syncLog.log(JSON.stringify(status))
|
||||
}
|
||||
185
src/main/modules/sync/data.ts
Normal file
185
src/main/modules/sync/data.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { STORE_NAMES } from '@common/constants'
|
||||
import getStore from '@main/utils/store'
|
||||
import { throttle } from '@common/utils/common'
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import log from './log'
|
||||
|
||||
|
||||
export const getSyncAuthKey = async(serverId: string) => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
const keys = store.get('syncAuthKey') as Record<string, LX.Sync.ClientKeyInfo> | null
|
||||
if (!keys) return null
|
||||
return keys[serverId] ?? null
|
||||
}
|
||||
export const setSyncAuthKey = async(serverId: string, info: LX.Sync.ClientKeyInfo) => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
let keys: Record<string, LX.Sync.ClientKeyInfo> = (store.get('syncAuthKey') as Record<string, LX.Sync.ClientKeyInfo> | null) ?? {}
|
||||
keys[serverId] = info
|
||||
store.set('syncAuthKey', keys)
|
||||
}
|
||||
|
||||
let syncHost: string
|
||||
export const getSyncHost = async() => {
|
||||
if (syncHost === undefined) {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
syncHost = (store.get('syncHost') as typeof syncHost | null) ?? ''
|
||||
}
|
||||
return syncHost
|
||||
}
|
||||
export const setSyncHost = async(host: string) => {
|
||||
// let hostInfo = await getData(syncHostPrefix) || {}
|
||||
// hostInfo.host = host
|
||||
// hostInfo.port = port
|
||||
syncHost = host
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('syncHost', syncHost)
|
||||
}
|
||||
let syncHostHistory: string[]
|
||||
export const getSyncHostHistory = async() => {
|
||||
if (syncHostHistory === undefined) {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
syncHostHistory = (store.get('syncHostHistory') as string[]) ?? []
|
||||
}
|
||||
return syncHostHistory
|
||||
}
|
||||
export const addSyncHostHistory = async(host: string) => {
|
||||
let syncHostHistory = await getSyncHostHistory()
|
||||
if (syncHostHistory.some(h => h == host)) return
|
||||
syncHostHistory.unshift(host)
|
||||
if (syncHostHistory.length > 20) syncHostHistory = syncHostHistory.slice(0, 20) // 最多存储20个
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('syncHostHistory', syncHostHistory)
|
||||
}
|
||||
export const removeSyncHostHistory = async(index: number) => {
|
||||
syncHostHistory.splice(index, 1)
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('syncHostHistory', syncHostHistory)
|
||||
}
|
||||
|
||||
export interface SnapshotInfo {
|
||||
latest: string | null
|
||||
time: number
|
||||
list: string[]
|
||||
}
|
||||
interface DevicesInfo {
|
||||
serverId: string
|
||||
clients: Record<string, LX.Sync.ServerKeyInfo>
|
||||
snapshotInfo: SnapshotInfo
|
||||
}
|
||||
// const devicesFilePath = path.join(global.lx.dataPath, 'devices.json')
|
||||
const devicesInfo: DevicesInfo = { serverId: '', clients: {}, snapshotInfo: { latest: null, time: 0, list: [] } }
|
||||
let deviceKeys: string[] = []
|
||||
const saveDevicesInfoThrottle = throttle(() => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('clients', devicesInfo.clients)
|
||||
})
|
||||
|
||||
const initDeviceInfo = () => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
const serverId = store.get('serverId') as string | undefined
|
||||
if (serverId) devicesInfo.serverId = serverId
|
||||
else {
|
||||
devicesInfo.serverId = randomBytes(4 * 4).toString('base64')
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('serverId', devicesInfo.serverId)
|
||||
}
|
||||
const devices = store.get('clients') as DevicesInfo['clients'] | undefined
|
||||
if (devices) devicesInfo.clients = devices
|
||||
deviceKeys = Object.values(devicesInfo.clients).map(device => device.snapshotKey).filter(k => k)
|
||||
const snapshotInfo = store.get('snapshotInfo') as DevicesInfo['snapshotInfo'] | undefined
|
||||
if (snapshotInfo) devicesInfo.snapshotInfo = snapshotInfo
|
||||
}
|
||||
|
||||
export const createClientKeyInfo = (deviceName: string, isMobile: boolean): LX.Sync.ServerKeyInfo => {
|
||||
const keyInfo: LX.Sync.ServerKeyInfo = {
|
||||
clientId: randomBytes(4 * 4).toString('base64'),
|
||||
key: randomBytes(16).toString('base64'),
|
||||
deviceName,
|
||||
isMobile,
|
||||
snapshotKey: '',
|
||||
lastSyncDate: 0,
|
||||
}
|
||||
saveClientKeyInfo(keyInfo)
|
||||
return keyInfo
|
||||
}
|
||||
export const saveClientKeyInfo = (keyInfo: LX.Sync.ServerKeyInfo) => {
|
||||
if (devicesInfo.clients[keyInfo.clientId] == null && Object.keys(devicesInfo.clients).length > 101) throw new Error('max keys')
|
||||
devicesInfo.clients[keyInfo.clientId] = keyInfo
|
||||
saveDevicesInfoThrottle()
|
||||
}
|
||||
export const getClientKeyInfo = (clientId?: string): LX.Sync.ServerKeyInfo | null => {
|
||||
if (!clientId) return null
|
||||
if (!devicesInfo.serverId) initDeviceInfo()
|
||||
return devicesInfo.clients[clientId] ?? null
|
||||
}
|
||||
export const getServerId = (): string => {
|
||||
if (!devicesInfo.serverId) initDeviceInfo()
|
||||
return devicesInfo.serverId
|
||||
}
|
||||
export const isIncluedsDevice = (name: string) => {
|
||||
return deviceKeys.includes(name)
|
||||
}
|
||||
export const clearOldSnapshot = async() => {
|
||||
if (!devicesInfo.snapshotInfo) return
|
||||
const snapshotList = devicesInfo.snapshotInfo.list.filter(name => !isIncluedsDevice(name))
|
||||
let requiredSave = snapshotList.length > global.lx.appSetting['sync.server.maxSsnapshotNum']
|
||||
while (snapshotList.length > global.lx.appSetting['sync.server.maxSsnapshotNum']) {
|
||||
const name = snapshotList.pop()
|
||||
if (name) {
|
||||
await removeSnapshot(name)
|
||||
devicesInfo.snapshotInfo.list.splice(devicesInfo.snapshotInfo.list.indexOf(name), 1)
|
||||
} else break
|
||||
}
|
||||
if (requiredSave) saveSnapshotInfo(devicesInfo.snapshotInfo)
|
||||
}
|
||||
export const updateDeviceSnapshotKey = (keyInfo: LX.Sync.ServerKeyInfo, key: string) => {
|
||||
if (keyInfo.snapshotKey) deviceKeys.splice(deviceKeys.indexOf(keyInfo.snapshotKey), 1)
|
||||
keyInfo.snapshotKey = key
|
||||
keyInfo.lastSyncDate = Date.now()
|
||||
saveClientKeyInfo(keyInfo)
|
||||
deviceKeys.push(key)
|
||||
saveDevicesInfoThrottle()
|
||||
void clearOldSnapshot()
|
||||
}
|
||||
|
||||
const saveSnapshotInfoThrottle = throttle(() => {
|
||||
const store = getStore(STORE_NAMES.SYNC)
|
||||
store.set('snapshotInfo', devicesInfo.snapshotInfo)
|
||||
})
|
||||
export const getSnapshotInfo = (): SnapshotInfo => {
|
||||
return devicesInfo.snapshotInfo
|
||||
}
|
||||
export const saveSnapshotInfo = (info: SnapshotInfo) => {
|
||||
devicesInfo.snapshotInfo = info
|
||||
saveSnapshotInfoThrottle()
|
||||
}
|
||||
|
||||
export const getSnapshot = async(name: string) => {
|
||||
console.log('getSnapshot', name)
|
||||
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
|
||||
let listData: LX.Sync.ListData
|
||||
try {
|
||||
listData = JSON.parse((await fs.promises.readFile(filePath)).toString('utf-8'))
|
||||
} catch (err) {
|
||||
log.warn(err)
|
||||
return null
|
||||
}
|
||||
return listData
|
||||
}
|
||||
export const saveSnapshot = async(name: string, data: string) => {
|
||||
console.log('saveSnapshot', name)
|
||||
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
|
||||
return fs.promises.writeFile(filePath, data).catch((err) => {
|
||||
log.error(err)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
export const removeSnapshot = async(name: string) => {
|
||||
console.log('removeSnapshot', name)
|
||||
const filePath = path.join(global.lxDataPath, `snapshot_${name}`)
|
||||
return fs.promises.unlink(filePath).catch((err) => {
|
||||
log.error(err)
|
||||
})
|
||||
}
|
||||
@@ -1,21 +1,28 @@
|
||||
// import Event from './event/event'
|
||||
|
||||
import { disconnectServer } from './client'
|
||||
import { stopServer } from './server'
|
||||
|
||||
// import eventNames from './event/name'
|
||||
import * as modules from './modules'
|
||||
import { startServer, stopServer, getStatus, generateCode } from './server/server'
|
||||
|
||||
|
||||
export {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
getStatus as getServerStatus,
|
||||
generateCode,
|
||||
// Event,
|
||||
// eventNames,
|
||||
modules,
|
||||
}
|
||||
} from './server'
|
||||
|
||||
export {
|
||||
connectServer,
|
||||
disconnectServer,
|
||||
getStatus as getClientStatus,
|
||||
} from './client'
|
||||
|
||||
export default () => {
|
||||
global.lx.event_app.on('main_window_close', () => {
|
||||
void stopServer()
|
||||
if (global.lx.appSetting['sync.mode'] == 'server') {
|
||||
void stopServer()
|
||||
} else {
|
||||
void disconnectServer()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
25
src/main/modules/sync/log.ts
Normal file
25
src/main/modules/sync/log.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { log as writeLog } from '@common/utils'
|
||||
|
||||
export default {
|
||||
r_info(...params: any[]) {
|
||||
writeLog.info(...params)
|
||||
},
|
||||
r_warn(...params: any[]) {
|
||||
writeLog.warn(...params)
|
||||
},
|
||||
r_error(...params: any[]) {
|
||||
writeLog.error(...params)
|
||||
},
|
||||
info(...params: any[]) {
|
||||
// if (global.lx.isEnableSyncLog) writeLog.info(...params)
|
||||
console.log(...params)
|
||||
},
|
||||
warn(...params: any[]) {
|
||||
// if (global.lx.isEnableSyncLog) writeLog.warn(...params)
|
||||
console.warn(...params)
|
||||
},
|
||||
error(...params: any[]) {
|
||||
// if (global.lx.isEnableSyncLog) writeLog.error(...params)
|
||||
console.warn(...params)
|
||||
},
|
||||
}
|
||||
@@ -1,99 +1,120 @@
|
||||
import type http from 'http'
|
||||
import { SYNC_CODE } from './config'
|
||||
import {
|
||||
aesEncrypt,
|
||||
aesDecrypt,
|
||||
createClientKeyInfo,
|
||||
getClientKeyInfo,
|
||||
setClientKeyInfo,
|
||||
rsaEncrypt,
|
||||
} from './utils'
|
||||
import { SYNC_CODE } from '@common/constants'
|
||||
import querystring from 'node:querystring'
|
||||
import { getIP } from './utils'
|
||||
import { createClientKeyInfo, getClientKeyInfo, saveClientKeyInfo } from '../data'
|
||||
import { aesDecrypt, aesEncrypt, getComputerName, rsaEncrypt } from '../utils'
|
||||
|
||||
const requestIps = new Map<string, number>()
|
||||
|
||||
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, authCode: string) => {
|
||||
|
||||
const getAvailableIP = (req: http.IncomingMessage) => {
|
||||
let ip = getIP(req)
|
||||
return ip && (requestIps.get(ip) ?? 0) < 10 ? ip : null
|
||||
}
|
||||
|
||||
const verifyByKey = (encryptMsg: string, userId: string) => {
|
||||
const keyInfo = getClientKeyInfo(userId)
|
||||
if (!keyInfo) return null
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(encryptMsg, keyInfo.key)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
// console.log(text)
|
||||
if (text.startsWith(SYNC_CODE.authMsg)) {
|
||||
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
|
||||
if (deviceName != keyInfo.deviceName) {
|
||||
keyInfo.deviceName = deviceName
|
||||
saveClientKeyInfo(keyInfo)
|
||||
}
|
||||
return aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const verifyByCode = (encryptMsg: string, password: string) => {
|
||||
let key = ''.padStart(16, Buffer.from(password).toString('hex'))
|
||||
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||
key = Buffer.from(key).toString('base64')
|
||||
// console.log(req.headers.m, authCode, key)
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(encryptMsg, key)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
// console.log(text)
|
||||
if (text.startsWith(SYNC_CODE.authMsg)) {
|
||||
const data = text.split('\n')
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
|
||||
const deviceName = data[2] || 'Unknown'
|
||||
const isMobile = data[3] == 'lx_music_mobile'
|
||||
const keyInfo = createClientKeyInfo(deviceName, isMobile)
|
||||
return rsaEncrypt(Buffer.from(JSON.stringify({
|
||||
clientId: keyInfo.clientId,
|
||||
key: keyInfo.key,
|
||||
serverName: getComputerName(),
|
||||
})), publicKey)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const authCode = async(req: http.IncomingMessage, res: http.ServerResponse, password: string) => {
|
||||
let code = 401
|
||||
let msg: string = SYNC_CODE.msgAuthFailed
|
||||
|
||||
let ip = req.socket.remoteAddress
|
||||
// console.log(req.headers)
|
||||
if (typeof req.headers.m == 'string') {
|
||||
if (ip && (requestIps.get(ip) ?? 0) < 10) {
|
||||
if (req.headers.m) {
|
||||
label:
|
||||
if (req.headers.i) { // key验证
|
||||
if (typeof req.headers.i != 'string') break label
|
||||
const keyInfo = getClientKeyInfo(req.headers.i)
|
||||
if (!keyInfo) break label
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(req.headers.m, keyInfo.key)
|
||||
} catch (err) {
|
||||
break label
|
||||
}
|
||||
// console.log(text)
|
||||
if (text.startsWith(SYNC_CODE.authMsg)) {
|
||||
code = 200
|
||||
const deviceName = text.replace(SYNC_CODE.authMsg, '') || 'Unknown'
|
||||
if (deviceName != keyInfo.deviceName) {
|
||||
keyInfo.deviceName = deviceName
|
||||
setClientKeyInfo(keyInfo)
|
||||
}
|
||||
msg = aesEncrypt(SYNC_CODE.helloMsg, keyInfo.key)
|
||||
}
|
||||
} else { // 连接码验证
|
||||
let key = ''.padStart(16, Buffer.from(authCode).toString('hex'))
|
||||
// const iv = Buffer.from(key.split('').reverse().join('')).toString('base64')
|
||||
key = Buffer.from(key).toString('base64')
|
||||
// console.log(req.headers.m, authCode, key)
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(req.headers.m, key)
|
||||
} catch (err) {
|
||||
break label
|
||||
}
|
||||
// console.log(text)
|
||||
if (text.startsWith(SYNC_CODE.authMsg)) {
|
||||
code = 200
|
||||
const data = text.split('\n')
|
||||
const publicKey = `-----BEGIN PUBLIC KEY-----\n${data[1]}\n-----END PUBLIC KEY-----`
|
||||
const deviceName = data[2] || 'Unknown'
|
||||
msg = rsaEncrypt(Buffer.from(JSON.stringify(createClientKeyInfo(deviceName))), publicKey)
|
||||
}
|
||||
}
|
||||
let ip = getAvailableIP(req)
|
||||
if (ip) {
|
||||
if (typeof req.headers.m == 'string' && req.headers.m) {
|
||||
const userId = req.headers.i
|
||||
const _msg = typeof userId == 'string' && userId
|
||||
? verifyByKey(req.headers.m, userId)
|
||||
: verifyByCode(req.headers.m, password)
|
||||
if (_msg != null) {
|
||||
msg = _msg
|
||||
code = 200
|
||||
}
|
||||
} else {
|
||||
code = 403
|
||||
msg = SYNC_CODE.msgBlockedIp
|
||||
}
|
||||
|
||||
if (code != 200) {
|
||||
const num = requestIps.get(ip) ?? 0
|
||||
// if (num > 20) return
|
||||
requestIps.set(ip, num + 1)
|
||||
}
|
||||
} else {
|
||||
code = 403
|
||||
msg = SYNC_CODE.msgBlockedIp
|
||||
}
|
||||
|
||||
res.writeHead(code)
|
||||
res.end(msg)
|
||||
|
||||
if (ip && code != 200) {
|
||||
const num = requestIps.get(ip) ?? 0
|
||||
if (num > 20) return
|
||||
requestIps.set(ip, num + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const verifyConnection = (encryptMsg: string, userId: string) => {
|
||||
const keyInfo = getClientKeyInfo(userId)
|
||||
if (!keyInfo) return false
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(encryptMsg, keyInfo.key)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
// console.log(text)
|
||||
return text == SYNC_CODE.msgConnect
|
||||
}
|
||||
export const authConnect = async(req: http.IncomingMessage) => {
|
||||
const query = querystring.parse((req.url as string).split('?')[1])
|
||||
const i = query.i
|
||||
const t = query.t
|
||||
label:
|
||||
if (typeof i == 'string' && typeof t == 'string') {
|
||||
const keyInfo = getClientKeyInfo(i)
|
||||
if (!keyInfo) break label
|
||||
let text
|
||||
try {
|
||||
text = aesDecrypt(t, keyInfo.key)
|
||||
} catch (err) {
|
||||
break label
|
||||
}
|
||||
// console.log(text)
|
||||
if (text == SYNC_CODE.msgConnect) return
|
||||
let ip = getAvailableIP(req)
|
||||
if (ip) {
|
||||
const query = querystring.parse((req.url as string).split('?')[1])
|
||||
const i = query.i
|
||||
const t = query.t
|
||||
if (typeof i == 'string' && typeof t == 'string' && verifyConnection(t, i)) return
|
||||
|
||||
const num = requestIps.get(ip) ?? 0
|
||||
requestIps.set(ip, num + 1)
|
||||
}
|
||||
throw new Error('failed')
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
export const SYNC_CODE = {
|
||||
helloMsg: 'Hello~::^-^::~v2~',
|
||||
idPrefix: 'OjppZDo6',
|
||||
authMsg: 'lx-music auth::',
|
||||
msgAuthFailed: 'Auth failed',
|
||||
msgBlockedIp: 'Blocked IP',
|
||||
msgConnect: 'lx-music connect',
|
||||
} as const
|
||||
@@ -1,7 +1,16 @@
|
||||
import { startServer, stopServer, getStatus } from './server'
|
||||
import * as modules from './modules'
|
||||
import {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
generateCode,
|
||||
} from './server'
|
||||
|
||||
|
||||
export {
|
||||
startServer,
|
||||
stopServer,
|
||||
getStatus,
|
||||
generateCode,
|
||||
modules,
|
||||
}
|
||||
|
||||
68
src/main/modules/sync/server/modules/list.ts
Normal file
68
src/main/modules/sync/server/modules/list.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// import { throttle } from '@common/utils/common'
|
||||
// import { sendSyncActionList } from '@main/modules/winMain'
|
||||
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||
import { updateDeviceSnapshotKey } from '../../data'
|
||||
import { handleRemoteListAction, registerListActionEvent } from '../../utils'
|
||||
import { createSnapshot, encryptMsg, getCurrentListInfoKey } from '../utils'
|
||||
|
||||
let wss: LX.Sync.Server.SocketServer | null
|
||||
let removeListener: (() => void) | null
|
||||
|
||||
// type listAction = 'list:action'
|
||||
|
||||
// const addMusic = (orderId, callback) => {
|
||||
// // ...
|
||||
// }
|
||||
|
||||
const broadcast = async(key: string, data: any, excludeIds: string[] = []) => {
|
||||
if (!wss) return
|
||||
const dataStr = JSON.stringify({ action: 'list:sync:action', data })
|
||||
for (const socket of wss.clients) {
|
||||
if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue
|
||||
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
|
||||
if (err) {
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const sendListAction = async(action: LX.Sync.ActionList) => {
|
||||
console.log('sendListAction', action.action)
|
||||
// io.sockets
|
||||
await broadcast(await getCurrentListInfoKey(), action)
|
||||
}
|
||||
|
||||
export const registerListHandler = (_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
|
||||
if (!wss) {
|
||||
wss = _wss
|
||||
removeListener = registerListActionEvent(sendListAction)
|
||||
}
|
||||
|
||||
socket.onRemoteEvent('list:sync:action', (action) => {
|
||||
if (!socket.isReady) return
|
||||
// console.log(msg)
|
||||
void handleRemoteListAction(action).then(updated => {
|
||||
if (!updated) return
|
||||
void createSnapshot().then(key => {
|
||||
if (!key) return
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
void broadcast(key, action, [socket.keyInfo.clientId])
|
||||
})
|
||||
})
|
||||
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
|
||||
})
|
||||
|
||||
// socket.on('list:add', addMusic)
|
||||
}
|
||||
export const unregisterListHandler = () => {
|
||||
wss = null
|
||||
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
removeListener = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import http from 'http'
|
||||
import { Server, type Socket } from 'socket.io'
|
||||
import { createHttpTerminator, type HttpTerminator } from 'http-terminator'
|
||||
import * as modules from '../modules'
|
||||
import http, { type IncomingMessage } from 'node:http'
|
||||
import url from 'node:url'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import * as modules from './modules'
|
||||
import { authCode, authConnect } from './auth'
|
||||
import { getAddress, getServerId, generateCode as handleGenerateCode, getClientKeyInfo, setClientKeyInfo } from './utils'
|
||||
import { syncList, removeSnapshot } from './syncList'
|
||||
import { log } from '@common/utils'
|
||||
import { sendStatus } from '@main/modules/winMain'
|
||||
import { SYNC_CODE } from './config'
|
||||
import syncList from './syncList'
|
||||
import log from '../log'
|
||||
import { SYNC_CLOSE_CODE, SYNC_CODE } from '@common/constants'
|
||||
import { decryptMsg, encryptMsg, generateCode as handleGenerateCode } from './utils'
|
||||
import { getAddress } from '../utils'
|
||||
import { sendServerStatus } from '@main/modules/winMain/index'
|
||||
import { getClientKeyInfo, getServerId, saveClientKeyInfo } from '../data'
|
||||
|
||||
|
||||
let status: LX.Sync.Status = {
|
||||
let status: LX.Sync.ServerStatus = {
|
||||
status: false,
|
||||
message: '',
|
||||
address: [],
|
||||
code: '',
|
||||
devices: [],
|
||||
}
|
||||
let stopingServer = false
|
||||
|
||||
const codeTools: {
|
||||
timeout: NodeJS.Timer | null
|
||||
@@ -27,7 +30,7 @@ const codeTools: {
|
||||
start() {
|
||||
this.stop()
|
||||
this.timeout = setInterval(() => {
|
||||
void generateCode()
|
||||
void handleGenerateCode()
|
||||
}, 60 * 3 * 1000)
|
||||
},
|
||||
stop() {
|
||||
@@ -37,12 +40,54 @@ const codeTools: {
|
||||
},
|
||||
}
|
||||
|
||||
const handleConnection = (io: Server, socket: LX.Sync.Socket) => {
|
||||
console.log('connection')
|
||||
const checkDuplicateClient = (newSocket: LX.Sync.Server.Socket) => {
|
||||
for (const client of [...wss!.clients]) {
|
||||
if (client === newSocket || client.keyInfo.clientId != newSocket.keyInfo.clientId) continue
|
||||
console.log('duplicate client', client.keyInfo.deviceName)
|
||||
client.isReady = false
|
||||
client.close(SYNC_CLOSE_CODE.normal)
|
||||
}
|
||||
}
|
||||
|
||||
const handleConnection = async(socket: LX.Sync.Server.Socket, request: IncomingMessage) => {
|
||||
const queryData = url.parse(request.url as string, true).query as Record<string, string>
|
||||
|
||||
// // if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
|
||||
const keyInfo = getClientKeyInfo(queryData.i)
|
||||
if (!keyInfo) {
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
keyInfo.lastSyncDate = Date.now()
|
||||
saveClientKeyInfo(keyInfo)
|
||||
// // socket.lx_keyInfo = keyInfo
|
||||
socket.keyInfo = keyInfo
|
||||
checkDuplicateClient(socket)
|
||||
|
||||
try {
|
||||
await syncList(wss as LX.Sync.Server.SocketServer, socket)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
log.warn(err)
|
||||
return
|
||||
}
|
||||
status.devices.push(keyInfo)
|
||||
socket.onClose(() => {
|
||||
// console.log('disconnect', reason)
|
||||
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
|
||||
sendServerStatus(status)
|
||||
})
|
||||
// handleConnection(io, socket)
|
||||
sendServerStatus(status)
|
||||
|
||||
// console.log('connection', keyInfo.deviceName)
|
||||
log.info('connection', keyInfo.deviceName)
|
||||
// console.log(socket.handshake.query)
|
||||
for (const module of Object.values(modules)) {
|
||||
module.registerListHandler(io, socket)
|
||||
module.registerListHandler(wss as LX.Sync.Server.SocketServer, socket)
|
||||
}
|
||||
|
||||
socket.isReady = true
|
||||
}
|
||||
|
||||
const handleUnconnection = () => {
|
||||
@@ -64,15 +109,21 @@ const authConnection = (req: http.IncomingMessage, callback: (err: string | null
|
||||
})
|
||||
}
|
||||
|
||||
let httpTerminator: HttpTerminator | null = null
|
||||
let io: Server | null = null
|
||||
let wss: LX.Sync.Server.SocketServer | null
|
||||
let httpServer: http.Server
|
||||
|
||||
const handleStartServer = async(port = 9527) => await new Promise((resolve, reject) => {
|
||||
const httpServer = http.createServer((req, res) => {
|
||||
function noop() {}
|
||||
function onSocketError(err: Error) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
const handleStartServer = async(port = 9527, ip = '0.0.0.0') => await new Promise((resolve, reject) => {
|
||||
httpServer = http.createServer((req, res) => {
|
||||
// console.log(req.url)
|
||||
const endUrl = `/${req.url?.split('/').at(-1) ?? ''}`
|
||||
let code
|
||||
let msg
|
||||
switch (req.url) {
|
||||
switch (endUrl) {
|
||||
case '/hello':
|
||||
code = 200
|
||||
msg = SYNC_CODE.helloMsg
|
||||
@@ -93,46 +144,101 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
|
||||
res.writeHead(code)
|
||||
res.end(msg)
|
||||
})
|
||||
httpTerminator = createHttpTerminator({
|
||||
server: httpServer,
|
||||
})
|
||||
io = new Server(httpServer, {
|
||||
path: '/sync',
|
||||
serveClient: false,
|
||||
connectTimeout: 10000,
|
||||
pingTimeout: 30000,
|
||||
maxHttpBufferSize: 1e9, // 1G
|
||||
allowRequest: authConnection,
|
||||
transports: ['websocket'],
|
||||
|
||||
wss = new WebSocketServer({
|
||||
noServer: true,
|
||||
})
|
||||
|
||||
io.on('connection', async(_socket: Socket) => {
|
||||
const socket = _socket as LX.Sync.Socket
|
||||
socket.on('disconnect', reason => {
|
||||
console.log('disconnect', reason)
|
||||
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo?.clientId), 1)
|
||||
sendStatus(status)
|
||||
if (!status.devices.length) handleUnconnection()
|
||||
wss.on('connection', function(socket, request) {
|
||||
socket.isReady = false
|
||||
socket.on('pong', () => {
|
||||
socket.isAlive = true
|
||||
})
|
||||
if (typeof socket.handshake.query.i != 'string') return socket.disconnect(true)
|
||||
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
|
||||
if (!keyInfo || !io) return socket.disconnect(true)
|
||||
keyInfo.connectionTime = Date.now()
|
||||
setClientKeyInfo(keyInfo)
|
||||
// socket.lx_keyInfo = keyInfo
|
||||
socket.data.keyInfo = keyInfo
|
||||
socket.data.isReady = false
|
||||
try {
|
||||
await syncList(io, socket)
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
log.warn(err)
|
||||
return
|
||||
|
||||
// const events = new Map<keyof ActionsType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
|
||||
// const events = new Map<keyof LX.Sync.ActionSyncType, Array<(err: Error | null, data: LX.Sync.ActionSyncType[keyof LX.Sync.ActionSyncType]) => void>>()
|
||||
let events: Partial<{ [K in keyof LX.Sync.ActionSyncType]: Array<(data: LX.Sync.ActionSyncType[K]) => void> }> = {}
|
||||
let closeEvents: Array<(err: Error) => (void | Promise<void>)> = []
|
||||
socket.addEventListener('message', ({ data }) => {
|
||||
if (typeof data === 'string') {
|
||||
let syncData: LX.Sync.ActionSync
|
||||
try {
|
||||
syncData = JSON.parse(decryptMsg(socket.keyInfo, data))
|
||||
} catch (err) {
|
||||
log.error('parse message error:', err)
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
return
|
||||
}
|
||||
const handlers = events[syncData.action]
|
||||
if (handlers) {
|
||||
// @ts-expect-error
|
||||
for (const handler of handlers) handler(syncData.data)
|
||||
}
|
||||
}
|
||||
})
|
||||
socket.addEventListener('close', () => {
|
||||
const err = new Error('closed')
|
||||
for (const handler of closeEvents) void handler(err)
|
||||
events = {}
|
||||
closeEvents = []
|
||||
if (!status.devices.length) handleUnconnection()
|
||||
log.info('deconnection', socket.keyInfo.deviceName)
|
||||
})
|
||||
socket.onRemoteEvent = function(eventName, handler) {
|
||||
let eventArr = events[eventName]
|
||||
if (!eventArr) events[eventName] = eventArr = []
|
||||
// let eventArr = events.get(eventName)
|
||||
// if (!eventArr) events.set(eventName, eventArr = [])
|
||||
eventArr.push(handler)
|
||||
|
||||
return () => {
|
||||
eventArr!.splice(eventArr!.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
status.devices.push(keyInfo)
|
||||
handleConnection(io, socket)
|
||||
socket.data.isReady = true
|
||||
sendStatus(status)
|
||||
socket.onClose = function(handler: typeof closeEvents[number]) {
|
||||
closeEvents.push(handler)
|
||||
return () => {
|
||||
closeEvents.splice(closeEvents.indexOf(handler), 1)
|
||||
}
|
||||
}
|
||||
socket.sendData = function(eventName, data, callback) {
|
||||
socket.send(encryptMsg(socket.keyInfo, JSON.stringify({ action: eventName, data })), callback)
|
||||
}
|
||||
void handleConnection(socket, request)
|
||||
})
|
||||
|
||||
httpServer.on('upgrade', function upgrade(request, socket, head) {
|
||||
socket.on('error', onSocketError)
|
||||
// This function is not defined on purpose. Implement it with your own logic.
|
||||
authConnection(request, err => {
|
||||
if (err) {
|
||||
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n')
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
socket.removeListener('error', onSocketError)
|
||||
|
||||
wss?.handleUpgrade(request, socket, head, function done(ws) {
|
||||
wss?.emit('connection', ws, request)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
wss?.clients.forEach(socket => {
|
||||
if (socket.isAlive == false) {
|
||||
socket.terminate()
|
||||
return
|
||||
}
|
||||
|
||||
socket.isAlive = false
|
||||
socket.ping(noop)
|
||||
if (socket.keyInfo.isMobile) socket.send('ping', noop)
|
||||
})
|
||||
}, 30000)
|
||||
|
||||
wss.on('close', function close() {
|
||||
clearInterval(interval)
|
||||
})
|
||||
|
||||
httpServer.on('error', error => {
|
||||
@@ -142,38 +248,48 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
|
||||
|
||||
httpServer.on('listening', () => {
|
||||
const addr = httpServer.address()
|
||||
// console.log(addr)
|
||||
if (!addr) {
|
||||
reject(new Error('address is null'))
|
||||
return
|
||||
}
|
||||
const bind = typeof addr == 'string' ? `pipe ${addr}` : `port ${addr.port}`
|
||||
console.info(`Listening on ${bind}`)
|
||||
log.info(`Listening on ${ip} ${bind}`)
|
||||
resolve(null)
|
||||
})
|
||||
|
||||
httpServer.listen(port)
|
||||
httpServer.listen(port, ip)
|
||||
})
|
||||
|
||||
const handleStopServer = async() => {
|
||||
if (!httpTerminator) return
|
||||
if (!io) return
|
||||
io.close()
|
||||
await httpTerminator.terminate().catch(() => {})
|
||||
io = null
|
||||
httpTerminator = null
|
||||
}
|
||||
const handleStopServer = async() => new Promise<void>((resolve, reject) => {
|
||||
if (!wss) return
|
||||
for (const client of wss.clients) client.close(SYNC_CLOSE_CODE.normal)
|
||||
wss.close()
|
||||
wss = null
|
||||
httpServer.close((err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
export const stopServer = async() => {
|
||||
console.log('stop')
|
||||
codeTools.stop()
|
||||
if (!status.status) {
|
||||
status.status = false
|
||||
status.message = ''
|
||||
status.address = []
|
||||
status.code = ''
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
return
|
||||
}
|
||||
console.log('stoping sync server...')
|
||||
status.message = 'stoping...'
|
||||
sendServerStatus(status)
|
||||
stopingServer = true
|
||||
await handleStopServer().then(() => {
|
||||
console.log('sync server stoped')
|
||||
status.status = false
|
||||
@@ -184,14 +300,17 @@ export const stopServer = async() => {
|
||||
console.log(err)
|
||||
status.message = err.message
|
||||
}).finally(() => {
|
||||
sendStatus(status)
|
||||
stopingServer = false
|
||||
sendServerStatus(status)
|
||||
})
|
||||
}
|
||||
|
||||
export const startServer = async(port: number) => {
|
||||
console.log('status.status', status.status)
|
||||
if (stopingServer) return
|
||||
if (status.status) await handleStopServer()
|
||||
|
||||
console.log('starting sync server...')
|
||||
log.info('starting sync server')
|
||||
await handleStartServer(port).then(() => {
|
||||
console.log('sync server started')
|
||||
status.status = true
|
||||
@@ -207,18 +326,14 @@ export const startServer = async(port: number) => {
|
||||
status.address = []
|
||||
status.code = ''
|
||||
}).finally(() => {
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
})
|
||||
}
|
||||
|
||||
export const getStatus = (): LX.Sync.Status => status
|
||||
export const getStatus = (): LX.Sync.ServerStatus => status
|
||||
|
||||
export const generateCode = async() => {
|
||||
status.code = handleGenerateCode()
|
||||
sendStatus(status)
|
||||
sendServerStatus(status)
|
||||
return status.code
|
||||
}
|
||||
|
||||
export {
|
||||
removeSnapshot,
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { promises as fsPromises } from 'fs'
|
||||
import { encryptMsg, decryptMsg, getSnapshotFilePath } from './utils'
|
||||
import { throttle } from '@common/utils'
|
||||
import { type Server } from 'socket.io'
|
||||
import { sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { SYNC_CLOSE_CODE } from '@common/constants'
|
||||
import { removeSelectModeListener, sendCloseSelectMode, sendSelectMode } from '@main/modules/winMain'
|
||||
import { getSnapshot, updateDeviceSnapshotKey } from '../data'
|
||||
import { getLocalListData, setLocalListData } from '../utils'
|
||||
import { createSnapshot, encryptMsg, getCurrentListInfoKey } from './utils'
|
||||
|
||||
|
||||
const handleSetLocalListData = async(listData: LX.Sync.ListData) => {
|
||||
await setLocalListData(listData)
|
||||
return createSnapshot()
|
||||
}
|
||||
|
||||
// type ListInfoType = LX.List.UserListInfoFull | LX.List.MyDefaultListInfoFull | LX.List.MyLoveListInfoFull
|
||||
|
||||
let io: Server | null
|
||||
let wss: LX.Sync.Server.SocketServer | null
|
||||
let syncingId: string | null = null
|
||||
const wait = async(time = 1000) => await new Promise((resolve, reject) => setTimeout(resolve, time))
|
||||
|
||||
@@ -19,102 +24,93 @@ const patchListData = (listData: Partial<LX.Sync.ListData>): LX.Sync.ListData =>
|
||||
}, listData)
|
||||
}
|
||||
|
||||
const getRemoteListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData> => await new Promise((resolve, reject) => {
|
||||
const getRemoteListData = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.ListData> => await new Promise((resolve, reject) => {
|
||||
console.log('getRemoteListData')
|
||||
const handleError = (reason: string) => {
|
||||
socket.removeListener('list:sync', handleSuccess)
|
||||
socket.removeListener('disconnect', handleError)
|
||||
reject(new Error(reason))
|
||||
}
|
||||
const handleSuccess = (enData: string) => {
|
||||
socket.removeListener('disconnect', handleError)
|
||||
socket.removeListener('list:sync', handleSuccess)
|
||||
console.log('getRemoteListData', 'handleSuccess')
|
||||
const data: LX.Sync.Data | null = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
|
||||
if (!data) {
|
||||
reject(new Error('Get remote list data failed'))
|
||||
return
|
||||
}
|
||||
if (data.action != 'getData') return
|
||||
resolve(patchListData(data.data))
|
||||
}
|
||||
|
||||
socket.on('disconnect', handleError)
|
||||
socket.on('list:sync', handleSuccess)
|
||||
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'getData', data: 'all' })))
|
||||
let removeEventClose = socket.onClose(reject)
|
||||
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_list_data', (listData) => {
|
||||
resolve(patchListData(listData))
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
socket.sendData('list:sync:list_sync_get_list_data', undefined, (err) => {
|
||||
if (!err) return
|
||||
reject(err)
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
})
|
||||
|
||||
// const getAllLists = async() => {
|
||||
// const lists = []
|
||||
// lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
||||
// lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
||||
const getRemoteListMD5 = async(socket: LX.Sync.Server.Socket): Promise<string> => await new Promise((resolve, reject) => {
|
||||
let removeEventClose = socket.onClose(reject)
|
||||
let removeEvent = socket.onRemoteEvent('list:sync:list_sync_get_md5', (md5) => {
|
||||
resolve(md5)
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
socket.sendData('list:sync:list_sync_get_md5', undefined, (err) => {
|
||||
if (!err) return
|
||||
reject(err)
|
||||
removeEventClose()
|
||||
removeEvent()
|
||||
})
|
||||
})
|
||||
|
||||
// for await (const list of userLists) {
|
||||
// lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics).map(m => toOldMusicInfo(m)) })))
|
||||
// }
|
||||
|
||||
// return lists
|
||||
// }
|
||||
|
||||
const getLocalListData = async(): Promise<LX.Sync.ListData> => {
|
||||
const lists: LX.Sync.ListData = {
|
||||
defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),
|
||||
loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),
|
||||
userList: [],
|
||||
const getSyncMode = async(socket: LX.Sync.Server.Socket): Promise<LX.Sync.Mode> => new Promise((resolve, reject) => {
|
||||
const handleDisconnect = (err: Error) => {
|
||||
sendCloseSelectMode()
|
||||
removeSelectModeListener()
|
||||
reject(err)
|
||||
}
|
||||
|
||||
const userListInfos = await global.lx.worker.dbService.getAllUserList()
|
||||
for await (const list of userListInfos) {
|
||||
lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)
|
||||
.then(musics => ({ ...list, list: musics })))
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
const getSyncMode = async(keyInfo: LX.Sync.KeyInfo): Promise<LX.Sync.Mode> => await new Promise((resolve, reject) => {
|
||||
let removeListener = sendSelectMode(keyInfo, (mode) => {
|
||||
removeListener()
|
||||
sendSelectMode(socket.keyInfo.deviceName, (mode) => {
|
||||
removeSelectModeListener()
|
||||
removeEventClose()
|
||||
resolve(mode)
|
||||
})
|
||||
let removeEventClose = socket.onClose(handleDisconnect)
|
||||
})
|
||||
|
||||
const finishedSync = (socket: LX.Sync.Socket) => {
|
||||
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
|
||||
action: 'finished',
|
||||
})))
|
||||
}
|
||||
const finishedSync = async(socket: LX.Sync.Server.Socket) => new Promise<void>((resolve, reject) => {
|
||||
socket.sendData('list:sync:finished', undefined, (err) => {
|
||||
if (err) reject(err)
|
||||
else resolve()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const setLocalList = (listData: LX.Sync.ListData) => {
|
||||
void global.lx.event_list.list_data_overwrite(listData, true)
|
||||
}
|
||||
const setRemotelList = async(socket: LX.Sync.Socket, listData: LX.Sync.ListData) => {
|
||||
if (!io) return
|
||||
|
||||
const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets()
|
||||
for (const socket of sockets) {
|
||||
// if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
|
||||
socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({ action: 'setData', data: listData })))
|
||||
const sendDataPromise = async(socket: LX.Sync.Server.Socket, dataStr: string, key: string) => new Promise<void>((resolve, reject) => {
|
||||
socket.send(encryptMsg(socket.keyInfo, dataStr), (err) => {
|
||||
if (err) {
|
||||
socket.close(SYNC_CLOSE_CODE.failed)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const overwriteRemoteListData = async(listData: LX.Sync.ListData, key: string, excludeIds: string[] = []) => {
|
||||
if (!wss) return
|
||||
const dataStr = JSON.stringify({ action: 'list:sync:action', data: { action: 'list_data_overwrite', data: listData } })
|
||||
const tasks: Array<Promise<void>> = []
|
||||
for (const socket of wss.clients) {
|
||||
if (excludeIds.includes(socket.keyInfo.clientId) || !socket.isReady) continue
|
||||
tasks.push(sendDataPromise(socket, dataStr, key))
|
||||
}
|
||||
if (!tasks.length) return
|
||||
await Promise.all(tasks)
|
||||
}
|
||||
|
||||
const writeFilePromises = new Map<string, Promise<void>>()
|
||||
const updateSnapshot = async(path: string, data: string) => {
|
||||
console.log('updateSnapshot', path)
|
||||
let writeFilePromise = writeFilePromises.get(path) ?? Promise.resolve()
|
||||
writeFilePromise = writeFilePromise.then(async() => {
|
||||
if (writeFilePromise !== writeFilePromises.get(path)) return
|
||||
await fsPromises.writeFile(path, data)
|
||||
const setRemotelList = async(socket: LX.Sync.Server.Socket, listData: LX.Sync.ListData, key: string): Promise<void> => new Promise((resolve, reject) => {
|
||||
socket.sendData('list:sync:list_sync_set_data', listData, (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
resolve()
|
||||
})
|
||||
writeFilePromises.set(path, writeFilePromise)
|
||||
await writeFilePromise.finally(() => {
|
||||
if (writeFilePromise !== writeFilePromises.get(path)) return
|
||||
writeFilePromises.delete(path)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
type UserDataObj = Record<string, LX.List.UserListInfoFull>
|
||||
|
||||
const createUserListDataObj = (listData: LX.Sync.ListData): UserDataObj => {
|
||||
const userListDataObj: UserDataObj = {}
|
||||
for (const list of listData.userList) userListDataObj[list.id] = list
|
||||
@@ -220,19 +216,18 @@ const overwriteList = (sourceListData: LX.Sync.ListData, targetListData: LX.Sync
|
||||
return newListData
|
||||
}
|
||||
|
||||
const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
|
||||
let isSelectingMode = false
|
||||
const handleDisconnect = () => {
|
||||
if (!isSelectingMode) return
|
||||
sendCloseSelectMode()
|
||||
const handleMergeListData = async(socket: LX.Sync.Server.Socket): Promise<[LX.Sync.ListData, boolean, boolean]> => {
|
||||
const mode: LX.Sync.Mode = await getSyncMode(socket)
|
||||
|
||||
if (mode == 'cancel') {
|
||||
socket.close(SYNC_CLOSE_CODE.normal)
|
||||
throw new Error('cancel')
|
||||
}
|
||||
socket.on('disconnect', handleDisconnect)
|
||||
isSelectingMode = true
|
||||
const mode: LX.Sync.Mode = await getSyncMode(socket.data.keyInfo)
|
||||
isSelectingMode = false
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleMergeListData', 'remoteListData, localListData')
|
||||
let listData: LX.Sync.ListData
|
||||
let requiredUpdateLocalListData = true
|
||||
let requiredUpdateRemoteListData = true
|
||||
switch (mode) {
|
||||
case 'merge_local_remote':
|
||||
listData = mergeList(localListData, remoteListData)
|
||||
@@ -248,63 +243,50 @@ const handleMergeListData = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListD
|
||||
break
|
||||
case 'overwrite_local_remote_full':
|
||||
listData = localListData
|
||||
requiredUpdateLocalListData = false
|
||||
break
|
||||
case 'overwrite_remote_local_full':
|
||||
listData = remoteListData
|
||||
requiredUpdateRemoteListData = false
|
||||
break
|
||||
case 'none': return null
|
||||
case 'cancel':
|
||||
socket.disconnect(true)
|
||||
// case 'none': return null
|
||||
// case 'cancel':
|
||||
default:
|
||||
socket.close(SYNC_CLOSE_CODE.normal)
|
||||
throw new Error('cancel')
|
||||
}
|
||||
return listData
|
||||
return [listData, requiredUpdateLocalListData, requiredUpdateRemoteListData]
|
||||
}
|
||||
|
||||
const handleSyncList = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
|
||||
const handleSyncList = async(socket: LX.Sync.Server.Socket) => {
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
console.log('handleSyncList', 'remoteListData, localListData')
|
||||
const listData: LX.Sync.ListData = {
|
||||
defaultList: [],
|
||||
loveList: [],
|
||||
userList: [],
|
||||
}
|
||||
if (localListData.defaultList.length || localListData.loveList.length || localListData.userList.length) {
|
||||
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
|
||||
const mergedList = await handleMergeListData(socket)
|
||||
const [mergedList, requiredUpdateLocalListData, requiredUpdateRemoteListData] = await handleMergeListData(socket)
|
||||
console.log('handleMergeListData', 'mergedList')
|
||||
// console.log(mergedList)
|
||||
if (!mergedList) return null
|
||||
listData.defaultList = mergedList.defaultList
|
||||
listData.loveList = mergedList.loveList
|
||||
listData.userList = mergedList.userList
|
||||
setLocalList(mergedList)
|
||||
void setRemotelList(socket, mergedList)
|
||||
let key
|
||||
if (requiredUpdateLocalListData) {
|
||||
key = await handleSetLocalListData(mergedList)
|
||||
await overwriteRemoteListData(mergedList, key, [socket.keyInfo.clientId])
|
||||
if (!requiredUpdateRemoteListData) updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
}
|
||||
if (requiredUpdateRemoteListData) {
|
||||
if (!key) key = await getCurrentListInfoKey()
|
||||
await setRemotelList(socket, mergedList, key)
|
||||
}
|
||||
} else {
|
||||
void setRemotelList(socket, localListData)
|
||||
listData.defaultList = localListData.defaultList
|
||||
listData.loveList = localListData.loveList
|
||||
listData.userList = localListData.userList
|
||||
await setRemotelList(socket, localListData, await getCurrentListInfoKey())
|
||||
}
|
||||
} else {
|
||||
let key: string
|
||||
if (remoteListData.defaultList.length || remoteListData.loveList.length || remoteListData.userList.length) {
|
||||
setLocalList(remoteListData)
|
||||
listData.defaultList = remoteListData.defaultList
|
||||
listData.loveList = remoteListData.loveList
|
||||
listData.userList = remoteListData.userList
|
||||
} else {
|
||||
listData.defaultList = localListData.defaultList
|
||||
listData.loveList = localListData.loveList
|
||||
listData.userList = localListData.userList
|
||||
key = await handleSetLocalListData(remoteListData)
|
||||
await overwriteRemoteListData(remoteListData, key, [socket.keyInfo.clientId])
|
||||
}
|
||||
key ??= await getCurrentListInfoKey()
|
||||
updateDeviceSnapshotKey(socket.keyInfo, key)
|
||||
}
|
||||
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
||||
defaultList: listData.defaultList,
|
||||
loveList: listData.loveList,
|
||||
userList: listData.userList,
|
||||
})).then(() => {
|
||||
socket.data.isCreatedSnapshot = true
|
||||
return listData
|
||||
})
|
||||
}
|
||||
|
||||
const mergeListDataFromSnapshot = (
|
||||
@@ -349,7 +331,16 @@ const mergeListDataFromSnapshot = (
|
||||
}
|
||||
return ids.map(id => map.get(id)) as LX.Music.MusicInfo[]
|
||||
}
|
||||
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot: LX.Sync.ListData): Promise<LX.Sync.ListData> => {
|
||||
const checkListLatest = async(socket: LX.Sync.Server.Socket) => {
|
||||
const remoteListMD5 = await getRemoteListMD5(socket)
|
||||
const currentListInfoKey = await getCurrentListInfoKey()
|
||||
const latest = remoteListMD5 == currentListInfoKey
|
||||
if (latest && socket.keyInfo.snapshotKey != currentListInfoKey) updateDeviceSnapshotKey(socket.keyInfo, currentListInfoKey)
|
||||
return latest
|
||||
}
|
||||
const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Server.Socket, snapshot: LX.Sync.ListData) => {
|
||||
if (await checkListLatest(socket)) return
|
||||
|
||||
const addMusicLocationType = global.lx.appSetting['list.addMusicLocationType']
|
||||
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
|
||||
const newListData: LX.Sync.ListData = {
|
||||
@@ -406,92 +397,55 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot:
|
||||
})
|
||||
|
||||
newListData.userList = newUserList
|
||||
setLocalList(newListData)
|
||||
void setRemotelList(socket, newListData)
|
||||
return await updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
|
||||
defaultList: newListData.defaultList,
|
||||
loveList: newListData.loveList,
|
||||
userList: newListData.userList,
|
||||
})).then(() => {
|
||||
socket.data.isCreatedSnapshot = true
|
||||
return newListData
|
||||
})
|
||||
const key = await handleSetLocalListData(newListData)
|
||||
await setRemotelList(socket, newListData, key)
|
||||
await overwriteRemoteListData(newListData, key, [socket.keyInfo.clientId])
|
||||
}
|
||||
|
||||
const registerUpdateSnapshotTask = (socket: LX.Sync.Socket, snapshot: LX.Sync.ListData) => {
|
||||
if (!socket.data.isCreatedSnapshot) return
|
||||
const handleUpdateSnapshot = throttle(() => {
|
||||
// TODO: 同步成功后再保存快照
|
||||
void getLocalListData().then(({ defaultList, loveList, userList }) => {
|
||||
if (defaultList != null) snapshot.defaultList = defaultList
|
||||
if (loveList != null) snapshot.loveList = loveList
|
||||
if (userList != null) snapshot.userList = userList
|
||||
void updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
|
||||
})
|
||||
}, 2000)
|
||||
global.lx.event_list.on('list_changed', handleUpdateSnapshot)
|
||||
socket.on('disconnect', () => {
|
||||
global.lx.event_list.off('list_changed', handleUpdateSnapshot)
|
||||
})
|
||||
}
|
||||
|
||||
const syncList = async(socket: LX.Sync.Socket): Promise<LX.Sync.ListData | null> => {
|
||||
socket.data.snapshotFilePath = getSnapshotFilePath(socket.data.keyInfo)
|
||||
let fileData: any
|
||||
let isSyncRequired = false
|
||||
try {
|
||||
fileData = (await fsPromises.readFile(socket.data.snapshotFilePath)).toString()
|
||||
fileData = JSON.parse(fileData)
|
||||
} catch (error) {
|
||||
const err = error as NodeJS.ErrnoException
|
||||
if (err.code != 'ENOENT') throw err
|
||||
isSyncRequired = true
|
||||
const syncList = async(socket: LX.Sync.Server.Socket) => {
|
||||
// socket.data.snapshotFilePath = getSnapshotFilePath(socket.keyInfo)
|
||||
if (socket.keyInfo.snapshotKey) {
|
||||
const listData = await getSnapshot(socket.keyInfo.snapshotKey)
|
||||
if (listData) {
|
||||
await handleMergeListDataFromSnapshot(socket, listData)
|
||||
return
|
||||
}
|
||||
}
|
||||
console.log('isSyncRequired', isSyncRequired)
|
||||
if (isSyncRequired) return await handleSyncList(socket)
|
||||
return await handleMergeListDataFromSnapshot(socket, patchListData(fileData))
|
||||
await handleSyncList(socket)
|
||||
}
|
||||
|
||||
const checkSyncQueue = async(): Promise<void> => {
|
||||
if (!syncingId) return
|
||||
console.log('sync queue...')
|
||||
await wait()
|
||||
await checkSyncQueue()
|
||||
}
|
||||
|
||||
// export {
|
||||
// syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
||||
// io = _io
|
||||
// await checkSyncQueue()
|
||||
// syncingId = socket.data.keyInfo.clientId
|
||||
// return await syncList(socket).then(newListData => {
|
||||
// registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
// return finishedSync(socket)
|
||||
// }).finally(() => {
|
||||
// syncingId = null
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
export default async(_wss: LX.Sync.Server.SocketServer, socket: LX.Sync.Server.Socket) => {
|
||||
if (!wss) {
|
||||
wss = _wss
|
||||
_wss.addListener('close', () => {
|
||||
wss = null
|
||||
})
|
||||
}
|
||||
|
||||
const _syncList = async(_io: Server, socket: LX.Sync.Socket) => {
|
||||
io = _io
|
||||
await checkSyncQueue()
|
||||
syncingId = socket.data.keyInfo.clientId
|
||||
return await syncList(socket).then(newListData => {
|
||||
if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
let disconnected = false
|
||||
socket.onClose(() => {
|
||||
disconnected = true
|
||||
if (syncingId == socket.keyInfo.clientId) syncingId = null
|
||||
})
|
||||
|
||||
while (true) {
|
||||
if (disconnected) throw new Error('disconnected')
|
||||
if (!syncingId) break
|
||||
await wait()
|
||||
}
|
||||
|
||||
syncingId = socket.keyInfo.clientId
|
||||
await syncList(socket).then(async() => {
|
||||
// if (newListData) registerUpdateSnapshotTask(socket, { ...newListData })
|
||||
return finishedSync(socket)
|
||||
}).finally(() => {
|
||||
syncingId = null
|
||||
})
|
||||
}
|
||||
|
||||
const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
|
||||
const filePath = getSnapshotFilePath(keyInfo)
|
||||
await fsPromises.unlink(filePath)
|
||||
}
|
||||
|
||||
export {
|
||||
_syncList as syncList,
|
||||
removeSnapshot,
|
||||
}
|
||||
|
||||
// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
|
||||
// const filePath = getSnapshotFilePath(keyInfo)
|
||||
// await fsPromises.unlink(filePath)
|
||||
// }
|
||||
|
||||
@@ -1,97 +1,28 @@
|
||||
import { networkInterfaces } from 'os'
|
||||
import { randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'crypto'
|
||||
import { join } from 'path'
|
||||
import getStore from '@main/utils/store'
|
||||
|
||||
const STORE_NAME = 'sync'
|
||||
|
||||
type KeyInfos = Record<string, LX.Sync.KeyInfo>
|
||||
|
||||
export const getAddress = (): string[] => {
|
||||
const nets = networkInterfaces()
|
||||
const results: string[] = []
|
||||
// console.log(nets)
|
||||
|
||||
for (const interfaceInfos of Object.values(nets)) {
|
||||
if (!interfaceInfos) continue
|
||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||
for (const interfaceInfo of interfaceInfos) {
|
||||
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
|
||||
results.push(interfaceInfo.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
let serverId: string | undefined
|
||||
export const getServerId = (): string => {
|
||||
if (serverId) return serverId
|
||||
const store = getStore(STORE_NAME)
|
||||
serverId = store.get('serverId') as string | undefined
|
||||
if (!serverId) {
|
||||
serverId = randomBytes(4 * 4).toString('base64')
|
||||
store.set('serverId', serverId)
|
||||
}
|
||||
return serverId
|
||||
}
|
||||
|
||||
let keyInfos: KeyInfos
|
||||
export const createClientKeyInfo = (deviceName: string): LX.Sync.KeyInfo => {
|
||||
const keyInfo: LX.Sync.KeyInfo = {
|
||||
clientId: randomBytes(4 * 4).toString('base64'),
|
||||
key: randomBytes(16).toString('base64'),
|
||||
deviceName,
|
||||
}
|
||||
const store = getStore(STORE_NAME)
|
||||
keyInfos ??= store.get('keys') as KeyInfos || {}
|
||||
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
|
||||
|
||||
keyInfos[keyInfo.clientId] = keyInfo
|
||||
store.set('keys', keyInfos)
|
||||
return keyInfo
|
||||
}
|
||||
export const setClientKeyInfo = (keyInfo: LX.Sync.KeyInfo) => {
|
||||
keyInfos[keyInfo.clientId] = keyInfo
|
||||
const store = getStore(STORE_NAME)
|
||||
store.set('keys', keyInfos)
|
||||
}
|
||||
export const getClientKeyInfo = (clientId: string): LX.Sync.KeyInfo | null => {
|
||||
if (!keyInfos) {
|
||||
const store = getStore(STORE_NAME)
|
||||
keyInfos = store.get('keys') as KeyInfos || {}
|
||||
}
|
||||
return keyInfos[clientId] ?? null
|
||||
}
|
||||
import { toMD5 } from '@common/utils/nodejs'
|
||||
import type http from 'node:http'
|
||||
import {
|
||||
getSnapshotInfo,
|
||||
saveSnapshot,
|
||||
saveSnapshotInfo,
|
||||
type SnapshotInfo,
|
||||
} from '../data'
|
||||
import { getLocalListData } from '../utils'
|
||||
|
||||
export const generateCode = (): string => {
|
||||
return Math.random().toString().substring(2, 8)
|
||||
}
|
||||
|
||||
export const aesEncrypt = (buffer: string | Buffer, key: string): string => {
|
||||
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
|
||||
export const getIP = (request: http.IncomingMessage) => {
|
||||
return request.socket.remoteAddress
|
||||
}
|
||||
|
||||
export const aesDecrypt = (text: string, key: string): string => {
|
||||
const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
}
|
||||
|
||||
export const rsaEncrypt = (buffer: Buffer, key: string): string => {
|
||||
return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')
|
||||
}
|
||||
export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {
|
||||
return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)
|
||||
}
|
||||
|
||||
export const encryptMsg = (keyInfo: LX.Sync.KeyInfo, msg: string): string => {
|
||||
export const encryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, msg: string): string => {
|
||||
return msg
|
||||
// if (!keyInfo) return ''
|
||||
// return aesEncrypt(msg, keyInfo.key, keyInfo.iv)
|
||||
}
|
||||
|
||||
export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
|
||||
export const decryptMsg = (keyInfo: LX.Sync.ServerKeyInfo, enMsg: string): string => {
|
||||
return enMsg
|
||||
// if (!keyInfo) return ''
|
||||
// let msg = ''
|
||||
@@ -103,6 +34,24 @@ export const decryptMsg = (keyInfo: LX.Sync.KeyInfo, enMsg: string): string => {
|
||||
// return msg
|
||||
}
|
||||
|
||||
export const getSnapshotFilePath = (keyInfo: LX.Sync.KeyInfo): string => {
|
||||
return join(global.lxDataPath, `snapshot-${Buffer.from(keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
|
||||
let snapshotInfo: SnapshotInfo
|
||||
export const createSnapshot = async() => {
|
||||
if (!snapshotInfo) snapshotInfo = getSnapshotInfo()
|
||||
const listData = JSON.stringify(await getLocalListData())
|
||||
const md5 = toMD5(listData)
|
||||
if (snapshotInfo.latest == md5) return md5
|
||||
if (snapshotInfo.list.includes(md5)) {
|
||||
snapshotInfo.list.splice(snapshotInfo.list.indexOf(md5), 1)
|
||||
} else await saveSnapshot(md5, listData)
|
||||
if (snapshotInfo.latest) snapshotInfo.list.unshift(snapshotInfo.latest)
|
||||
snapshotInfo.latest = md5
|
||||
snapshotInfo.time = Date.now()
|
||||
saveSnapshotInfo(snapshotInfo)
|
||||
return md5
|
||||
}
|
||||
|
||||
|
||||
export const getCurrentListInfoKey = async() => {
|
||||
// if (!snapshotInfo) snapshotInfo = getSnapshotInfo()
|
||||
return createSnapshot()
|
||||
}
|
||||
|
||||
@@ -1,59 +1,84 @@
|
||||
// import { throttle } from '@common/utils/common'
|
||||
import { type Server } from 'socket.io'
|
||||
// import { sendSyncActionList } from '@main/modules/winMain'
|
||||
import { encryptMsg, decryptMsg } from '../server/utils'
|
||||
import { createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from 'node:crypto'
|
||||
import os, { networkInterfaces } from 'node:os'
|
||||
import cp from 'node:child_process'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
let io: Server | null
|
||||
let removeListener: (() => void) | null
|
||||
|
||||
type listAction = 'list:action'
|
||||
export const getAddress = (): string[] => {
|
||||
const nets = networkInterfaces()
|
||||
const results: string[] = []
|
||||
// console.log(nets)
|
||||
|
||||
const handleListAction = ({ action, data }: LX.Sync.ActionList) => {
|
||||
// console.log('handleListAction', action)
|
||||
|
||||
switch (action) {
|
||||
case 'list_data_overwrite':
|
||||
void global.lx.event_list.list_data_overwrite(data, true)
|
||||
break
|
||||
case 'list_create':
|
||||
void global.lx.event_list.list_create(data.position, data.listInfos, true)
|
||||
break
|
||||
case 'list_remove':
|
||||
void global.lx.event_list.list_remove(data, true)
|
||||
break
|
||||
case 'list_update':
|
||||
void global.lx.event_list.list_update(data, true)
|
||||
break
|
||||
case 'list_update_position':
|
||||
void global.lx.event_list.list_update_position(data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_add':
|
||||
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_move':
|
||||
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_remove':
|
||||
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
|
||||
break
|
||||
case 'list_music_update':
|
||||
void global.lx.event_list.list_music_update(data, true)
|
||||
break
|
||||
case 'list_music_update_position':
|
||||
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_overwrite':
|
||||
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
|
||||
break
|
||||
case 'list_music_clear':
|
||||
void global.lx.event_list.list_music_clear(data, true)
|
||||
break
|
||||
default:
|
||||
break
|
||||
for (const interfaceInfos of Object.values(nets)) {
|
||||
if (!interfaceInfos) continue
|
||||
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
|
||||
for (const interfaceInfo of interfaceInfos) {
|
||||
if (interfaceInfo.family === 'IPv4' && !interfaceInfo.internal) {
|
||||
results.push(interfaceInfo.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
const registerListActionEvent = () => {
|
||||
|
||||
// https://stackoverflow.com/a/75309339
|
||||
export const getComputerName = () => {
|
||||
let name: string | undefined
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
name = process.env.COMPUTERNAME
|
||||
break
|
||||
case 'darwin':
|
||||
name = cp.execSync('scutil --get ComputerName').toString().trim()
|
||||
break
|
||||
case 'linux':
|
||||
name = cp.execSync('hostnamectl --pretty').toString().trim()
|
||||
break
|
||||
}
|
||||
if (!name) name = os.hostname()
|
||||
return name
|
||||
}
|
||||
|
||||
export const aesEncrypt = (text: string, key: string) => {
|
||||
const cipher = createCipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([cipher.update(Buffer.from(text)), cipher.final()]).toString('base64')
|
||||
}
|
||||
|
||||
export const aesDecrypt = (text: string, key: string) => {
|
||||
const decipher = createDecipheriv('aes-128-ecb', Buffer.from(key, 'base64'), '')
|
||||
return Buffer.concat([decipher.update(Buffer.from(text, 'base64')), decipher.final()]).toString()
|
||||
}
|
||||
|
||||
export const rsaEncrypt = (buffer: Buffer, key: string): string => {
|
||||
return publicEncrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer).toString('base64')
|
||||
}
|
||||
export const rsaDecrypt = (buffer: Buffer, key: string): Buffer => {
|
||||
return privateDecrypt({ key, padding: constants.RSA_PKCS1_OAEP_PADDING }, buffer)
|
||||
}
|
||||
|
||||
export const getLocalListData = async(): Promise<LX.Sync.ListData> => {
|
||||
const lists: LX.Sync.ListData = {
|
||||
defaultList: await global.lx.worker.dbService.getListMusics(LIST_IDS.DEFAULT),
|
||||
loveList: await global.lx.worker.dbService.getListMusics(LIST_IDS.LOVE),
|
||||
userList: [],
|
||||
}
|
||||
|
||||
const userListInfos = await global.lx.worker.dbService.getAllUserList()
|
||||
for await (const list of userListInfos) {
|
||||
lists.userList.push(await global.lx.worker.dbService.getListMusics(list.id)
|
||||
.then(musics => ({ ...list, list: musics })))
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
|
||||
export const setLocalListData = async(listData: LX.Sync.ListData) => {
|
||||
await global.lx.event_list.list_data_overwrite(listData, true)
|
||||
}
|
||||
|
||||
|
||||
export const registerListActionEvent = (sendListAction: (action: LX.Sync.ActionList) => (void | Promise<void>)) => {
|
||||
const list_data_overwrite = async(listData: MakeOptional<LX.List.ListDataFull, 'tempList'>, isRemote: boolean = false) => {
|
||||
if (isRemote) return
|
||||
await sendListAction({ action: 'list_data_overwrite', data: listData })
|
||||
@@ -130,48 +155,48 @@ const registerListActionEvent = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// const addMusic = (orderId, callback) => {
|
||||
// // ...
|
||||
// }
|
||||
export const handleRemoteListAction = async({ action, data }: LX.Sync.ActionList) => {
|
||||
// console.log('handleRemoteListAction', action)
|
||||
|
||||
const broadcast = async(action: listAction, data: any, excludeIds: string[] = []) => {
|
||||
if (!io) return
|
||||
const sockets: LX.Sync.RemoteSocket[] = await io.fetchSockets()
|
||||
for (const socket of sockets) {
|
||||
if (excludeIds.includes(socket.data.keyInfo.clientId) || !socket.data.isReady) continue
|
||||
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
|
||||
}
|
||||
}
|
||||
|
||||
export const sendListAction = async(action: LX.Sync.ActionList) => {
|
||||
console.log('sendListAction', action.action)
|
||||
// io.sockets
|
||||
await broadcast('list:action', JSON.stringify(action))
|
||||
}
|
||||
|
||||
export const registerListHandler = (_io: Server, socket: LX.Sync.Socket) => {
|
||||
if (!io) {
|
||||
io = _io
|
||||
removeListener = registerListActionEvent()
|
||||
}
|
||||
|
||||
socket.on('list:action', msg => {
|
||||
if (!socket.data.isReady) return
|
||||
// console.log(msg)
|
||||
msg = decryptMsg(socket.data.keyInfo, msg)
|
||||
if (!msg) return
|
||||
handleListAction(JSON.parse(msg))
|
||||
void broadcast('list:action', msg, [socket.data.keyInfo.clientId])
|
||||
// socket.broadcast.emit('list:action', { action: 'list_remove', data: { id: 'default', index: 0 } })
|
||||
})
|
||||
|
||||
// socket.on('list:add', addMusic)
|
||||
}
|
||||
export const unregisterListHandler = () => {
|
||||
io = null
|
||||
|
||||
if (removeListener) {
|
||||
removeListener()
|
||||
removeListener = null
|
||||
switch (action) {
|
||||
case 'list_data_overwrite':
|
||||
void global.lx.event_list.list_data_overwrite(data, true)
|
||||
break
|
||||
case 'list_create':
|
||||
void global.lx.event_list.list_create(data.position, data.listInfos, true)
|
||||
break
|
||||
case 'list_remove':
|
||||
void global.lx.event_list.list_remove(data, true)
|
||||
break
|
||||
case 'list_update':
|
||||
void global.lx.event_list.list_update(data, true)
|
||||
break
|
||||
case 'list_update_position':
|
||||
void global.lx.event_list.list_update_position(data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_add':
|
||||
void global.lx.event_list.list_music_add(data.id, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_move':
|
||||
void global.lx.event_list.list_music_move(data.fromId, data.toId, data.musicInfos, data.addMusicLocationType, true)
|
||||
break
|
||||
case 'list_music_remove':
|
||||
void global.lx.event_list.list_music_remove(data.listId, data.ids, true)
|
||||
break
|
||||
case 'list_music_update':
|
||||
void global.lx.event_list.list_music_update(data, true)
|
||||
break
|
||||
case 'list_music_update_position':
|
||||
void global.lx.event_list.list_music_update_position(data.listId, data.position, data.ids, true)
|
||||
break
|
||||
case 'list_music_overwrite':
|
||||
void global.lx.event_list.list_music_overwrite(data.listId, data.musicInfos, true)
|
||||
break
|
||||
case 'list_music_clear':
|
||||
void global.lx.event_list.list_music_clear(data, true)
|
||||
break
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron')
|
||||
const needle = require('needle')
|
||||
const zlib = require('zlib')
|
||||
const { createCipheriv, publicEncrypt, constants, randomBytes, createHash } = require('crypto')
|
||||
const USER_API_RENDERER_EVENT_NAME = require('../rendererEvent/name')
|
||||
|
||||
@@ -139,11 +140,15 @@ const handleShowUpdateAlert = (data, resolve, reject) => {
|
||||
|
||||
contextBridge.exposeInMainWorld('lx', {
|
||||
EVENT_NAMES,
|
||||
request(url, { method = 'get', timeout, headers, body, form, formData }, callback) {
|
||||
request(url, { method = 'get', timeout, headers, body, form, formData, bodyBinaryBase64 }, callback) {
|
||||
let options = { headers }
|
||||
let data
|
||||
if (body) {
|
||||
data = body
|
||||
} else if (bodyBinaryBase64) {
|
||||
try {
|
||||
data = Buffer.from(bodyBinaryBase64, 'base64')
|
||||
} catch {}
|
||||
} else if (form) {
|
||||
data = form
|
||||
// data.content_type = 'application/x-www-form-urlencoded'
|
||||
@@ -233,8 +238,26 @@ contextBridge.exposeInMainWorld('lx', {
|
||||
return Buffer.from(buf, 'binary').toString(format)
|
||||
},
|
||||
},
|
||||
zlib: {
|
||||
inflate(buf) {
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.inflate(buf, (err, data) => {
|
||||
if (err) reject(new Error(err.message))
|
||||
else resolve(data)
|
||||
})
|
||||
})
|
||||
},
|
||||
deflate(data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
zlib.deflate(data, (err, buf) => {
|
||||
if (err) reject(new Error(err.message))
|
||||
else resolve(buf)
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
version: '1.2.0',
|
||||
version: '1.4.0',
|
||||
// removeEvent(eventName, handler) {
|
||||
// if (!eventNames.includes(eventName)) return Promise.reject(new Error('The event is not supported: ' + eventName))
|
||||
// let handlers
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isLinux } from '@common/utils'
|
||||
import { closeWindow, createWindow, getBounds, isExistWindow, alwaysOnTopTools, setBounds, setIgnoreMouseEvents, setSkipTaskbar } from './main'
|
||||
import { sendConfigChange } from './rendererEvent'
|
||||
import { buildLyricConfig, getLyricWindowBounds, watchConfigKeys } from './utils'
|
||||
import { buildLyricConfig, getLyricWindowBounds, initWindowSize, watchConfigKeys } from './utils'
|
||||
|
||||
let isLock: boolean
|
||||
let isEnable: boolean
|
||||
@@ -62,6 +62,14 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (keys.includes('desktopLyric.x') && setting['desktopLyric.x'] == null) {
|
||||
setBounds(initWindowSize(
|
||||
global.lx.appSetting['desktopLyric.x'],
|
||||
global.lx.appSetting['desktopLyric.y'],
|
||||
global.lx.appSetting['desktopLyric.width'],
|
||||
global.lx.appSetting['desktopLyric.height'],
|
||||
))
|
||||
}
|
||||
}
|
||||
if (keys.includes('desktopLyric.enable') && isEnable != global.lx.appSetting['desktopLyric.enable']) {
|
||||
isEnable = global.lx.appSetting['desktopLyric.enable']
|
||||
|
||||
@@ -6,6 +6,7 @@ import { closeWindow, createWindow, isExistWindow } from './main'
|
||||
// import main from './main'
|
||||
// import { Event, EVENT_NAMES } from './event'
|
||||
|
||||
let isMainWidnowFullscreen = false
|
||||
|
||||
export default () => {
|
||||
initRendererEvent()
|
||||
@@ -13,17 +14,34 @@ export default () => {
|
||||
// global.app_event.winMain.
|
||||
|
||||
global.lx.event_app.on('main_window_inited', () => {
|
||||
isMainWidnowFullscreen = global.lx.appSetting['common.startInFullscreen']
|
||||
|
||||
if (global.lx.appSetting['desktopLyric.enable']) {
|
||||
if (isExistWindow()) sendMainWindowInitedEvent()
|
||||
else createWindow()
|
||||
if (global.lx.appSetting['desktopLyric.fullscreenHide'] && isMainWidnowFullscreen) {
|
||||
closeWindow()
|
||||
} else {
|
||||
if (isExistWindow()) sendMainWindowInitedEvent()
|
||||
else createWindow()
|
||||
}
|
||||
}
|
||||
})
|
||||
global.lx.event_app.on('updated_config', (keys, setting) => {
|
||||
setLrcConfig(keys, setting)
|
||||
if (keys.includes('desktopLyric.fullscreenHide') && global.lx.appSetting['desktopLyric.enable'] && isMainWidnowFullscreen) {
|
||||
if (global.lx.appSetting['desktopLyric.fullscreenHide']) closeWindow()
|
||||
else if (!isExistWindow()) createWindow()
|
||||
}
|
||||
})
|
||||
global.lx.event_app.on('main_window_close', () => {
|
||||
closeWindow()
|
||||
})
|
||||
global.lx.event_app.on('main_window_fullscreen', (isFullscreen) => {
|
||||
isMainWidnowFullscreen = isFullscreen
|
||||
if (global.lx.appSetting['desktopLyric.enable'] && global.lx.appSetting['desktopLyric.fullscreenHide']) {
|
||||
if (isFullscreen) closeWindow()
|
||||
else if (!isExistWindow()) createWindow()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// global.lx_event.mainWindow.on(MAIN_WINDOW_EVENT_NAME.setLyricInfo, info => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { join } from 'path'
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { debounce, isLinux } from '@common/utils'
|
||||
import { getLyricWindowBounds, minHeight, minWidth, padding } from './utils'
|
||||
import { initWindowSize } from './utils'
|
||||
import { mainSend } from '@common/mainIpc'
|
||||
import { encodePath } from '@common/utils/electron'
|
||||
|
||||
@@ -9,11 +9,11 @@ import { encodePath } from '@common/utils/electron'
|
||||
// require('./rendererEvent')
|
||||
|
||||
let browserWindow: Electron.BrowserWindow | null = null
|
||||
let isWinBoundsUdating = false
|
||||
let isWinBoundsUpdateing = false
|
||||
|
||||
const saveBoundsConfig = debounce((config: Partial<LX.AppSetting>) => {
|
||||
global.lx.event_app.update_config(config)
|
||||
if (isWinBoundsUdating) isWinBoundsUdating = false
|
||||
if (isWinBoundsUpdateing) isWinBoundsUpdateing = false
|
||||
}, 500)
|
||||
|
||||
const winEvent = () => {
|
||||
@@ -32,8 +32,8 @@ const winEvent = () => {
|
||||
|
||||
browserWindow.on('move', () => {
|
||||
// bounds = browserWindow.getBounds()
|
||||
// console.log('move', isWinBoundsUdating)
|
||||
if (isWinBoundsUdating) {
|
||||
// console.log('move', isWinBoundsUpdateing)
|
||||
if (isWinBoundsUpdateing) {
|
||||
const bounds = browserWindow!.getBounds()
|
||||
saveBoundsConfig({
|
||||
'desktopLyric.x': bounds.x,
|
||||
@@ -81,6 +81,7 @@ const winEvent = () => {
|
||||
// browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')
|
||||
// }
|
||||
if (global.lx.appSetting['desktopLyric.isAlwaysOnTop'] && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) alwaysOnTopTools.startLoop()
|
||||
browserWindow!.blur()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -95,27 +96,12 @@ export const createWindow = () => {
|
||||
// let isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']
|
||||
let isShowTaskbar = global.lx.appSetting['desktopLyric.isShowTaskbar']
|
||||
// let { width: screenWidth, height: screenHeight } = global.envParams.workAreaSize
|
||||
if (x == null || y == null) {
|
||||
if (width < minWidth) width = minWidth
|
||||
if (height < minHeight) height = minHeight
|
||||
if (global.envParams.workAreaSize) {
|
||||
x = global.envParams.workAreaSize.width + padding - width
|
||||
y = global.envParams.workAreaSize.height + padding - height
|
||||
} else {
|
||||
x = y = -padding
|
||||
}
|
||||
} else {
|
||||
let bounds = getLyricWindowBounds({ x, y, width, height }, { x: 0, y: 0, w: width, h: height })
|
||||
x = bounds.x
|
||||
y = bounds.y
|
||||
width = bounds.width
|
||||
height = bounds.height
|
||||
}
|
||||
const winSize = initWindowSize(x, y, width, height)
|
||||
global.lx.event_app.update_config({
|
||||
'desktopLyric.x': x,
|
||||
'desktopLyric.y': y,
|
||||
'desktopLyric.width': width,
|
||||
'desktopLyric.height': height,
|
||||
'desktopLyric.x': winSize.x,
|
||||
'desktopLyric.y': winSize.y,
|
||||
'desktopLyric.width': winSize.width,
|
||||
'desktopLyric.height': winSize.height,
|
||||
})
|
||||
|
||||
const { shouldUseDarkColors, theme } = global.lx.theme
|
||||
@@ -124,10 +110,10 @@ export const createWindow = () => {
|
||||
* Initial window options
|
||||
*/
|
||||
browserWindow = new BrowserWindow({
|
||||
height,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
height: winSize.height,
|
||||
width: winSize.width,
|
||||
x: winSize.x,
|
||||
y: winSize.y,
|
||||
minWidth: 380,
|
||||
minHeight: 80,
|
||||
useContentSize: true,
|
||||
@@ -183,7 +169,7 @@ export const getBounds = (): Electron.Rectangle => {
|
||||
|
||||
export const setBounds = (bounds: Electron.Rectangle) => {
|
||||
if (!browserWindow) return
|
||||
isWinBoundsUdating = true
|
||||
isWinBoundsUpdateing = true
|
||||
browserWindow.setBounds(bounds)
|
||||
}
|
||||
|
||||
|
||||
@@ -95,3 +95,28 @@ export const buildLyricConfig = (appSetting: Partial<LX.AppSetting>): Partial<LX
|
||||
}
|
||||
return setting
|
||||
}
|
||||
|
||||
export const initWindowSize = (x: LX.AppSetting['desktopLyric.x'], y: LX.AppSetting['desktopLyric.y'], width: LX.AppSetting['desktopLyric.width'], height: LX.AppSetting['desktopLyric.height']) => {
|
||||
if (x == null || y == null) {
|
||||
if (width < minWidth) width = minWidth
|
||||
if (height < minHeight) height = minHeight
|
||||
if (global.envParams.workAreaSize) {
|
||||
x = global.envParams.workAreaSize.width + padding - width
|
||||
y = global.envParams.workAreaSize.height + padding - height
|
||||
} else {
|
||||
x = y = -padding
|
||||
}
|
||||
} else {
|
||||
let bounds = getLyricWindowBounds({ x, y, width, height }, { x: 0, y: 0, w: width, h: height })
|
||||
x = bounds.x
|
||||
y = bounds.y
|
||||
width = bounds.width
|
||||
height = bounds.height
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ export default () => {
|
||||
})
|
||||
// 全屏
|
||||
mainHandle<boolean, boolean>(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, async({ params: isFullscreen }) => {
|
||||
global.lx.event_app.main_window_fullscreen(isFullscreen)
|
||||
return setFullScreen(isFullscreen)
|
||||
})
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import sync from './sync'
|
||||
import data from './data'
|
||||
import music from './music'
|
||||
import download from './download'
|
||||
import soundEffect from './soundEffect'
|
||||
import { sendEvent } from '../main'
|
||||
|
||||
export * from './app'
|
||||
@@ -33,6 +34,7 @@ export default () => {
|
||||
data()
|
||||
music()
|
||||
download()
|
||||
soundEffect()
|
||||
|
||||
global.lx.event_app.on('updated_config', (keys, setting) => {
|
||||
sendConfigChange(setting)
|
||||
|
||||
20
src/main/modules/winMain/rendererEvent/soundEffect.ts
Normal file
20
src/main/modules/winMain/rendererEvent/soundEffect.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { STORE_NAMES } from '@common/constants'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { mainOn, mainHandle } from '@common/mainIpc'
|
||||
import getStore from '@main/utils/store'
|
||||
|
||||
export default () => {
|
||||
mainHandle<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_eq_preset, async() => {
|
||||
return getStore(STORE_NAMES.SOUND_EFFECT).get('eqPreset') as LX.SoundEffect.EQPreset[] | null ?? []
|
||||
})
|
||||
mainOn<LX.SoundEffect.EQPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_eq_preset, ({ params }) => {
|
||||
getStore(STORE_NAMES.SOUND_EFFECT).set('eqPreset', params)
|
||||
})
|
||||
|
||||
mainHandle<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.get_sound_effect_convolution_preset, async() => {
|
||||
return getStore(STORE_NAMES.SOUND_EFFECT).get('convolutionPreset') as LX.SoundEffect.ConvolutionPreset[] | null ?? []
|
||||
})
|
||||
mainOn<LX.SoundEffect.ConvolutionPreset[]>(WIN_MAIN_RENDERER_EVENT_NAME.save_sound_effect_convolution_preset, ({ params }) => {
|
||||
getStore(STORE_NAMES.SOUND_EFFECT).set('convolutionPreset', params)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
import { startServer, stopServer, getStatus, generateCode } from '@main/modules/sync'
|
||||
import { startServer, stopServer, getServerStatus, generateCode, connectServer, disconnectServer, getClientStatus } from '@main/modules/sync'
|
||||
import { sendEvent } from '../main'
|
||||
|
||||
let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
||||
@@ -8,13 +8,15 @@ let selectModeListenr: ((mode: LX.Sync.Mode) => void) | null = null
|
||||
export default () => {
|
||||
mainHandle<LX.Sync.SyncServiceActions, any>(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, async({ params: data }) => {
|
||||
switch (data.action) {
|
||||
case 'enable':
|
||||
case 'enable_server':
|
||||
data.data.enable ? await startServer(parseInt(data.data.port)) : await stopServer()
|
||||
return
|
||||
case 'get_status':
|
||||
return getStatus()
|
||||
case 'generate_code':
|
||||
return await generateCode()
|
||||
case 'enable_client':
|
||||
data.data.enable ? await connectServer(data.data.host, data.data.authCode) : await disconnectServer()
|
||||
return
|
||||
case 'get_server_status': return getServerStatus()
|
||||
case 'get_client_status': return getClientStatus()
|
||||
case 'generate_code': return generateCode()
|
||||
case 'select_mode':
|
||||
if (selectModeListenr) selectModeListenr(data.data)
|
||||
break
|
||||
@@ -29,18 +31,24 @@ export const sendSyncAction = (data: LX.Sync.SyncMainWindowActions) => {
|
||||
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.sync_action, data)
|
||||
}
|
||||
|
||||
export const sendStatus = (status: LX.Sync.Status) => {
|
||||
export const sendClientStatus = (status: LX.Sync.ClientStatus) => {
|
||||
sendSyncAction({
|
||||
action: 'status',
|
||||
action: 'client_status',
|
||||
data: status,
|
||||
})
|
||||
}
|
||||
export const sendSelectMode = (keyInfo: LX.Sync.KeyInfo, listener: (mode: LX.Sync.Mode) => void) => {
|
||||
export const sendServerStatus = (status: LX.Sync.ServerStatus) => {
|
||||
sendSyncAction({
|
||||
action: 'server_status',
|
||||
data: status,
|
||||
})
|
||||
}
|
||||
export const sendSelectMode = (deviceName: string, listener: (mode: LX.Sync.Mode) => void) => {
|
||||
selectModeListenr = listener
|
||||
sendSyncAction({ action: 'select_mode', data: keyInfo })
|
||||
return () => {
|
||||
selectModeListenr = null
|
||||
}
|
||||
sendSyncAction({ action: 'select_mode', data: deviceName })
|
||||
}
|
||||
export const removeSelectModeListener = () => {
|
||||
selectModeListenr = null
|
||||
}
|
||||
export const sendCloseSelectMode = () => {
|
||||
sendSyncAction({ action: 'close_select_mode' })
|
||||
|
||||
1
src/main/types/common.d.ts
vendored
1
src/main/types/common.d.ts
vendored
@@ -10,3 +10,4 @@ import '@common/types/player'
|
||||
import '@common/types/desktop_lyric'
|
||||
import '@common/types/theme'
|
||||
import '@common/types/ipc_main'
|
||||
import '@common/types/sound_effect'
|
||||
|
||||
67
src/main/types/sync.d.ts
vendored
67
src/main/types/sync.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { Socket as _Socket, RemoteSocket as _RemoteSocket } from 'socket.io'
|
||||
import type WS from 'ws'
|
||||
|
||||
type DefaultEventsMap = Record<string, (...args: any[]) => void>
|
||||
|
||||
@@ -6,24 +6,57 @@ type DefaultEventsMap = Record<string, (...args: any[]) => void>
|
||||
declare global {
|
||||
namespace LX {
|
||||
namespace Sync {
|
||||
class Socket extends _Socket {
|
||||
data: SocketData
|
||||
namespace Client {
|
||||
interface Socket extends WS.WebSocket {
|
||||
isReady: boolean
|
||||
data: {
|
||||
keyInfo: ClientKeyInfo
|
||||
urlInfo: UrlInfo
|
||||
}
|
||||
|
||||
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncSendType>(
|
||||
eventName: T,
|
||||
handler: (data: LX.Sync.ActionSyncSendType[T]) => (void | Promise<void>)
|
||||
) => () => void
|
||||
|
||||
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
|
||||
|
||||
sendData: <T extends keyof LX.Sync.ActionSyncType>(
|
||||
eventName: T,
|
||||
data?: LX.Sync.ActionSyncType[T],
|
||||
callback?: (err?: Error) => void
|
||||
) => void
|
||||
}
|
||||
|
||||
interface UrlInfo {
|
||||
wsProtocol: string
|
||||
httpProtocol: string
|
||||
hostPath: string
|
||||
href: string
|
||||
}
|
||||
}
|
||||
class RemoteSocket extends _RemoteSocket<DefaultEventsMap, any> {
|
||||
readonly data: SocketData
|
||||
namespace Server {
|
||||
interface Socket extends WS.WebSocket {
|
||||
isAlive?: boolean
|
||||
isMobile: boolean
|
||||
isReady: boolean
|
||||
keyInfo: LX.Sync.ServerKeyInfo
|
||||
|
||||
onRemoteEvent: <T extends keyof LX.Sync.ActionSyncType>(
|
||||
eventName: T,
|
||||
handler: (data: LX.Sync.ActionSyncType[T]) => void
|
||||
) => () => void
|
||||
|
||||
onClose: (handler: (err: Error) => (void | Promise<void>)) => () => void
|
||||
|
||||
sendData: <T extends keyof LX.Sync.ActionSyncSendType>(
|
||||
eventName: T,
|
||||
data?: LX.Sync.ActionSyncSendType[T],
|
||||
callback?: (err?: Error) => void
|
||||
) => void
|
||||
}
|
||||
type SocketServer = WS.Server<Socket>
|
||||
}
|
||||
interface Data {
|
||||
action: string
|
||||
data: any
|
||||
}
|
||||
interface SocketData {
|
||||
snapshotFilePath: string
|
||||
isCreatedSnapshot: boolean
|
||||
keyInfo: KeyInfo
|
||||
isReady: boolean
|
||||
}
|
||||
type Action = 'list:sync'
|
||||
type ListAction = 'getData' | 'finished'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,7 +154,15 @@ export const initHotKey = async() => {
|
||||
let localConfig = electronStore_hotKey.get('local') as LX.HotKeyConfig | null
|
||||
let globalConfig = electronStore_hotKey.get('global') as LX.HotKeyConfig | null
|
||||
|
||||
if (!localConfig) {
|
||||
if (globalConfig) {
|
||||
// 移除v2.2.0及之前设置的全局媒体快捷键注册
|
||||
if (globalConfig.keys.MediaPlayPause) {
|
||||
delete globalConfig.keys.MediaPlayPause
|
||||
delete globalConfig.keys.MediaNextTrack
|
||||
delete globalConfig.keys.MediaPreviousTrack
|
||||
electronStore_hotKey.set('global', globalConfig)
|
||||
}
|
||||
} else {
|
||||
// migrate hotKey
|
||||
const config = await migrateHotKey()
|
||||
if (config) {
|
||||
|
||||
221
src/main/utils/request.ts
Normal file
221
src/main/utils/request.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import needle, { type NeedleHttpVerbs, type NeedleOptions, type BodyData, type NeedleCallback, type NeedleResponse } from 'needle'
|
||||
// import progress from 'request-progress'
|
||||
import { httpOverHttp, httpsOverHttp } from 'tunnel'
|
||||
import { type ClientRequest } from 'node:http'
|
||||
// import fs from 'fs'
|
||||
|
||||
export const requestMsg = {
|
||||
fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。',
|
||||
unachievable: '哦No😱...接口无法访问了!',
|
||||
timeout: '请求超时',
|
||||
// unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~',
|
||||
notConnectNetwork: '无法连接到服务器',
|
||||
cancelRequest: '取消http请求',
|
||||
} as const
|
||||
|
||||
|
||||
const httpsRxp = /^https:/
|
||||
let envProxy: null | { host: string, port: string } = null
|
||||
|
||||
const getRequestAgent = (url: string) => {
|
||||
if (envProxy == null) {
|
||||
if (global.envParams.cmdParams['proxy-server'] && typeof global.envParams.cmdParams['proxy-server'] == 'string') {
|
||||
const [host, port = ''] = global.envParams.cmdParams['proxy-server'].split(':')
|
||||
envProxy = {
|
||||
host,
|
||||
port,
|
||||
}
|
||||
}
|
||||
}
|
||||
const proxy = {
|
||||
enable: global.lx.appSetting['network.proxy.enable'],
|
||||
host: global.lx.appSetting['network.proxy.host'],
|
||||
port: global.lx.appSetting['network.proxy.port'],
|
||||
envProxy,
|
||||
}
|
||||
|
||||
let options
|
||||
if (global.lx.appSetting['network.proxy.enable'] && proxy.host) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.host,
|
||||
port: parseInt(proxy.port || '80'),
|
||||
},
|
||||
}
|
||||
} else if (proxy.envProxy) {
|
||||
options = {
|
||||
proxy: {
|
||||
host: proxy.envProxy.host,
|
||||
port: parseInt(proxy.envProxy.port || '80'),
|
||||
},
|
||||
}
|
||||
}
|
||||
return options ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)(options) : undefined
|
||||
}
|
||||
|
||||
export interface RequestOptions extends NeedleOptions {
|
||||
method?: NeedleHttpVerbs
|
||||
body?: BodyData
|
||||
form?: BodyData
|
||||
formData?: BodyData
|
||||
}
|
||||
export type RequestCallback = NeedleCallback
|
||||
type RequestResponse = NeedleResponse
|
||||
const request = (url: string, options: RequestOptions, callback: RequestCallback): ClientRequest => {
|
||||
let data: BodyData = null
|
||||
if (options.body) {
|
||||
data = options.body
|
||||
} else if (options.form) {
|
||||
data = options.form
|
||||
// data.content_type = 'application/x-www-form-urlencoded'
|
||||
options.json = false
|
||||
} else if (options.formData) {
|
||||
data = options.formData
|
||||
// data.content_type = 'multipart/form-data'
|
||||
options.json = false
|
||||
}
|
||||
options.response_timeout = options.timeout
|
||||
|
||||
return needle.request(options.method ?? 'get', url, data, options, (err, resp, body) => {
|
||||
if (!err) {
|
||||
body = resp.body = resp.raw.toString()
|
||||
try {
|
||||
resp.body = JSON.parse(resp.body)
|
||||
} catch (_) {}
|
||||
body = resp.body
|
||||
}
|
||||
callback(err, resp, body)
|
||||
// @ts-expect-error
|
||||
}).request
|
||||
}
|
||||
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
|
||||
}
|
||||
// var proxyUrl = "http://" + user + ":" + password + "@" + host + ":" + port;
|
||||
// var proxiedRequest = request.defaults({'proxy': proxyUrl});
|
||||
|
||||
// interface RequestPromise extends Promise<RequestResponse> {
|
||||
// abort: () => void
|
||||
// }
|
||||
|
||||
/**
|
||||
* promise 形式的请求方法
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
*/
|
||||
const buildHttpPromose = async(url: string, options: RequestOptions): Promise<RequestResponse> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
void fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// options.isShowProgress && window.api.hideProgress()
|
||||
// debugRequest && console.log(`\n---response------${url}------------`)
|
||||
// debugRequest && console.log(body)
|
||||
// obj.requestObj = null
|
||||
// obj.cancelFn = null
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(resp)
|
||||
})
|
||||
// .then(request => {
|
||||
// // obj.requestObj = ro
|
||||
// // if (obj.isCancelled) obj.cancelHttp()
|
||||
// promise.abort = () => {
|
||||
// request.destroy(new Error('cancelled'))
|
||||
// }
|
||||
// })
|
||||
})
|
||||
// let obj = {
|
||||
// isCancelled: false,
|
||||
// cancelHttp: () => {
|
||||
// if (!obj.requestObj) return obj.isCancelled = true
|
||||
// cancelHttp(obj.requestObj)
|
||||
// obj.requestObj = null
|
||||
// obj.promise = obj.cancelHttp = null
|
||||
// obj.cancelFn(new Error(requestMsg.cancelRequest))
|
||||
// obj.cancelFn = null
|
||||
// },
|
||||
// }
|
||||
// obj.promise = new Promise((resolve, reject) => {
|
||||
// obj.cancelFn = reject
|
||||
// debugRequest && console.log(`\n---send request------${url}------------`)
|
||||
// fetchData(url, options.method, options, (err, resp, body) => {
|
||||
// // options.isShowProgress && window.api.hideProgress()
|
||||
// debugRequest && console.log(`\n---response------${url}------------`)
|
||||
// debugRequest && console.log(body)
|
||||
// obj.requestObj = null
|
||||
// obj.cancelFn = null
|
||||
// if (err) { reject(err); return }
|
||||
// resolve(resp)
|
||||
// }).then(ro => {
|
||||
// obj.requestObj = ro
|
||||
// if (obj.isCancelled) obj.cancelHttp()
|
||||
// })
|
||||
// })
|
||||
// return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求超时自动重试
|
||||
* @param {*} url
|
||||
* @param {*} options
|
||||
*/
|
||||
export const httpFetch = async(url: string, options: RequestOptions = { method: 'get' }) => {
|
||||
return buildHttpPromose(url, options).catch(async(err: any) => {
|
||||
// console.log('出错', err)
|
||||
if (err.message === 'socket hang up') {
|
||||
// window.globalObj.apiSource = 'temp'
|
||||
throw new Error(requestMsg.unachievable)
|
||||
}
|
||||
switch (err.code) {
|
||||
case 'ETIMEDOUT':
|
||||
case 'ESOCKETTIMEDOUT':
|
||||
throw new Error(requestMsg.timeout)
|
||||
case 'ENOTFOUND':
|
||||
throw new Error(requestMsg.notConnectNetwork)
|
||||
default:
|
||||
throw err
|
||||
}
|
||||
})
|
||||
// requestObj.promise = requestObj.promise.catch(async err => {
|
||||
// // console.log('出错', err)
|
||||
// if (err.message === 'socket hang up') {
|
||||
// // window.globalObj.apiSource = 'temp'
|
||||
// return Promise.reject(new Error(requestMsg.unachievable))
|
||||
// }
|
||||
// switch (err.code) {
|
||||
// case 'ETIMEDOUT':
|
||||
// case 'ESOCKETTIMEDOUT':
|
||||
// return Promise.reject(new Error(requestMsg.timeout))
|
||||
// case 'ENOTFOUND':
|
||||
// return Promise.reject(new Error(requestMsg.notConnectNetwork))
|
||||
// default:
|
||||
// return Promise.reject(err)
|
||||
// }
|
||||
// })
|
||||
// return requestPromise
|
||||
}
|
||||
|
||||
const fetchData = async(url: string, method: RequestOptions['method'], {
|
||||
headers = {},
|
||||
format = 'json',
|
||||
timeout = 15000,
|
||||
...options
|
||||
}, callback: RequestCallback) => {
|
||||
// console.log(url, options)
|
||||
console.log('---start---', url)
|
||||
headers = Object.assign({}, headers)
|
||||
|
||||
return request(url, {
|
||||
...options,
|
||||
method,
|
||||
headers: Object.assign({}, defaultHeaders, headers),
|
||||
timeout,
|
||||
agent: getRequestAgent(url),
|
||||
json: format === 'json',
|
||||
}, (err, resp, body) => {
|
||||
callback(err, resp, body)
|
||||
})
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export default (name: string, isIgnoredError = true, isShowErrorAlert = true): S
|
||||
|
||||
|
||||
const backPath = join(global.lxDataPath, name + '.json.bak')
|
||||
fs.copyFileSync(join(global.lxDataPath, name + '.json'), backPath)
|
||||
fs.renameSync(join(global.lxDataPath, name + '.json'), backPath)
|
||||
if (isShowErrorAlert) {
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
|
||||
@@ -29,7 +29,7 @@ export default (db: Database.Database) => {
|
||||
// PRAGMA user_version = x
|
||||
// console.log(db.prepare('PRAGMA user_version').get().user_version)
|
||||
// https://github.com/WiseLibs/better-sqlite3/issues/668#issuecomment-1145285728
|
||||
const version = db.prepare('SELECT "field_value" FROM "main"."db_info" WHERE "field_name" = ?').get('version').field_value
|
||||
const version = (db.prepare<[string]>('SELECT "field_value" FROM "main"."db_info" WHERE "field_name" = ?').get('version') as { field_value: string }).field_value
|
||||
switch (version) {
|
||||
case '1':
|
||||
migrateV1(db)
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
/**
|
||||
* 查询下载歌曲列表
|
||||
*/
|
||||
export const queryDownloadList = (): LX.DBService.DownloadMusicInfo[] => {
|
||||
export const queryDownloadList = () => {
|
||||
const queryStatement = createQueryStatement()
|
||||
return queryStatement.all()
|
||||
return queryStatement.all() as LX.DBService.DownloadMusicInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getDB } from '../../db'
|
||||
*/
|
||||
export const createQueryStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
SELECT "id", "isComplate", "status", "statusText", "progress_downloaded", "progress_total", "url", "quality", "ext", "fileName", "filePath", "musicInfo", "position"
|
||||
FROM download_list
|
||||
ORDER BY "position" ASC
|
||||
@@ -30,7 +30,7 @@ export const createInsertStatement = () => {
|
||||
*/
|
||||
export const createClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
DELETE FROM "main"."download_list"
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ import {
|
||||
* 获取用户列表
|
||||
* @returns
|
||||
*/
|
||||
export const queryAllUserList = (): LX.DBService.UserListInfo[] => {
|
||||
return createListQueryStatement().all()
|
||||
export const queryAllUserList = () => {
|
||||
return createListQueryStatement().all() as LX.DBService.UserListInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,9 +154,9 @@ export const updateMusicInfos = (list: LX.DBService.MusicInfo[]) => {
|
||||
* @param listId 列表Id
|
||||
* @returns 列表歌曲
|
||||
*/
|
||||
export const queryMusicInfoByListId = (listId: string): LX.DBService.MusicInfo[] => {
|
||||
export const queryMusicInfoByListId = (listId: string) => {
|
||||
const musicInfoQueryStatement = createMusicInfoQueryStatement()
|
||||
return musicInfoQueryStatement.all({ listId })
|
||||
return musicInfoQueryStatement.all({ listId }) as LX.DBService.MusicInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,9 +268,9 @@ export const removeMusicInfoByListId = (ids: string[]) => {
|
||||
* @param musicInfoId 音乐id
|
||||
* @returns
|
||||
*/
|
||||
export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId: string): LX.DBService.MusicInfo | null => {
|
||||
export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId: string) => {
|
||||
const musicInfoByListAndMusicInfoIdQueryStatement = createMusicInfoByListAndMusicInfoIdQueryStatement()
|
||||
return musicInfoByListAndMusicInfoIdQueryStatement.get({ listId, musicInfoId })
|
||||
return musicInfoByListAndMusicInfoIdQueryStatement.get({ listId, musicInfoId }) as LX.DBService.MusicInfo | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,9 +278,9 @@ export const queryMusicInfoByListIdAndMusicInfoId = (listId: string, musicInfoId
|
||||
* @param id 音乐id
|
||||
* @returns
|
||||
*/
|
||||
export const queryMusicInfoByMusicInfoId = (id: string): LX.DBService.MusicInfo[] => {
|
||||
export const queryMusicInfoByMusicInfoId = (id: string) => {
|
||||
const musicInfoByMusicInfoIdQueryStatement = createMusicInfoByMusicInfoIdQueryStatement()
|
||||
return musicInfoByMusicInfoIdQueryStatement.all(id)
|
||||
return musicInfoByMusicInfoIdQueryStatement.all(id) as LX.DBService.MusicInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ import { getDB } from '../../db'
|
||||
*/
|
||||
export const createListQueryStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
SELECT "id", "name", "source", "sourceListId", "position", "locationUpdateTime"
|
||||
FROM "main"."my_list"
|
||||
`)
|
||||
@@ -30,7 +30,7 @@ export const createListInsertStatement = () => {
|
||||
*/
|
||||
export const createListClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare('DELETE FROM "main"."my_list"')
|
||||
return db.prepare<[]>('DELETE FROM "main"."my_list"')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +100,7 @@ export const createMusicInfoUpdateStatement = () => {
|
||||
*/
|
||||
export const createMusicInfoClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare('DELETE FROM "main"."my_list_music_info"')
|
||||
return db.prepare<[]>('DELETE FROM "main"."my_list_music_info"')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,7 +162,7 @@ export const createMusicInfoOrderInsertStatement = () => {
|
||||
*/
|
||||
export const createMusicInfoOrderClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare('DELETE FROM "main"."my_list_music_info_order"')
|
||||
return db.prepare<[]>('DELETE FROM "main"."my_list_music_info_order"')
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,9 +20,9 @@ import {
|
||||
* @param id 歌曲id
|
||||
* @returns 歌词信息
|
||||
*/
|
||||
export const queryLyric = (id: string): LX.DBService.Lyricnfo[] => {
|
||||
export const queryLyric = (id: string) => {
|
||||
const lyricQueryStatement = createLyricQueryStatement()
|
||||
return lyricQueryStatement.all(id)
|
||||
return lyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,9 +30,9 @@ export const queryLyric = (id: string): LX.DBService.Lyricnfo[] => {
|
||||
* @param id 歌曲id
|
||||
* @returns 歌词信息
|
||||
*/
|
||||
export const queryRawLyric = (id: string): LX.DBService.Lyricnfo[] => {
|
||||
export const queryRawLyric = (id: string) => {
|
||||
const rawLyricQueryStatement = createRawLyricQueryStatement()
|
||||
return rawLyricQueryStatement.all(id)
|
||||
return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +84,7 @@ export const clearRawLyric = () => {
|
||||
*/
|
||||
export const countRawLyric = () => {
|
||||
const countStatement = createRawLyricCountStatement()
|
||||
return countStatement.get().count
|
||||
return (countStatement.get() as { count: number }).count
|
||||
}
|
||||
|
||||
|
||||
@@ -93,9 +93,9 @@ export const countRawLyric = () => {
|
||||
* @param id 歌曲id
|
||||
* @returns 歌词信息
|
||||
*/
|
||||
export const queryEditedLyric = (id: string): LX.DBService.Lyricnfo[] => {
|
||||
export const queryEditedLyric = (id: string) => {
|
||||
const rawLyricQueryStatement = createEditedLyricQueryStatement()
|
||||
return rawLyricQueryStatement.all(id)
|
||||
return rawLyricQueryStatement.all(id) as LX.DBService.Lyricnfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,5 +148,5 @@ export const clearEditedLyric = () => {
|
||||
*/
|
||||
export const countEditedLyric = () => {
|
||||
const countStatement = createEditedLyricCountStatement()
|
||||
return countStatement.get().count
|
||||
return (countStatement.get() as { count: number }).count
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export const createRawLyricInsertStatement = () => {
|
||||
*/
|
||||
export const createRawLyricClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
DELETE FROM "main"."lyric"
|
||||
WHERE "source"='${RAW_LYRIC}'
|
||||
`)
|
||||
@@ -83,7 +83,7 @@ export const createRawLyricUpdateStatement = () => {
|
||||
*/
|
||||
export const createRawLyricCountStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${RAW_LYRIC}'`)
|
||||
return db.prepare<[]>(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${RAW_LYRIC}'`)
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export const createEditedLyricInsertStatement = () => {
|
||||
*/
|
||||
export const createEditedLyricClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
DELETE FROM "main"."lyric"
|
||||
WHERE "source"='${EDITED_LYRIC}'
|
||||
`)
|
||||
@@ -153,5 +153,5 @@ export const createEditedLyricUpdateStatement = () => {
|
||||
*/
|
||||
export const createEditedLyricCountStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${EDITED_LYRIC}'`)
|
||||
return db.prepare<[]>(`SELECT COUNT(*) as count FROM "main"."lyric" WHERE "source"='${EDITED_LYRIC}'`)
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
* @param id 歌曲id
|
||||
* @returns 歌曲信息
|
||||
*/
|
||||
export const queryMusicInfo = (id: string): LX.DBService.MusicInfoOtherSource[] => {
|
||||
export const queryMusicInfo = (id: string) => {
|
||||
const musicInfoQueryStatement = createMusicInfoQueryStatement()
|
||||
return musicInfoQueryStatement.all(id)
|
||||
return musicInfoQueryStatement.all(id) as LX.DBService.MusicInfoOtherSource[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,5 +55,5 @@ export const clearMusicInfo = () => {
|
||||
*/
|
||||
export const countMusicInfo = () => {
|
||||
const countStatement = createCountStatement()
|
||||
return countStatement.get().count
|
||||
return (countStatement.get() as { count: number }).count
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export const createMusicInfoInsertStatement = () => {
|
||||
*/
|
||||
export const createMusicInfoClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
DELETE FROM "main"."music_info_other_source"
|
||||
`)
|
||||
}
|
||||
@@ -56,5 +56,5 @@ export const createMusicInfoDeleteStatement = () => {
|
||||
*/
|
||||
export const createCountStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare('SELECT COUNT(*) as count FROM "main"."music_info_other_source"')
|
||||
return db.prepare<[]>('SELECT COUNT(*) as count FROM "main"."music_info_other_source"')
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import {
|
||||
* @param id 歌曲id
|
||||
* @returns url
|
||||
*/
|
||||
export const queryMusicUrl = (id: string): string | null => {
|
||||
export const queryMusicUrl = (id: string) => {
|
||||
const queryStatement = createQueryStatement()
|
||||
return queryStatement.get(id)?.url
|
||||
return (queryStatement.get(id) as { url: string } | null)?.url ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,5 +71,5 @@ export const clearMusicUrl = () => {
|
||||
*/
|
||||
export const countMusicUrl = () => {
|
||||
const countStatement = createCountStatement()
|
||||
return countStatement.get().count
|
||||
return (countStatement.get() as { count: number }).count
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const createInsertStatement = () => {
|
||||
*/
|
||||
export const createClearStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare(`
|
||||
return db.prepare<[]>(`
|
||||
DELETE FROM "main"."music_url"
|
||||
`)
|
||||
}
|
||||
@@ -65,5 +65,5 @@ export const createUpdateStatement = () => {
|
||||
*/
|
||||
export const createCountStatement = () => {
|
||||
const db = getDB()
|
||||
return db.prepare('SELECT COUNT(*) as count FROM "main"."music_url"')
|
||||
return db.prepare<[]>('SELECT COUNT(*) as count FROM "main"."music_url"')
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import tables from './tables'
|
||||
|
||||
const rxp = /\n|\s|;|--.+/g
|
||||
export default (db: Database.Database) => {
|
||||
const result = db.prepare('SELECT type,name,tbl_name,sql FROM "main".sqlite_master WHERE sql NOT NULL;').all()
|
||||
const result = db.prepare<[]>('SELECT type,name,tbl_name,sql FROM "main".sqlite_master WHERE sql NOT NULL;').all() as Array<{ type: string, name: string, tbl_name: string, sql: string }>
|
||||
const dbTableMap = new Map<string, string>()
|
||||
for (const info of result) dbTableMap.set(info.name, info.sql.replace(rxp, ''))
|
||||
return Array.from(tables.entries()).every(([name, sql]) => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<html lang="cn">
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" style="background-color: transparent;">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge"/>
|
||||
<title>桌面歌词-洛雪音乐助手</title>
|
||||
</head>
|
||||
<body>
|
||||
<body id="body" style="background-color: transparent;">
|
||||
<div id="root"></div>
|
||||
<script>
|
||||
window.dom_style_theme = document.createElement('style')
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<layout-update-modal />
|
||||
<layout-pact-modal />
|
||||
<layout-sync-mode-modal />
|
||||
<layout-sync-auth-code-modal />
|
||||
<layout-play-detail />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
BIN
src/renderer/assets/medias/Silence02s.mp3
Normal file
BIN
src/renderer/assets/medias/Silence02s.mp3
Normal file
Binary file not shown.
5
src/renderer/assets/svgs/tune-variant.svg
Normal file
5
src/renderer/assets/svgs/tune-variant.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 13C6.14 13 4.59 14.28 4.14 16H2V18H4.14C4.59 19.72 6.14 21 8 21S11.41 19.72 11.86 18H22V16H11.86C11.41 14.28 9.86 13 8 13M8 19C6.9 19 6 18.1 6 17C6 15.9 6.9 15 8 15S10 15.9 10 17C10 18.1 9.1 19 8 19M19.86 6C19.41 4.28 17.86 3 16 3S12.59 4.28 12.14 6H2V8H12.14C12.59 9.72 14.14 11 16 11S19.41 9.72 19.86 8H22V6H19.86M16 9C14.9 9 14 8.1 14 7C14 5.9 14.9 5 16 5S18 5.9 18 7C18 8.1 17.1 9 16 9Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 505 B |
@@ -56,6 +56,7 @@ const popupStyle = reactive({
|
||||
|
||||
const arrowHeight = 9
|
||||
const arrowWidth = 8
|
||||
const sidePadding = 50
|
||||
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (!visible || !dom_content.value || !props.btnEl) return
|
||||
@@ -63,24 +64,24 @@ watch(() => props.visible, (visible) => {
|
||||
const maxHeight = document.body.clientHeight
|
||||
const elTop = rect.top - window.lx.rootOffset
|
||||
const bottomTopVal = elTop + rect.height
|
||||
const contentHeight = dom_content.value.scrollHeight + arrowHeight + 10
|
||||
const contentHeight = dom_content.value.scrollHeight + arrowHeight + sidePadding
|
||||
if (bottomTopVal + contentHeight < maxHeight || (contentHeight > elTop && elTop <= maxHeight - bottomTopVal)) {
|
||||
isShowTop.value = false
|
||||
popupStyle.top = bottomTopVal + arrowHeight + 'px'
|
||||
popupStyle.maxHeight = maxHeight - bottomTopVal - arrowHeight - 10 + 'px'
|
||||
popupStyle.maxHeight = maxHeight - bottomTopVal - arrowHeight - sidePadding + 'px'
|
||||
} else {
|
||||
isShowTop.value = true
|
||||
let maxContentHeight = elTop - arrowHeight - 10
|
||||
popupStyle.top = (elTop - (elTop < contentHeight ? elTop : contentHeight) + 10) + 'px'
|
||||
let maxContentHeight = elTop - arrowHeight - sidePadding
|
||||
popupStyle.top = (elTop - (elTop < contentHeight ? elTop : contentHeight) + sidePadding) + 'px'
|
||||
popupStyle.maxHeight = maxContentHeight + 'px'
|
||||
}
|
||||
|
||||
const maxWidth = document.body.clientWidth - 20
|
||||
let center = dom_content.value.clientWidth / 2
|
||||
let left = rect.left + rect.width / 2 - window.lx.rootOffset - center
|
||||
if (left < 10) {
|
||||
center -= 10 - left
|
||||
left = 10
|
||||
if (left < sidePadding) {
|
||||
center -= sidePadding - left
|
||||
left = sidePadding
|
||||
} else if (left + dom_content.value.clientWidth > maxWidth) {
|
||||
let newLeft = maxWidth - dom_content.value.clientWidth
|
||||
center = center + left - newLeft
|
||||
|
||||
@@ -22,6 +22,7 @@ import { watch, ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
|
||||
import { addListMusics, moveListMusics, createUserList, getMusicExistListIds } from '@renderer/store/list/action'
|
||||
import useKeyDown from '@renderer/utils/compositions/useKeyDown'
|
||||
import { useI18n } from '@/lang'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -63,6 +64,7 @@ export default {
|
||||
emits: ['update:show'],
|
||||
setup(props) {
|
||||
const keyModDown = useKeyDown('mod')
|
||||
const t = useI18n()
|
||||
const lists = ref([])
|
||||
|
||||
const currentMusicInfo = ref({})
|
||||
@@ -81,8 +83,8 @@ export default {
|
||||
|
||||
const getList = () => {
|
||||
lists.value = [
|
||||
defaultList,
|
||||
loveList,
|
||||
{ ...defaultList, name: t(defaultList.name) },
|
||||
{ ...loveList, name: t(loveList.name) },
|
||||
...userLists,
|
||||
].filter(l => !props.excludeListId.includes(l.id)).map(l => ({ ...l, isExist: false }))
|
||||
checkMusicExist(currentMusicInfo.value)
|
||||
|
||||
@@ -21,6 +21,7 @@ import { computed } from '@common/utils/vueTools'
|
||||
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
|
||||
import { addListMusics, moveListMusics, createUserList } from '@renderer/store/list/action'
|
||||
import useKeyDown from '@renderer/utils/compositions/useKeyDown'
|
||||
import { useI18n } from '@/lang'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -64,11 +65,12 @@ export default {
|
||||
emits: ['update:show', 'confirm'],
|
||||
setup(props) {
|
||||
const keyModDown = useKeyDown('mod')
|
||||
const t = useI18n()
|
||||
|
||||
const lists = computed(() => {
|
||||
return [
|
||||
defaultList,
|
||||
loveList,
|
||||
{ ...defaultList, name: t(defaultList.name) },
|
||||
{ ...loveList, name: t(loveList.name) },
|
||||
...userLists,
|
||||
].filter(l => !props.excludeListId.includes(l.id))
|
||||
})
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<template #content>
|
||||
<div :class="$style.setting">
|
||||
<div :class="$style.info">
|
||||
<span>{{ playbackRate }}x</span>
|
||||
<base-btn min @click="handleUpdatePlaybackRate(1)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
|
||||
<span>{{ playbackRate.toFixed(2) }}x</span>
|
||||
<base-btn min @click="handleUpdatePlaybackRate(100)">{{ $t('player__playback_rate_reset_btn') }}</base-btn>
|
||||
</div>
|
||||
<base-slider-bar :value="playbackRate" :min="0.5" :max="2" @change="handleUpdatePlaybackRate" />
|
||||
<base-slider-bar :class="$style.slider" :value="playbackRate * 100" :min="60" :max="200" @change="handleUpdatePlaybackRate" />
|
||||
</div>
|
||||
</template>
|
||||
</material-popup-btn>
|
||||
@@ -22,7 +22,7 @@
|
||||
import { playbackRate } from '@renderer/store/player/playbackRate'
|
||||
|
||||
const handleUpdatePlaybackRate = (val) => {
|
||||
window.app_event.setPlaybackRate(Math.round(val * 10) / 10)
|
||||
window.app_event.setPlaybackRate(Math.round(val) / 100)
|
||||
}
|
||||
|
||||
// const icon = computed(() => {
|
||||
@@ -87,6 +87,7 @@ const handleUpdatePlaybackRate = (val) => {
|
||||
flex-flow: column nowrap;
|
||||
padding: 2px 3px;
|
||||
gap: 8px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.info {
|
||||
@@ -100,5 +101,9 @@ const handleUpdatePlaybackRate = (val) => {
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<base-btn min :class="[$style.newPreset, {[$style.editing]: isEditing}]" :aria-label="$t('player__sound_effect_biquad_filter_save_btn')" @click="handleEditing($event)">
|
||||
<svg-icon name="plus" />
|
||||
<base-input ref="input" :class="$style.newPresetInput" :value="newPresetName" :placeholder="$t('player__sound_effect_biquad_filter_save_input')" @keyup.enter="handleSave($event)" @blur="handleSave($event)" />
|
||||
</base-btn>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { saveUserConvolutionPreset } from '@renderer/store/soundEffect'
|
||||
|
||||
const isEditing = ref(false)
|
||||
const input = ref(false)
|
||||
const newPresetName = ref('')
|
||||
|
||||
const handleEditing = () => {
|
||||
if (isEditing.value) return
|
||||
// if (!this.newPresetName) this.newPresetName = this.listName
|
||||
isEditing.value = true
|
||||
nextTick(() => {
|
||||
input.value.$el.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = (event) => {
|
||||
let name = event.target.value.trim()
|
||||
newPresetName.value = event.target.value = ''
|
||||
isEditing.value = false
|
||||
if (!name) return
|
||||
if (name.length > 20) name = name.substring(0, 20)
|
||||
saveUserConvolutionPreset({
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
source: appSetting['player.soundEffect.convolution.fileName'],
|
||||
mainGain: appSetting['player.soundEffect.convolution.mainGain'],
|
||||
sendGain: appSetting['player.soundEffect.convolution.sendGain'],
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.newPreset {
|
||||
position: relative;
|
||||
border: 1px dashed var(--color-primary-font-hover);
|
||||
// background-color: var(--color-main-background);
|
||||
color: var(--color-primary-font-hover);
|
||||
opacity: .7;
|
||||
height: 22px;
|
||||
|
||||
&.editing {
|
||||
opacity: 1;
|
||||
width: 90px;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
.newPresetInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.svg-icon {
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.newPresetInput {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// line-height: 16px;
|
||||
background: none !important;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
padding: 0 3px;
|
||||
border-radius: 0;
|
||||
display: none;
|
||||
&::placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<base-btn min :class="[$style.newPreset, {[$style.editing]: isEditing}]" :aria-label="$t('player__sound_effect_biquad_filter_save_btn')" @click="handleEditing($event)">
|
||||
<svg-icon name="plus" />
|
||||
<base-input ref="input" :class="$style.newPresetInput" :value="newPresetName" :placeholder="$t('player__sound_effect_biquad_filter_save_input')" @keyup.enter="handleSave($event)" @blur="handleSave($event)" />
|
||||
</base-btn>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { saveUserEQPreset } from '@renderer/store/soundEffect'
|
||||
|
||||
const isEditing = ref(false)
|
||||
const input = ref(false)
|
||||
const newPresetName = ref('')
|
||||
|
||||
const handleEditing = () => {
|
||||
if (isEditing.value) return
|
||||
// if (!this.newPresetName) this.newPresetName = this.listName
|
||||
isEditing.value = true
|
||||
nextTick(() => {
|
||||
input.value.$el.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSave = (event) => {
|
||||
let name = event.target.value.trim()
|
||||
newPresetName.value = event.target.value = ''
|
||||
isEditing.value = false
|
||||
if (!name) return
|
||||
if (name.length > 20) name = name.substring(0, 20)
|
||||
saveUserEQPreset({
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
hz31: appSetting['player.soundEffect.biquadFilter.hz31'],
|
||||
hz62: appSetting['player.soundEffect.biquadFilter.hz62'],
|
||||
hz125: appSetting['player.soundEffect.biquadFilter.hz125'],
|
||||
hz250: appSetting['player.soundEffect.biquadFilter.hz250'],
|
||||
hz500: appSetting['player.soundEffect.biquadFilter.hz500'],
|
||||
hz1000: appSetting['player.soundEffect.biquadFilter.hz1000'],
|
||||
hz2000: appSetting['player.soundEffect.biquadFilter.hz2000'],
|
||||
hz4000: appSetting['player.soundEffect.biquadFilter.hz4000'],
|
||||
hz8000: appSetting['player.soundEffect.biquadFilter.hz8000'],
|
||||
hz16000: appSetting['player.soundEffect.biquadFilter.hz16000'],
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.newPreset {
|
||||
position: relative;
|
||||
border: 1px dashed var(--color-primary-font-hover);
|
||||
// background-color: var(--color-main-background);
|
||||
color: var(--color-primary-font-hover);
|
||||
opacity: .7;
|
||||
height: 22px;
|
||||
|
||||
&.editing {
|
||||
opacity: 1;
|
||||
width: 90px;
|
||||
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
.newPresetInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.svg-icon {
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.newPresetInput {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// line-height: 16px;
|
||||
background: none !important;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
padding: 0 3px;
|
||||
border-radius: 0;
|
||||
display: none;
|
||||
&::placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div :class="$style.contnet">
|
||||
<h3 class="player__sound_effect_title">{{ $t('player__sound_effect_convolution') }}</h3>
|
||||
<div :class="$style.convolution">
|
||||
<div :class="$style.convolutionList">
|
||||
<base-checkbox
|
||||
v-for="item in convolutions"
|
||||
:id="`player__convolution_${item.name}`"
|
||||
:key="item.name"
|
||||
:class="$style.checkbox"
|
||||
:model-value="appSetting['player.soundEffect.convolution.fileName']"
|
||||
:label="$t(`player__sound_effect_convolution_file_${item.name}`)"
|
||||
:value="item.source"
|
||||
@update:model-value="updateConvolution($event)"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.sliderList">
|
||||
<div :class="$style.sliderItem">
|
||||
<span :class="$style.label">{{ $t('player__sound_effect_convolution_main_gain') }}</span>
|
||||
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.convolution.mainGain']" :min="0" :max="50" @change="handleUpdateMainGain" />
|
||||
<span :class="[$style.value]">{{ appSetting['player.soundEffect.convolution.mainGain'] * 10 }}%</span>
|
||||
</div>
|
||||
<div :class="$style.sliderItem">
|
||||
<span :class="$style.label">{{ $t('player__sound_effect_convolution_send_gain') }}</span>
|
||||
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.convolution.sendGain']" :min="0" :max="50" @change="handleUpdateSendGain" />
|
||||
<span :class="[$style.value]">{{ appSetting['player.soundEffect.convolution.sendGain'] * 10 }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['scroll', $style.saveList]">
|
||||
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
|
||||
<AddConvolutionPresetBtn v-if="userPresetList.length < 31" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { convolutions } from '@renderer/plugins/player'
|
||||
import AddConvolutionPresetBtn from './AddConvolutionPresetBtn'
|
||||
import { getUserConvolutionPresetList, removeUserConvolutionPreset } from '@renderer/store/soundEffect'
|
||||
|
||||
const updateConvolution = val => {
|
||||
const target = convolutions.find(c => c.source == val)
|
||||
const setting = {
|
||||
'player.soundEffect.convolution.fileName': val,
|
||||
}
|
||||
if (target) {
|
||||
setting['player.soundEffect.convolution.mainGain'] = target.mainGain * 10
|
||||
setting['player.soundEffect.convolution.sendGain'] = target.sendGain * 10
|
||||
}
|
||||
updateSetting(setting)
|
||||
}
|
||||
|
||||
const handleUpdateMainGain = (value) => {
|
||||
updateSetting({ 'player.soundEffect.convolution.mainGain': Math.round(value) })
|
||||
}
|
||||
const handleUpdateSendGain = (value) => {
|
||||
updateSetting({ 'player.soundEffect.convolution.sendGain': Math.round(value) })
|
||||
}
|
||||
|
||||
const handleSetPreset = (item) => {
|
||||
updateSetting({
|
||||
'player.soundEffect.convolution.fileName': item.source,
|
||||
'player.soundEffect.convolution.mainGain': item.mainGain,
|
||||
'player.soundEffect.convolution.sendGain': item.sendGain,
|
||||
})
|
||||
}
|
||||
const userPresetList = ref([])
|
||||
const handleRemovePreset = id => {
|
||||
removeUserConvolutionPreset(id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserConvolutionPresetList().then(list => {
|
||||
userPresetList.value = list
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.contnet {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 3px;
|
||||
min-height: 0;
|
||||
}
|
||||
.convolution {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.convolutionList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.checkbox {
|
||||
margin-right: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sliderList {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.sliderItem {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.slider {
|
||||
flex: auto;
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
// width: 50px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.value {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary-font);
|
||||
}
|
||||
}
|
||||
.saveList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
126
src/renderer/components/common/SoundEffectBtn/AudioPanner.vue
Normal file
126
src/renderer/components/common/SoundEffectBtn/AudioPanner.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div :class="$style.contnet">
|
||||
<div class="player__sound_effect_title" :class="$style.header">
|
||||
<h3>{{ $t('player__sound_effect_panner') }}</h3>
|
||||
<base-checkbox
|
||||
id="player__sound_effect_panner_enabled"
|
||||
:class="$style.checkbox"
|
||||
:label="$t('player__sound_effect_panner_enabled')"
|
||||
:model-value="appSetting['player.soundEffect.panner.enable']"
|
||||
@update:model-value="updateEnabled"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.eqList">
|
||||
<div :class="$style.eqItem">
|
||||
<span :class="$style.label">{{ $t('player__sound_effect_panner_sound_speed') }}</span>
|
||||
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.panner.speed']" :min="1" :max="50" @change="handleUpdateSpeed" />
|
||||
<span :class="[$style.value, { [$style.active]: appSetting['player.soundEffect.panner.speed'] != 25 }]">{{ appSetting['player.soundEffect.panner.speed'] }}</span>
|
||||
</div>
|
||||
<div :class="$style.eqItem">
|
||||
<span :class="$style.label">{{ $t('player__sound_effect_panner_sound_r') }}</span>
|
||||
<base-slider-bar :class="$style.slider" :value="appSetting['player.soundEffect.panner.soundR']" :min="1" :max="30" @change="handleUpdateSoundR" />
|
||||
<span :class="[$style.value, { [$style.active]: appSetting['player.soundEffect.panner.soundR'] != 5 }]">{{ appSetting['player.soundEffect.panner.soundR'] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// import { reactive } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
// const setting = reactive({
|
||||
// enabled: false,
|
||||
// soundR: 5,
|
||||
// speed: 25,
|
||||
// })
|
||||
|
||||
const updateEnabled = (enabled) => {
|
||||
// console.log(enabled)
|
||||
updateSetting({ 'player.soundEffect.panner.enable': enabled })
|
||||
}
|
||||
|
||||
const handleUpdateSoundR = (value) => {
|
||||
updateSetting({ 'player.soundEffect.panner.soundR': Math.round(value) })
|
||||
}
|
||||
const handleUpdateSpeed = (value) => {
|
||||
updateSetting({ 'player.soundEffect.panner.speed': Math.round(value) })
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.contnet {
|
||||
padding-top: 15px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 8px;
|
||||
&:before {
|
||||
.mixin-after;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
border-top: 1px dashed var(--color-primary-light-100-alpha-700);
|
||||
}
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 5px;
|
||||
// padding-top: 5px;
|
||||
}
|
||||
.eqList {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
.eqItem {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
// width: 50px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.value {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary-font);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
// justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// font-size: 13px;
|
||||
span {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
</style>
|
||||
163
src/renderer/components/common/SoundEffectBtn/BiquadFilter.vue
Normal file
163
src/renderer/components/common/SoundEffectBtn/BiquadFilter.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div :class="$style.contnet">
|
||||
<div class="player__sound_effect_title" :class="$style.header">
|
||||
<h3>{{ $t('player__sound_effect_biquad_filter') }}</h3>
|
||||
<base-btn min @click="handleReset">{{ $t('player__sound_effect_biquad_filter_reset_btn') }}</base-btn>
|
||||
</div>
|
||||
<div :class="$style.eqList">
|
||||
<div v-for="(v, i) in freqs" :key="v" :class="$style.eqItem">
|
||||
<span :class="$style.label">{{ labels[i] }}</span>
|
||||
<base-slider-bar :class="$style.slider" :value="appSetting[`player.soundEffect.biquadFilter.hz${v}`]" :min="-15" :max="15" @change="handleUpdate(v, $event)" />
|
||||
<span :class="$style.value">{{ appSetting[`player.soundEffect.biquadFilter.hz${v}`] }}db</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['scroll', $style.saveList]">
|
||||
<!-- <base-btn min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_slow`) }}</base-btn> -->
|
||||
<base-btn v-for="item in freqsPreset" :key="item.name" min @click="handleSetPreset(item)">{{ $t(`player__sound_effect_biquad_filter_preset_${item.name}`) }}</base-btn>
|
||||
<base-btn v-for="item in userPresetList" :key="item.id" min @click="handleSetPreset(item)" @contextmenu="handleRemovePreset(item.id)">{{ item.name }}</base-btn>
|
||||
<AddEQPresetBtn v-if="userPresetList.length < 31" />
|
||||
</div>
|
||||
<!-- <div :class="$style.footer">
|
||||
<base-btn min @click="handleReset">{{ $t('player__sound_effect_biquad_filter_reset_btn') }}</base-btn>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from '@common/utils/vueTools'
|
||||
import { freqs, freqsPreset } from '@renderer/plugins/player'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import AddEQPresetBtn from './AddEQPresetBtn'
|
||||
import { getUserEQPresetList, removeUserEQPreset } from '@renderer/store/soundEffect'
|
||||
|
||||
const labels = freqs.map(num => num < 1000 ? num : `${num / 1000}k`)
|
||||
|
||||
const handleUpdate = (key, value) => {
|
||||
value = Math.round(value)
|
||||
// values[index] = value
|
||||
updateSetting({ [`player.soundEffect.biquadFilter.hz${key}`]: value })
|
||||
// console.log(index, event.target.value, bfs)
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
const setting = {}
|
||||
for (const key of freqs) {
|
||||
setting[`player.soundEffect.biquadFilter.hz${key}`] = 0
|
||||
}
|
||||
updateSetting(setting)
|
||||
}
|
||||
|
||||
const handleSetPreset = (item) => {
|
||||
updateSetting({
|
||||
'player.soundEffect.biquadFilter.hz31': item.hz31,
|
||||
'player.soundEffect.biquadFilter.hz62': item.hz62,
|
||||
'player.soundEffect.biquadFilter.hz125': item.hz125,
|
||||
'player.soundEffect.biquadFilter.hz250': item.hz250,
|
||||
'player.soundEffect.biquadFilter.hz500': item.hz500,
|
||||
'player.soundEffect.biquadFilter.hz1000': item.hz1000,
|
||||
'player.soundEffect.biquadFilter.hz2000': item.hz2000,
|
||||
'player.soundEffect.biquadFilter.hz4000': item.hz4000,
|
||||
'player.soundEffect.biquadFilter.hz8000': item.hz8000,
|
||||
'player.soundEffect.biquadFilter.hz16000': item.hz16000,
|
||||
})
|
||||
}
|
||||
|
||||
const userPresetList = ref([])
|
||||
|
||||
const handleRemovePreset = id => {
|
||||
removeUserEQPreset(id)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserEQPresetList().then(list => {
|
||||
userPresetList.value = list
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.contnet {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 5px;
|
||||
// padding-top: 5px;
|
||||
}
|
||||
.eqList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
// gap: 15px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
.mixin-after;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
height: 100%;
|
||||
border-left: 1px dashed var(--color-primary-light-100-alpha-700);
|
||||
}
|
||||
}
|
||||
.eqItem {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
width: 50%;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
box-sizing: border-box;
|
||||
&:nth-child(odd) {
|
||||
padding-right: 10px;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
padding-left: 10px;
|
||||
}
|
||||
&:nth-last-child(1), &:nth-last-child(2) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.value {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
// justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// font-size: 13px;
|
||||
span {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.saveList {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
134
src/renderer/components/common/SoundEffectBtn/index.vue
Normal file
134
src/renderer/components/common/SoundEffectBtn/index.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<button :class="$style.btn" :aria-label="$t('player__sound_effect')" @click="visible = true">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="90%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-tune-variant" />
|
||||
</svg>
|
||||
</button>
|
||||
<material-modal :show="visible" bg-close="bg-close" :teleport="teleport" @close="visible = false">
|
||||
<!-- <main :class="$style.main"> -->
|
||||
<!-- <h2 :class="$style.title">{{ $t('theme_edit_modal__title') }}</h2> -->
|
||||
<div :class="$style.content">
|
||||
<div :class="$style.row">
|
||||
<AudioConvolution />
|
||||
<AudioPanner />
|
||||
</div>
|
||||
<div :class="$style.row">
|
||||
<BiquadFilter />
|
||||
</div>
|
||||
</div>
|
||||
<!-- </main> -->
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
// import useNextTogglePlay from '@renderer/utils/compositions/useNextTogglePlay'
|
||||
// import useToggleDesktopLyric from '@renderer/utils/compositions/useToggleDesktopLyric'
|
||||
// import { musicInfo, playMusicInfo } from '@renderer/store/player/state'
|
||||
// import { saveVolumeIsMute } from '@renderer/store/setting'
|
||||
// import { volume, isMute } from '@renderer/store/player/volume'
|
||||
// import fs from 'node:fs'
|
||||
import BiquadFilter from './BiquadFilter'
|
||||
import AudioPanner from './AudioPanner'
|
||||
import AudioConvolution from './AudioConvolution'
|
||||
|
||||
defineProps({
|
||||
teleport: {
|
||||
type: String,
|
||||
default: '#root',
|
||||
},
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.btn {
|
||||
position: relative;
|
||||
// color: var(--color-button-font);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: color @transition-normal;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 24px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
padding: 0;
|
||||
|
||||
svg {
|
||||
transition: opacity @transition-fast;
|
||||
opacity: .6;
|
||||
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
&:hover {
|
||||
svg {
|
||||
opacity: .9;
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
min-width: 300px;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
}
|
||||
// .title {
|
||||
// flex: none;
|
||||
// font-size: 16px;
|
||||
// color: var(--color-font);
|
||||
// line-height: 1.3;
|
||||
// text-align: center;
|
||||
// padding: 10px;
|
||||
// }
|
||||
.content {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 0 15px;
|
||||
margin: 15px 0;
|
||||
gap: 30px;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
|
||||
&:before {
|
||||
.mixin-after;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
height: 100%;
|
||||
border-left: 1px dashed var(--color-primary-light-100-alpha-700);
|
||||
}
|
||||
// width: 400px;
|
||||
|
||||
:global {
|
||||
// .player__sound_effect_contnet {
|
||||
// display: flex;
|
||||
// }
|
||||
.player__sound_effect_title {
|
||||
// margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
@update:model-value="saveVolumeIsMute($event)"
|
||||
/>
|
||||
</div>
|
||||
<base-slider-bar :value="volume" :min="0" :max="1" @change="handleUpdateVolume" />
|
||||
<base-slider-bar :class="$style.slider" :value="volume" :min="0" :max="1" @change="handleUpdateVolume" />
|
||||
</div>
|
||||
</template>
|
||||
</material-popup-btn>
|
||||
@@ -91,6 +91,7 @@ const icon = computed(() => {
|
||||
flex-flow: column nowrap;
|
||||
padding: 2px 3px;
|
||||
gap: 8px;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.info {
|
||||
@@ -104,5 +105,8 @@ const icon = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -137,8 +137,8 @@ export default {
|
||||
// padding: 18px 3px;
|
||||
// margin: 5px 0;
|
||||
// border-left: 5px solid transparent;
|
||||
transition: @transition-normal;
|
||||
transition-property: color;
|
||||
transition: @transition-fast;
|
||||
transition-property: background-color, opacity;
|
||||
color: var(--color-nav-font);
|
||||
cursor: pointer;
|
||||
font-size: 11.5px;
|
||||
@@ -148,17 +148,30 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
transition: 0.3s ease;
|
||||
transition-property: background-color, opacity;
|
||||
// border-radius: @radius-border;
|
||||
.mixin-ellipsis-1;
|
||||
&:before {
|
||||
.mixin-after;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 3px;
|
||||
height: 100%;
|
||||
background-color: var(--color-primary-dark-200-alpha-700);
|
||||
border-radius: 4px;
|
||||
transform: translateX(-100%);
|
||||
transition: transform @transition-fast;
|
||||
}
|
||||
|
||||
&.active {
|
||||
// border-left-color: @color-theme-active;
|
||||
background-color: var(--color-primary-light-400-alpha-600);
|
||||
background-color: var(--color-primary-light-300-alpha-700);
|
||||
|
||||
&:before {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-light-300-alpha-600);
|
||||
background-color: var(--color-primary-light-300-alpha-800);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,12 +181,12 @@ export default {
|
||||
|
||||
&:not(.active) {
|
||||
opacity: .8;
|
||||
background-color: var(--color-primary-light-500-alpha-600);
|
||||
background-color: var(--color-primary-light-400-alpha-700);
|
||||
}
|
||||
}
|
||||
&:active:not(.active) {
|
||||
opacity: .6;
|
||||
background-color: var(--color-primary-light-200-alpha-600);
|
||||
background-color: var(--color-primary-light-300-alpha-600);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,20 +12,20 @@
|
||||
<p><strong>5、</strong>由于使用本软件产生的包括由于本协议或由于使用或无法使用本软件而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害(包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿,或任何及所有其他商业损害或损失)由使用者负责。</p>
|
||||
<p><strong>6、</strong>本项目完全免费,且开源发布于 <span class="hover underline" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#readme')">GitHub</span> 面向全世界人用作对技术的学习交流,本软件不对项目内的技术可能存在违反当地法律法规的行为作保证,<strong>禁止在违反当地法律法规的情况下使用本软件</strong>,对于使用者在明知或不知当地法律法规不允许的情况下使用本软件所造成的任何违法违规行为由使用者承担,本软件不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。</p><br>
|
||||
<p><strong>*</strong> 若协议更新,恕不另行通知,可到开源地址查看。</p>
|
||||
<p><strong>*</strong> 本软件的初衷是帮助官方音乐平台简化数据后代为展示,帮助使用者根据歌曲名、艺术家等关键字快速地定位所需内容所在的音乐平台。</p>
|
||||
<p><strong>*</strong> 音乐平台不易,建议到对应音乐平台支持正版资源。</p><br>
|
||||
<p><strong>*</strong> 音乐平台不易,建议到对应音乐平台支持正版资源。</p>
|
||||
<p><strong>*</strong> 本项目仅用于对技术可行性的探索及研究,不接受任何商业(包括但不限于广告等)合作及捐赠。</p><br>
|
||||
<p v-if="!isAgreePact"><strong>若你(使用者)接受以上协议,请点击下面的“接受”按钮签署本协议,若不接受,请点击“不接受”后退出软件并清除本软件的所有数据。</strong></p>
|
||||
</div>
|
||||
<div v-if="!isAgreePact" :class="$style.btns">
|
||||
<base-btn :class="$style.btn" @click="handleClose(true)">{{ $t('not_agree') }}</base-btn>
|
||||
<base-btn :class="$style.btn" :disabled="!btnEnable" @click="handleClick()">{{ $t('agree') }} {{ timeStr }}</base-btn>
|
||||
<base-btn :class="$style.btn" :disabled="!btnEnable" @click="handleClick">{{ $t('agree') }} {{ timeStr }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { quitApp } from '@renderer/utils/ipc'
|
||||
import { checkUpdate, quitApp } from '@renderer/utils/ipc'
|
||||
import { openUrl } from '@common/utils/electron'
|
||||
import { isShowPact } from '@renderer/store'
|
||||
import { appSetting, saveAgreePact } from '@renderer/store/setting'
|
||||
@@ -75,6 +75,8 @@ export default {
|
||||
this.$dialog({
|
||||
message: Buffer.from('e69cace8bdafe4bbb6e5ae8ce585a8e5858de8b4b9e4b894e5bc80e6ba90efbc8ce5a682e69e9ce4bda0e698afe88ab1e992b1e8b4ade4b9b0e79a84efbc8ce8afb7e79bb4e68ea5e7bb99e5b7aee8af84efbc810a0a5468697320736f667477617265206973206672656520616e64206f70656e20736f757263652e', 'hex').toString(),
|
||||
confirmButtonText: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),
|
||||
}).then(() => {
|
||||
checkUpdate()
|
||||
})
|
||||
}, 2e3)
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from '@common/utils/vueTools'
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import usePlayProgress from '@renderer/utils/compositions/usePlayProgress'
|
||||
import { isShowPlayerDetail } from '@renderer/store/player/state'
|
||||
|
||||
@@ -28,9 +28,10 @@ export default {
|
||||
const dom_btn = ref<HTMLElement | null>(null)
|
||||
|
||||
const handleShowPopup = (evt) => {
|
||||
if (visible.value) evt.stopPropagation()
|
||||
if (visible.value) handlMsLeave()
|
||||
else handlMsEnter()
|
||||
if (visible.value) {
|
||||
evt.stopPropagation()
|
||||
handlMsLeave()
|
||||
} else handlMsEnter()
|
||||
}
|
||||
const {
|
||||
nowPlayTimeStr,
|
||||
@@ -68,12 +69,12 @@ export default {
|
||||
visibleProgress.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
visible.value = true
|
||||
requestAnimationFrame(() => {
|
||||
visible.value = false
|
||||
})
|
||||
})
|
||||
// onMounted(() => {
|
||||
// visible.value = true
|
||||
// requestAnimationFrame(() => {
|
||||
// visible.value = false
|
||||
// })
|
||||
// })
|
||||
|
||||
return {
|
||||
visible,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user