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
148 Commits
v2.0.4
...
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 | ||
|
|
067ca91c4d | ||
|
|
afd8d692fd | ||
|
|
a9a8fa2857 | ||
|
|
4b3a624ed9 | ||
|
|
549ea19c26 | ||
|
|
c0515c9c4f | ||
|
|
e81d594d01 | ||
|
|
3d469a4986 | ||
|
|
b91bd7ede0 | ||
|
|
6048af99bc | ||
|
|
ef9427122e | ||
|
|
02402f5207 | ||
|
|
80197ee18c | ||
|
|
8989b05a69 | ||
|
|
f809782559 | ||
|
|
9522e975a0 | ||
|
|
8854fb69d8 | ||
|
|
10bd75ea7b | ||
|
|
f33ffb55a7 | ||
|
|
3bff44da0c | ||
|
|
cf960e3695 | ||
|
|
c8eac2690f | ||
|
|
8af458e61a | ||
|
|
07899327f1 | ||
|
|
dd5bffe071 | ||
|
|
83000b75ca | ||
|
|
e6c3815493 | ||
|
|
32626ef773 | ||
|
|
ab1d18debe | ||
|
|
62330571a3 | ||
|
|
a8827c361c | ||
|
|
4635481c77 | ||
|
|
c6f9e421c1 | ||
|
|
448e1cd0c3 | ||
|
|
5ee7c5a319 | ||
|
|
89625188f1 | ||
|
|
fdd19068a8 | ||
|
|
ef7033cac0 | ||
|
|
65840e45cd | ||
|
|
6075f413dc | ||
|
|
ba3b2f613a | ||
|
|
165832e703 | ||
|
|
7e27ec488b | ||
|
|
4e3356ccb2 | ||
|
|
6b69cdf12f | ||
|
|
c24b29a42c | ||
|
|
c013d19530 | ||
|
|
5a1e1864d9 | ||
|
|
7c56b2fce2 | ||
|
|
7266352ca0 | ||
|
|
c1cff1e3c2 | ||
|
|
c003efe1e9 | ||
|
|
68d847aca5 | ||
|
|
82bea640c4 | ||
|
|
4fa88dd7a0 |
@@ -51,6 +51,7 @@
|
||||
"@typescript-eslint/space-before-function-paren": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"vue/max-attributes-per-line": "off",
|
||||
"vue/singleline-html-element-content-newline": "off",
|
||||
"vue/use-v-on-exact": "off",
|
||||
@@ -102,6 +103,7 @@
|
||||
}],
|
||||
"@typescript-eslint/naming-convention": "off",
|
||||
"@typescript-eslint/return-await": "off",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"multiline-ternary": "off",
|
||||
"@typescript-eslint/comma-dangle": "off",
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
@@ -4,10 +4,10 @@ module.exports = {
|
||||
'electron',
|
||||
'chalk',
|
||||
'del',
|
||||
'comlink',
|
||||
],
|
||||
// target: 'newest',
|
||||
// filter: [
|
||||
// /^vue/,
|
||||
// 'electron-builder',
|
||||
// 'electron-updater',
|
||||
// ],
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
129
CHANGELOG.md
129
CHANGELOG.md
@@ -6,6 +6,135 @@ 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
|
||||
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复处于最新版本时更新弹窗日志内容显示异常的问题
|
||||
- 修复更新到最新版本后的首次启动时的更新日志未显示的问题
|
||||
|
||||
## [2.1.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.1.0...v2.1.1) - 2023-02-18
|
||||
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复检查更新日志地址不正确的问题
|
||||
|
||||
## [2.1.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.5...v2.1.0) - 2023-02-18
|
||||
|
||||
由于软件内功能在设计时只考虑简单便捷性,是否对新手友好并不是我们考虑的重点,功能的新增、变更会在更新日志中注明,不会在软件内做指引提示,
|
||||
因此为了更愉快地使用本软件,我们建议在使用新版本时阅读一遍更新日志以了解软件的变更情况,同时若遇到问题可以去阅读常见问题找解决方案
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增桌面歌词设置字体加粗设置,可以到设置-桌面歌词设置-加粗字体修改
|
||||
- 新增是否自动下载更新设置,默认开启,可以去设置-软件更新更改
|
||||
- 新增当前版本更新日志显示弹窗(建议大家阅读更新日志以了解当前版本的变化),在更新版本后将自动弹出
|
||||
- 新增是否在更新版本的首次启动时显示更新日志弹窗设置,默认开启,可以去设置-软件更新更改
|
||||
- 新增播放速率调整功能,可以去播放详情页的控制按钮调整,范围限制为x0.5至x2之间(#13)
|
||||
- 添加wy、tx源(感谢某位不愿透露姓名的大佬提供的C++算法源码,但由于作者不希望公开,所以将会以预构建二进制文件的形式加入代码仓库中)逐字歌词的支持
|
||||
- 新增设置-下载设置-是否嵌入翻译歌词、罗马音歌词设置,默认关闭
|
||||
- 添加启动时的数据库表及表结构完整性校验,若未通过校验,则会显示弹窗提示后将该数据库重命名添加`.bak`后缀后重建数据库启动。对于某些人遇到更新到v2.0.0后出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表的问题,可以通过此特性自动重建数据库并重新迁移数据,不再需要手动去数据目录删除数据库
|
||||
|
||||
### 优化
|
||||
|
||||
- 微调了桌面歌词逐行字体阴影,使其看起来更匀称
|
||||
- 调整了桌面歌词在启用滚动到顶部时的距离,现在滚动到顶部的歌词更靠边,不再受字体大小、歌词间距影响
|
||||
- 优化更新弹窗内容的显示,添加了自动更新失败时的更新指引
|
||||
- 为所有文本输入框添加右键快速粘贴的功能,右击输入框可以自动粘贴剪贴板的文字,若选中文字时将粘贴并替换选中文字
|
||||
- 防止桌面歌词窗口在屏幕分辨率变小时,窗口位置跟随分辨率变化的问题,现在若屏幕分辨率变小后窗口位置仍会在原始分辨率的位置(添加这个机制是为了解决屏幕分辨率被临时调整时的位置更新问题,如运行某些低分辨率的全屏游戏、高分辨率外接屏幕休眠时),但若你的分辨率调整不是临时的,因窗口在原始位置导致看不到窗口可以开关桌面歌词即可重新自动调节回屏幕内
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复播放下载列表的歌曲时,调整歌词偏移时间功能异常的问题
|
||||
- 修复较旧Linux arm64系统下无法启动软件的问题(将预构建模块的所需glibc版本降级到2.28)(#1161)
|
||||
- 修改列表响应式更新机制,尝试修复偶现的删除歌曲列表未更新的问题
|
||||
- 修复某些kg歌单链接无法打开的问题
|
||||
- 修复将桌面歌词放到屏幕边缘时,偶现的开启桌面歌词后出现歌词窗口位置出现少许偏移的问题,以及将歌词窗口调整到全屏大小后,重开桌面歌词窗口被缩小出现边距的问题
|
||||
|
||||
### 其他
|
||||
|
||||
- 更新Electron到v22.3.0
|
||||
|
||||
## [2.0.5](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.4...v2.0.5) - 2023-01-18
|
||||
|
||||
这应该是LX今年的最后一个版本,提前祝大家新年快乐~😘
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复声音输出设备更改时后的自动暂停播放设置无效的问题
|
||||
- 重写桌面歌词窗口坐标的计算逻辑,修复桌面歌词移动到最边缘时,某些情况下在启用歌词后会出现窗口偏移的问题(远古bug了)
|
||||
- 修复随机播放模式下使用稍后播放功能播放我的列表的歌曲时,切换下一曲永远是当前歌曲的问题(#1147)
|
||||
- 修复macOS下的软件系统菜单中的退出功能不会完全退出软件的问题(#1148)
|
||||
|
||||
## [2.0.4](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.3...v2.0.4) - 2023-01-15
|
||||
|
||||
|
||||
|
||||
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>
|
||||
|
||||
@@ -3,32 +3,64 @@ const fsPromises = require('fs').promises
|
||||
const path = require('path')
|
||||
const { Arch } = require('electron-builder')
|
||||
|
||||
const fileNameMap = {
|
||||
[Arch.arm64]: 'arm64',
|
||||
[Arch.armv7l]: 'armv7l',
|
||||
const better_sqlite3_fileNameMap = {
|
||||
[Arch.x64]: 'electron-v110-linux-x64',
|
||||
[Arch.arm64]: 'electron-v110-linux-arm64',
|
||||
[Arch.armv7l]: 'electron-v110-linux-arm',
|
||||
}
|
||||
|
||||
const qrc_decode_fileNameMap = {
|
||||
win32: {
|
||||
[Arch.x64]: 'electron-v110-win32-x64',
|
||||
[Arch.ia32]: 'electron-v110-win32-ia32',
|
||||
[Arch.arm64]: 'electron-v110-win32-arm64',
|
||||
},
|
||||
linux: {
|
||||
[Arch.x64]: 'electron-v110-linux-x64',
|
||||
[Arch.arm64]: 'electron-v110-linux-arm64',
|
||||
[Arch.armv7l]: 'electron-v110-linux-arm',
|
||||
},
|
||||
darwin: {
|
||||
[Arch.x64]: 'electron-v110-darwin-x64',
|
||||
[Arch.arm64]: 'electron-v110-darwin-arm64',
|
||||
},
|
||||
}
|
||||
|
||||
const replaceSqliteLib = async(arch) => {
|
||||
// console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))
|
||||
// if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return
|
||||
// https://github.com/lyswhut/lx-music-desktop/issues/1102
|
||||
// https://github.com/lyswhut/lx-music-desktop/issues/1161
|
||||
console.log('replace sqlite lib...')
|
||||
const filePath = path.join(__dirname, `./lib/better_sqlite3.linux.${fileNameMap[arch]}.node`)
|
||||
const filePath = path.join(__dirname, `./lib/better_sqlite3_${better_sqlite3_fileNameMap[arch]}.node`)
|
||||
const targetPath = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')
|
||||
await fsPromises.unlink(targetPath).catch(_ => _)
|
||||
await fsPromises.copyFile(filePath, targetPath)
|
||||
}
|
||||
|
||||
const replaceQrcDecodeLib = async(platform, arch) => {
|
||||
console.log('replace qrc_decode lib...', platform, qrc_decode_fileNameMap[platform][arch])
|
||||
const filePath = path.join(__dirname, `./lib/qrc_decode_${qrc_decode_fileNameMap[platform][arch]}.node`)
|
||||
const targetPath = path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
const targetDir = path.dirname(targetPath)
|
||||
if (fs.existsSync(targetDir)) await fsPromises.unlink(targetPath).catch(_ => _)
|
||||
else await fsPromises.mkdir(targetDir, { recursive: true })
|
||||
await fsPromises.copyFile(filePath, targetPath)
|
||||
}
|
||||
|
||||
|
||||
module.exports = async(context) => {
|
||||
const { electronPlatformName, arch } = context
|
||||
if (electronPlatformName !== 'linux') return
|
||||
await replaceQrcDecodeLib(electronPlatformName, arch)
|
||||
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)) {
|
||||
console.log('rename binding file...')
|
||||
// console.log('rename binding file...')
|
||||
await fsPromises.rename(bindingFilePath, bindingBakFilePath)
|
||||
}
|
||||
await replaceSqliteLib(arch)
|
||||
@@ -36,7 +68,7 @@ module.exports = async(context) => {
|
||||
|
||||
default:
|
||||
if (fs.existsSync(bindingFilePath)) return
|
||||
console.log('restore binding file...')
|
||||
// console.log('restore binding file...')
|
||||
await fsPromises.rename(bindingBakFilePath, bindingFilePath)
|
||||
break
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
build-config/lib/better_sqlite3_electron-v110-linux-arm.node
Normal file
BIN
build-config/lib/better_sqlite3_electron-v110-linux-arm.node
Normal file
Binary file not shown.
BIN
build-config/lib/better_sqlite3_electron-v110-linux-arm64.node
Normal file
BIN
build-config/lib/better_sqlite3_electron-v110-linux-arm64.node
Normal file
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.
BIN
build-config/lib/qrc_decode_electron-v110-darwin-arm64.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-darwin-arm64.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-darwin-x64.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-darwin-x64.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-linux-arm.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-linux-arm.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-linux-arm64.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-linux-arm64.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-linux-x64.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-linux-x64.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-win32-arm64.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-win32-arm64.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-win32-ia32.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-win32-ia32.node
Normal file
Binary file not shown.
BIN
build-config/lib/qrc_decode_electron-v110-win32-x64.node
Normal file
BIN
build-config/lib/qrc_decode_electron-v110-win32-x64.node
Normal file
Binary file not shown.
@@ -1,6 +1,8 @@
|
||||
const path = require('path')
|
||||
const ESLintPlugin = require('eslint-webpack-plugin')
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
target: 'electron-main',
|
||||
output: {
|
||||
@@ -10,12 +12,13 @@ module.exports = {
|
||||
},
|
||||
path: path.join(__dirname, '../../dist'),
|
||||
},
|
||||
externals: [
|
||||
'font-list',
|
||||
'better-sqlite3',
|
||||
'bufferutil',
|
||||
'utf-8-validate',
|
||||
],
|
||||
externals: {
|
||||
'font-list': 'font-list',
|
||||
'better-sqlite3': 'better-sqlite3',
|
||||
bufferutil: 'bufferutil',
|
||||
'utf-8-validate': 'utf-8-validate',
|
||||
'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@main': path.join(__dirname, '../../src/main'),
|
||||
|
||||
2
build-config/post-install.js
Normal file
2
build-config/post-install.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const { Arch } = require('electron-builder')
|
||||
require('./build-before-pack')({ electronPlatformName: process.platform, arch: Arch[process.arch] })
|
||||
@@ -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: {
|
||||
|
||||
@@ -14,6 +14,8 @@ const mainConfig = require('./main/webpack.config.dev')
|
||||
const rendererConfig = require('./renderer/webpack.config.dev')
|
||||
const rendererLyricConfig = require('./renderer-lyric/webpack.config.dev')
|
||||
const rendererScriptConfig = require('./renderer-scripts/webpack.config.dev')
|
||||
const { Arch } = require('electron-builder')
|
||||
const replaceLib = require('./build-before-pack')
|
||||
|
||||
let electronProcess = null
|
||||
let manualRestart = false
|
||||
@@ -224,6 +226,7 @@ function init() {
|
||||
function handleFail(name) {
|
||||
spinners.fail(name, { text: name + ' compile fail!' })
|
||||
}
|
||||
replaceLib({ electronPlatformName: process.platform, arch: Arch[process.arch] })
|
||||
|
||||
Promise.all([
|
||||
startRenderer().then(() => handleSuccess('renderer')).catch((err) => {
|
||||
|
||||
6020
package-lock.json
generated
6020
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
107
package.json
107
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lx-music-desktop",
|
||||
"version": "2.0.4",
|
||||
"version": "2.3.0-beta.3",
|
||||
"description": "一个免费的音乐查找助手",
|
||||
"main": "./dist/main.js",
|
||||
"productName": "lx-music-desktop",
|
||||
@@ -65,13 +65,13 @@
|
||||
"build:renderer-scripts": "cross-env NODE_ENV=production webpack --config build-config/renderer-scripts/webpack.config.prod.js --progress",
|
||||
"build": "npm run clean:electron && npm run build:main && npm run build:renderer && npm run build:renderer-lyric && npm run build:renderer-scripts",
|
||||
"lint": "eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly src",
|
||||
"lint:fix": "eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly --fix src",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"lint:fix": "eslint --ext .js,.vue -f node_modules/eslint-formatter-friendly --fix src",
|
||||
"dp": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:1081 npm run pack",
|
||||
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:1081 npm i"
|
||||
},
|
||||
"browserslist": [
|
||||
"Electron 19.1.0"
|
||||
"Electron 22.3.0"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 16",
|
||||
@@ -100,6 +100,7 @@
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/bufferutil",
|
||||
"node_modules/utf-8-validate",
|
||||
"build/Release/qrc_decode.node",
|
||||
"dist/**/*"
|
||||
],
|
||||
"asar": {
|
||||
@@ -194,105 +195,106 @@
|
||||
"electron-app",
|
||||
"vuejs3"
|
||||
],
|
||||
"author": "lyswhut <lyswhut@qq.com>",
|
||||
"author": {
|
||||
"name": "lyswhut",
|
||||
"email": "lyswhut@qq.com"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/lyswhut/lx-music-desktop/issues"
|
||||
},
|
||||
"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.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"@volar/vue-language-plugin-pug": "^1.0.24",
|
||||
"@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.4",
|
||||
"browserslist": "^4.21.5",
|
||||
"chalk": "^4.1.2",
|
||||
"changelog-parser": "^3.0.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"core-js": "^3.27.1",
|
||||
"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": "^19.1.9",
|
||||
"electron-builder": "^24.0.0-alpha.10",
|
||||
"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.284",
|
||||
"electron-updater": "^6.0.0-alpha.7",
|
||||
"eslint": "^8.32.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": "^27.0.1",
|
||||
"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.4",
|
||||
"eslint-plugin-n": "^15.6.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^15.7.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-vue": "^9.9.0",
|
||||
"eslint-webpack-plugin": "^3.2.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.0.5",
|
||||
"rimraf": "^5.0.0",
|
||||
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"svg-transform-loader": "^2.0.13",
|
||||
"svgo-loader": "^3.0.3",
|
||||
"terser": "^5.16.1",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"svgo-loader": "^4.0.0",
|
||||
"terser": "^5.17.1",
|
||||
"terser-webpack-plugin": "^5.3.8",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.4",
|
||||
"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.0.1",
|
||||
"better-sqlite3": "^8.3.0",
|
||||
"bufferutil": "^4.0.7",
|
||||
"comlink": "^4.3.1",
|
||||
"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.5.4",
|
||||
"node-id3": "^0.2.6",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tunnel": "^0.0.6",
|
||||
"utf-8-validate": "^6.0.0",
|
||||
"vue": "^3.2.45",
|
||||
"vue-router": "^4.1.6"
|
||||
"utf-8-validate": "^6.0.3",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"overrides": {
|
||||
"got": "^11",
|
||||
@@ -303,6 +305,7 @@
|
||||
},
|
||||
"svg-baker": {
|
||||
"postcss": "latest"
|
||||
}
|
||||
},
|
||||
"http-cache-semantics": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
### 新增
|
||||
|
||||
### 修复
|
||||
- 新增音效设置(实验性功能),支持10段均衡器设置、内置的一些环境混响音效、3D立体环绕音效
|
||||
|
||||
- 修复备份文件导入指引无法识别v2配置的问题
|
||||
- 修复从搜索界面进入歌单详情后,若启用强迫症设置的清空功能会导致意外清空搜索框、搜索列表的问题
|
||||
- 修复桌面歌词在启用卡拉OK歌词后字体边缘可能被截断的问题(特别是纵向歌词某些字的边角被截断导致后面的阴影露出来或阴影不均匀的问题)
|
||||
- 修复桌面歌词启用歌词缩放后的阴影显示问题
|
||||
- 修复Linux armv7l系统(如树莓派)下无法启动的问题(与修复Linux arm64的方法一样采用内置预编译模块的方式修复)
|
||||
- 修复备份与恢复的列表导入列表信息设置逻辑问题与潜在导入问题
|
||||
### 其他
|
||||
|
||||
- 更新 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 = {
|
||||
@@ -38,6 +39,8 @@ export const DATA_KEYS = {
|
||||
leaderboardSetting: 'leaderboardSetting',
|
||||
songListSetting: 'songListSetting',
|
||||
searchSetting: 'searchSetting',
|
||||
|
||||
lastStartInfo: 'lastStartInfo',
|
||||
} as const
|
||||
|
||||
export const DEFAULT_SETTING = {
|
||||
@@ -73,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,8 +17,10 @@ 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,
|
||||
|
||||
'player.startupAutoPlay': false,
|
||||
'player.togglePlayMethod': 'listLoop',
|
||||
@@ -23,17 +28,34 @@ const defaultSetting: LX.AppSetting = {
|
||||
'player.isShowTaskProgess': true,
|
||||
'player.volume': 1,
|
||||
'player.isMute': false,
|
||||
'player.playbackRate': 1,
|
||||
'player.mediaDeviceId': 'default',
|
||||
'player.isMediaDeviceRemovedStopPlay': false,
|
||||
'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,
|
||||
@@ -46,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,
|
||||
@@ -58,14 +81,17 @@ const defaultSetting: LX.AppSetting = {
|
||||
'desktopLyric.style.align': 'center',
|
||||
'desktopLyric.style.font': '',
|
||||
'desktopLyric.style.fontSize': 20,
|
||||
'desktopLyric.style.lineGap': 14,
|
||||
'desktopLyric.style.lineGap': 15,
|
||||
'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',
|
||||
'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',
|
||||
'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.15)',
|
||||
'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.18)',
|
||||
// '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': true,
|
||||
'desktopLyric.style.isFontWeightExtended': true,
|
||||
|
||||
'list.isClickPlayList': false,
|
||||
'list.isShowSource': true,
|
||||
@@ -84,6 +110,8 @@ const defaultSetting: LX.AppSetting = {
|
||||
'download.lrcFormat': 'utf8',
|
||||
'download.isEmbedPic': true,
|
||||
'download.isEmbedLyric': false,
|
||||
'download.isEmbedLyricT': false,
|
||||
'download.isEmbedLyricR': false,
|
||||
'download.isUseOtherSource': false,
|
||||
|
||||
'search.isShowHotSearch': false,
|
||||
@@ -100,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',
|
||||
@@ -117,7 +148,7 @@ const defaultSetting: LX.AppSetting = {
|
||||
// 使用新年皮肤
|
||||
if (new Date().getMonth() < 2) {
|
||||
defaultSetting['theme.id'] = 'happy_new_year'
|
||||
defaultSetting['desktopLyric.style.lyricPlayedColor'] = 'rgba(255, 18, 34, 1)'
|
||||
defaultSetting['desktopLyric.style.lyricPlayedColor'] = 'rgba(255, 57, 71, 1)'
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ const modules = {
|
||||
change_tray: 'change_tray',
|
||||
quit_update: 'quit_update',
|
||||
update_check: 'update_check',
|
||||
update_download_update: 'update_download_update',
|
||||
update_available: 'update_available',
|
||||
update_error: 'update_error',
|
||||
update_progress: 'update_progress',
|
||||
@@ -72,6 +73,7 @@ const modules = {
|
||||
// lang_s2t: 'lang_s2t',
|
||||
|
||||
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
|
||||
handle_tx_decode_lyric: 'handle_tx_decode_lyric',
|
||||
get_lyric_info: 'get_lyric_info',
|
||||
set_lyric_info: 'set_lyric_info',
|
||||
set_config: 'set_config',
|
||||
@@ -88,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',
|
||||
|
||||
142
src/common/types/app_setting.d.ts
vendored
142
src/common/types/app_setting.d.ts
vendored
@@ -68,6 +68,16 @@ declare global {
|
||||
*/
|
||||
'common.playBarProgressStyle': 'mini' | 'full' | 'middle'
|
||||
|
||||
/**
|
||||
* 尝试自动更新
|
||||
*/
|
||||
'common.tryAutoUpdate': boolean
|
||||
|
||||
/**
|
||||
* 更新版本后是否显示变更日志
|
||||
*/
|
||||
'common.showChangeLog': boolean
|
||||
|
||||
/**
|
||||
* 启动时自动播放歌曲
|
||||
*/
|
||||
@@ -98,6 +108,11 @@ declare global {
|
||||
*/
|
||||
'player.isMute': boolean
|
||||
|
||||
/**
|
||||
* 播放速率
|
||||
*/
|
||||
'player.playbackRate': number
|
||||
|
||||
/**
|
||||
* 音频输出设备id
|
||||
*/
|
||||
@@ -148,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
|
||||
|
||||
/**
|
||||
* 是否启用音频加载失败时自动切歌
|
||||
*/
|
||||
@@ -204,6 +299,11 @@ declare global {
|
||||
*/
|
||||
'desktopLyric.audioVisualization': boolean
|
||||
|
||||
/**
|
||||
* 是否在全屏时隐藏歌词
|
||||
*/
|
||||
'desktopLyric.fullscreenHide': boolean
|
||||
|
||||
/**
|
||||
* 桌面歌词窗口宽度
|
||||
*/
|
||||
@@ -304,6 +404,21 @@ declare global {
|
||||
*/
|
||||
'desktopLyric.style.isZoomActiveLrc': boolean
|
||||
|
||||
/**
|
||||
* 是否加粗逐字歌词字体
|
||||
*/
|
||||
'desktopLyric.style.isFontWeightFont': boolean
|
||||
|
||||
/**
|
||||
* 是否加粗逐行歌词字体
|
||||
*/
|
||||
'desktopLyric.style.isFontWeightLine': boolean
|
||||
|
||||
/**
|
||||
* 是否加粗翻译、罗马音字体
|
||||
*/
|
||||
'desktopLyric.style.isFontWeightExtended': boolean
|
||||
|
||||
/**
|
||||
* 是否启用双击列表里的歌曲时自动切换到当前列表播放(仅对歌单、排行榜有效)
|
||||
*/
|
||||
@@ -384,6 +499,16 @@ declare global {
|
||||
*/
|
||||
'download.isEmbedLyric': boolean
|
||||
|
||||
/**
|
||||
* 是否在音频文件中嵌入翻译歌词
|
||||
*/
|
||||
'download.isEmbedLyricT': boolean
|
||||
|
||||
/**
|
||||
* 是否在音频文件中嵌入罗马音歌词
|
||||
*/
|
||||
'download.isEmbedLyricR': boolean
|
||||
|
||||
/**
|
||||
* 歌曲源不可用时,是否启用换源下载
|
||||
*/
|
||||
@@ -459,6 +584,11 @@ declare global {
|
||||
*/
|
||||
'tray.themeId': number
|
||||
|
||||
/**
|
||||
* 同步服务模式
|
||||
*/
|
||||
'sync.mode': 'server' | 'client'
|
||||
|
||||
/**
|
||||
* 是否启用同步服务
|
||||
*/
|
||||
@@ -467,7 +597,17 @@ declare global {
|
||||
/**
|
||||
* 同步服务端口号
|
||||
*/
|
||||
'sync.port': '23332' | string
|
||||
'sync.server.port': '23332' | string
|
||||
|
||||
/**
|
||||
* 最大备份快照数
|
||||
*/
|
||||
'sync.server.maxSsnapshotNum': number
|
||||
|
||||
/**
|
||||
* 同步服务地址
|
||||
*/
|
||||
'sync.client.host': string
|
||||
|
||||
/**
|
||||
* 是否在离开搜索界面时自动清空搜索框
|
||||
|
||||
1
src/common/types/common.d.ts
vendored
1
src/common/types/common.d.ts
vendored
@@ -102,6 +102,7 @@ declare namespace LX {
|
||||
prev: boolean
|
||||
}
|
||||
|
||||
type UpdateStatus = 'downloaded' | 'downloading' | 'error' | 'checking' | 'idle'
|
||||
interface VersionInfo {
|
||||
version: string
|
||||
desc: string
|
||||
|
||||
9
src/common/types/desktop_lyric.d.ts
vendored
9
src/common/types/desktop_lyric.d.ts
vendored
@@ -26,11 +26,15 @@ declare namespace LX {
|
||||
// 'desktopLyric.style.fontWeight': LX.AppSetting['desktopLyric.style.fontWeight']
|
||||
'desktopLyric.style.opacity': LX.AppSetting['desktopLyric.style.opacity']
|
||||
'desktopLyric.style.ellipsis': LX.AppSetting['desktopLyric.style.ellipsis']
|
||||
'desktopLyric.style.isFontWeightFont': LX.AppSetting['desktopLyric.style.isFontWeightFont']
|
||||
'desktopLyric.style.isFontWeightLine': LX.AppSetting['desktopLyric.style.isFontWeightLine']
|
||||
'desktopLyric.style.isFontWeightExtended': LX.AppSetting['desktopLyric.style.isFontWeightExtended']
|
||||
'desktopLyric.style.isZoomActiveLrc': LX.AppSetting['desktopLyric.style.isZoomActiveLrc']
|
||||
'common.langId': LX.AppSetting['common.langId']
|
||||
'player.isShowLyricTranslation': LX.AppSetting['player.isShowLyricTranslation']
|
||||
'player.isShowLyricRoma': LX.AppSetting['player.isShowLyricRoma']
|
||||
'player.isPlayLxlrc': LX.AppSetting['player.isPlayLxlrc']
|
||||
'player.playbackRate': LX.AppSetting['player.playbackRate']
|
||||
}
|
||||
|
||||
type WinMainActions = 'get_info' | 'get_status' | 'get_analyser_data_array'
|
||||
@@ -69,6 +73,7 @@ declare namespace LX {
|
||||
lxlrc: string | null
|
||||
}>
|
||||
| LyricAction<'set_offset', number>
|
||||
| LyricAction<'set_playbackRate', number>
|
||||
| LyricAction<'set_play', number>
|
||||
| LyricAction<'set_pause'>
|
||||
| LyricAction<'set_stop'>
|
||||
@@ -76,8 +81,8 @@ declare namespace LX {
|
||||
|
||||
|
||||
interface NewBounds {
|
||||
x?: number | null
|
||||
y?: number
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
109
src/common/types/download_list.d.ts
vendored
109
src/common/types/download_list.d.ts
vendored
@@ -1,66 +1,69 @@
|
||||
import { type Message } from '@/lang'
|
||||
|
||||
// interface DownloadList {
|
||||
|
||||
// }
|
||||
|
||||
|
||||
declare namespace LX {
|
||||
namespace Download {
|
||||
type DownloadTaskStatus = 'run'
|
||||
| 'waiting'
|
||||
| 'pause'
|
||||
| 'error'
|
||||
| 'completed'
|
||||
declare global {
|
||||
namespace LX {
|
||||
namespace Download {
|
||||
type DownloadTaskStatus = 'run'
|
||||
| 'waiting'
|
||||
| 'pause'
|
||||
| 'error'
|
||||
| 'completed'
|
||||
|
||||
type FileExt = 'mp3' | 'flac' | 'wav' | 'ape'
|
||||
type FileExt = 'mp3' | 'flac' | 'wav' | 'ape'
|
||||
|
||||
interface ProgressInfo {
|
||||
progress: number
|
||||
speed: string
|
||||
downloaded: number
|
||||
total: number
|
||||
}
|
||||
|
||||
interface DownloadTaskActionBase <A> {
|
||||
action: A
|
||||
}
|
||||
interface DownloadTaskActionData<A, D> extends DownloadTaskActionBase<A> {
|
||||
data: D
|
||||
}
|
||||
type DownloadTaskAction<A, D = undefined> = D extends undefined ? DownloadTaskActionBase<A> : DownloadTaskActionData<A, D>
|
||||
|
||||
type DownloadTaskActions = DownloadTaskAction<'start'>
|
||||
| DownloadTaskAction<'complete'>
|
||||
| DownloadTaskAction<'refreshUrl'>
|
||||
| DownloadTaskAction<'statusText', string>
|
||||
| DownloadTaskAction<'progress', ProgressInfo>
|
||||
| DownloadTaskAction<'error', {
|
||||
error?: string
|
||||
message?: string
|
||||
}>
|
||||
|
||||
interface ListItem {
|
||||
id: string
|
||||
isComplate: boolean
|
||||
status: DownloadTaskStatus
|
||||
statusText: string
|
||||
downloaded: number
|
||||
total: number
|
||||
progress: number
|
||||
speed: string
|
||||
metadata: {
|
||||
musicInfo: LX.Music.MusicInfoOnline
|
||||
url: string | null
|
||||
quality: LX.Quality
|
||||
ext: FileExt
|
||||
fileName: string
|
||||
filePath: string
|
||||
interface ProgressInfo {
|
||||
progress: number
|
||||
speed: string
|
||||
downloaded: number
|
||||
total: number
|
||||
}
|
||||
}
|
||||
|
||||
interface saveDownloadMusicInfo {
|
||||
list: ListItem[]
|
||||
addMusicLocationType: LX.AddMusicLocationType
|
||||
interface DownloadTaskActionBase <A> {
|
||||
action: A
|
||||
}
|
||||
interface DownloadTaskActionData<A, D> extends DownloadTaskActionBase<A> {
|
||||
data: D
|
||||
}
|
||||
type DownloadTaskAction<A, D = undefined> = D extends undefined ? DownloadTaskActionBase<A> : DownloadTaskActionData<A, D>
|
||||
|
||||
type DownloadTaskActions = DownloadTaskAction<'start'>
|
||||
| DownloadTaskAction<'complete'>
|
||||
| DownloadTaskAction<'refreshUrl'>
|
||||
| DownloadTaskAction<'statusText', string>
|
||||
| DownloadTaskAction<'progress', ProgressInfo>
|
||||
| DownloadTaskAction<'error', {
|
||||
error?: keyof Message
|
||||
message?: string
|
||||
}>
|
||||
|
||||
interface ListItem {
|
||||
id: string
|
||||
isComplate: boolean
|
||||
status: DownloadTaskStatus
|
||||
statusText: string
|
||||
downloaded: number
|
||||
total: number
|
||||
progress: number
|
||||
speed: string
|
||||
metadata: {
|
||||
musicInfo: LX.Music.MusicInfoOnline
|
||||
url: string | null
|
||||
quality: LX.Quality
|
||||
ext: FileExt
|
||||
fileName: string
|
||||
filePath: string
|
||||
}
|
||||
}
|
||||
|
||||
interface saveDownloadMusicInfo {
|
||||
list: ListItem[]
|
||||
addMusicLocationType: LX.AddMusicLocationType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
|
||||
|
||||
2
src/common/types/music_metadata.d.ts
vendored
2
src/common/types/music_metadata.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
IAudioMetadata as iAudioMetadata,
|
||||
type IAudioMetadata as iAudioMetadata,
|
||||
} from 'music-metadata'
|
||||
|
||||
declare global {
|
||||
|
||||
2
src/common/types/shims_vue.d.ts
vendored
2
src/common/types/shims_vue.d.ts
vendored
@@ -4,7 +4,7 @@
|
||||
// }
|
||||
|
||||
declare module '*.vue' {
|
||||
import { Component } from 'vue'
|
||||
import { type Component } from 'vue'
|
||||
const component: Component
|
||||
export default component
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import path from 'path'
|
||||
import { EventEmitter } from 'events'
|
||||
import { performance } from 'perf_hooks'
|
||||
import { STATUS } from './util'
|
||||
import http from 'http'
|
||||
import { request, Options as RequestOptions } from './request'
|
||||
import type http from 'http'
|
||||
import { request, type Options as RequestOptions } from './request'
|
||||
|
||||
export interface Options {
|
||||
forceResume: boolean
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Downloader, { Options as DownloaderOptions } from './Downloader'
|
||||
import Downloader, { type Options as DownloaderOptions } from './Downloader'
|
||||
import { getRequestAgent } from './util'
|
||||
import { sizeFormate } from '@common/utils'
|
||||
import http from 'http'
|
||||
import type http from 'http'
|
||||
|
||||
// these are the default options
|
||||
// const options = {
|
||||
|
||||
@@ -28,6 +28,7 @@ const createAnimation = (dom, duration, isVertical) => new window.Animation(new
|
||||
module.exports = class FontPlayer {
|
||||
constructor({
|
||||
time = 0,
|
||||
rate = 1,
|
||||
lyric = '',
|
||||
lineContentClassName = 'line-content',
|
||||
lineClassName = 'line',
|
||||
@@ -43,6 +44,8 @@ module.exports = class FontPlayer {
|
||||
this.time = time
|
||||
this.lyric = lyric
|
||||
|
||||
this._rate = rate
|
||||
|
||||
this.isVertical = isVertical
|
||||
|
||||
this.lineContentClassName = lineContentClassName
|
||||
@@ -136,7 +139,7 @@ module.exports = class FontPlayer {
|
||||
|
||||
const dom = document.createElement('span')
|
||||
dom.textContent = text
|
||||
const animation = createAnimation(dom, time, this.isVertical)
|
||||
const animation = createAnimation(dom, time / this._rate, this.isVertical)
|
||||
this.lrcContent.appendChild(dom)
|
||||
// lineText += text
|
||||
|
||||
@@ -186,7 +189,7 @@ module.exports = class FontPlayer {
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
return (getNow() - this._performanceTime) * this._rate + this._startTime
|
||||
}
|
||||
|
||||
_findcurFontNum(curTime, startIndex = 0) {
|
||||
@@ -201,7 +204,7 @@ module.exports = class FontPlayer {
|
||||
const currentTime = this._currentTime()
|
||||
const driftTime = currentTime - curFont.startTime
|
||||
if (currentTime > curFont.startTime + curFont.time) {
|
||||
this._handlePlayFont(curFont, driftTime, true)
|
||||
this._handlePlayFont(curFont, driftTime / this._rate, true)
|
||||
this.lineContent.classList.add('played')
|
||||
this.isPlay = false
|
||||
this.pause()
|
||||
@@ -257,13 +260,13 @@ module.exports = class FontPlayer {
|
||||
|
||||
if (driftTime >= 0 || this.curFontNum == 0) {
|
||||
let nextFont = this.fonts[this.curFontNum + 1]
|
||||
this.delay = nextFont.startTime - curFont.startTime - driftTime
|
||||
if (this.delay > 0) {
|
||||
const delay = (nextFont.startTime - curFont.startTime - driftTime) / this._rate
|
||||
if (delay > 0) {
|
||||
if (this.isPlay) {
|
||||
this.timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}, delay)
|
||||
}
|
||||
this._handlePlayFont(curFont, driftTime)
|
||||
return
|
||||
@@ -342,6 +345,13 @@ module.exports = class FontPlayer {
|
||||
this.curFontNum = this.maxFontNum
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this._rate = rate
|
||||
if (!this.lines.length) return
|
||||
if (!this.isPlay) return
|
||||
this.play(this._currentTime())
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.pause()
|
||||
if (this.isLineMode) return this._handlePlayLine(false)
|
||||
|
||||
@@ -8,6 +8,7 @@ module.exports = class Lyric {
|
||||
lyric = '',
|
||||
extendedLyrics = [],
|
||||
offset = 0,
|
||||
rate = 1,
|
||||
lineContentClassName = 'line-content',
|
||||
lineClassName = 'line',
|
||||
shadowClassName = 'shadow',
|
||||
@@ -25,6 +26,7 @@ module.exports = class Lyric {
|
||||
this.lyric = lyric
|
||||
this.extendedLyrics = extendedLyrics
|
||||
this.offset = offset
|
||||
this.rate = rate
|
||||
this.onPlay = onPlay
|
||||
this.onSetLyric = onSetLyric
|
||||
this.onUpdateLyric = onUpdateLyric
|
||||
@@ -50,6 +52,7 @@ module.exports = class Lyric {
|
||||
|
||||
this.linePlayer = new LinePlayer({
|
||||
offset: this.offset,
|
||||
rate: this.rate,
|
||||
onPlay: this._handleLinePlayerOnPlay,
|
||||
onSetLyric: this._handleLinePlayerOnSetLyric,
|
||||
})
|
||||
@@ -116,6 +119,7 @@ module.exports = class Lyric {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
rate: this.rate,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineContentClassName: this.lineContentClassName,
|
||||
@@ -141,6 +145,7 @@ module.exports = class Lyric {
|
||||
this._lines = lyricLines.map(line => {
|
||||
const fontPlayer = new FontPlayer({
|
||||
time: line.time,
|
||||
rate: this.rate,
|
||||
lyric: line.text,
|
||||
extendedLyrics: line.extendedLyrics,
|
||||
lineContentClassName: this.lineContentClassName,
|
||||
@@ -200,6 +205,17 @@ module.exports = class Lyric {
|
||||
this._init()
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this.rate = rate
|
||||
this.linePlayer.setPlaybackRate(rate)
|
||||
this._initLines(this.initInfo.lines, this.initInfo.offset, true)
|
||||
if (this.linePlayer.isPlay) {
|
||||
const num = this.playingLineNum
|
||||
this.playingLineNum = 0
|
||||
this._handleLinePlayerOnPlay(num, '', this.linePlayer._currentTime())
|
||||
} else this.playingLineNum = 0
|
||||
}
|
||||
|
||||
setVertical(isVertical) {
|
||||
this.isVertical = isVertical
|
||||
this._initLines(this.initInfo.lines, this.initInfo.offset, true)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { getNow, TimeoutTools } = require('./utils')
|
||||
|
||||
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
|
||||
const timeExp = /[\d:.]+/g
|
||||
const timeExp = /\d{1,3}(:\d{1,3}){0,2}(?:\.\d{1,3})/g
|
||||
const tagRegMap = {
|
||||
title: 'ti',
|
||||
artist: 'ar',
|
||||
@@ -12,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++) {
|
||||
@@ -24,8 +33,7 @@ const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (let time of times) {
|
||||
if (!time.includes('.')) time += '.0'
|
||||
const timeStr = time.replace(/(?:\.0+|0+)$/, '')
|
||||
const timeStr = formatTimeLabel(time)
|
||||
const targetLine = lrcLinesMap[timeStr]
|
||||
if (targetLine) targetLine.extendedLyrics.push(text)
|
||||
}
|
||||
@@ -35,7 +43,7 @@ const parseExtendedLyric = (lrcLinesMap, extendedLyric) => {
|
||||
}
|
||||
|
||||
module.exports = class LinePlayer {
|
||||
constructor({ offset = 0, onPlay = function() { }, onSetLyric = function() { } } = {}) {
|
||||
constructor({ offset = 0, rate = 1, onPlay = function() { }, onSetLyric = function() { } } = {}) {
|
||||
this.tags = {}
|
||||
this.lines = null
|
||||
this.onPlay = onPlay
|
||||
@@ -46,6 +54,7 @@ module.exports = class LinePlayer {
|
||||
this.offset = offset
|
||||
this._performanceTime = 0
|
||||
this._startTime = 0
|
||||
this._rate = rate
|
||||
}
|
||||
|
||||
_init() {
|
||||
@@ -84,18 +93,16 @@ module.exports = class LinePlayer {
|
||||
const times = timeField.match(timeExp)
|
||||
if (times == null) continue
|
||||
for (let time of times) {
|
||||
if (!time.includes('.')) time += '.0'
|
||||
const timeStr = time.replace(/(?:\.0+|0+)$/, '')
|
||||
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,
|
||||
@@ -115,7 +122,7 @@ module.exports = class LinePlayer {
|
||||
}
|
||||
|
||||
_currentTime() {
|
||||
return getNow() - this._performanceTime + this._startTime
|
||||
return (getNow() - this._performanceTime) * this._rate + this._startTime
|
||||
}
|
||||
|
||||
_findCurLineNum(curTime, startIndex = 0) {
|
||||
@@ -142,14 +149,14 @@ module.exports = class LinePlayer {
|
||||
|
||||
if (driftTime >= 0 || this.curLineNum === 0) {
|
||||
let nextLine = this.lines[this.curLineNum + 1]
|
||||
this.delay = nextLine.time - curLine.time - driftTime
|
||||
const delay = (nextLine.time - curLine.time - driftTime) / this._rate
|
||||
|
||||
if (this.delay > 0) {
|
||||
if (delay > 0) {
|
||||
if (this.isPlay) {
|
||||
timeoutTools.start(() => {
|
||||
if (!this.isPlay) return
|
||||
this._refresh()
|
||||
}, this.delay)
|
||||
}, delay)
|
||||
}
|
||||
this.onPlay(this.curLineNum, curLine.text, currentTime)
|
||||
return
|
||||
@@ -191,6 +198,13 @@ module.exports = class LinePlayer {
|
||||
}
|
||||
}
|
||||
|
||||
setPlaybackRate(rate) {
|
||||
this._rate = rate
|
||||
if (!this.lines.length) return
|
||||
if (!this.isPlay) return
|
||||
this.play(this._currentTime())
|
||||
}
|
||||
|
||||
setLyric(lyric, extendedLyrics) {
|
||||
// console.log(extendedLyrics)
|
||||
if (this.isPlay) this.pause()
|
||||
|
||||
@@ -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, // 歌曲图片链接
|
||||
}
|
||||
@@ -120,7 +120,7 @@ export const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => {
|
||||
}
|
||||
|
||||
export const filterMusicList = <T extends LX.Music.MusicInfo>(list: T[]): T[] => {
|
||||
const ids: Set<string> = new Set()
|
||||
const ids = new Set<string>()
|
||||
return list.filter(s => {
|
||||
if (!s.id || ids.has(s.id) || !s.name) return false
|
||||
if (s.singer == null) s.singer = ''
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
shallowRef,
|
||||
unref,
|
||||
markRaw,
|
||||
ComputedRef,
|
||||
Ref,
|
||||
ShallowRef,
|
||||
type ComputedRef,
|
||||
type Ref,
|
||||
type ShallowRef,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
defineComponent,
|
||||
|
||||
@@ -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",
|
||||
@@ -64,6 +65,7 @@
|
||||
"download_status_error_check_path": "There is an error in checking the download path, please check whether the set download directory is normal",
|
||||
"download_status_error_check_path_exist": "A file with the same name exists, skip download",
|
||||
"download_status_error_refresh_url": "Link is dead, link is being refreshed",
|
||||
"download_status_error_response": "download failed:",
|
||||
"download_status_error_url_failed": "Failed to get music link",
|
||||
"download_status_error_write": "The song save location is occupied or does not have write permission, please try to change the song save directory or restart the software or restart the computer, the error details:",
|
||||
"download_status_start": "start download",
|
||||
@@ -91,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",
|
||||
@@ -204,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: ",
|
||||
@@ -218,9 +223,46 @@
|
||||
"player__play_toggle_mode_off": "Disable",
|
||||
"player__play_toggle_mode_random": "List Random",
|
||||
"player__play_toggle_mode_single_loop": "Single Loop",
|
||||
"player__playback_rate": "Current playback rate:",
|
||||
"player__playback_rate_reset_btn": "Reset",
|
||||
"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",
|
||||
@@ -307,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",
|
||||
@@ -323,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",
|
||||
@@ -333,6 +380,8 @@
|
||||
"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",
|
||||
"setting__download_lyric_format": "Downloaded lyrics file encoding format",
|
||||
@@ -394,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 🤪)",
|
||||
@@ -434,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",
|
||||
@@ -446,14 +498,28 @@
|
||||
"setting__search_focus_search_box": "Automatically focus the search box on startup",
|
||||
"setting__search_history": "Search history",
|
||||
"setting__search_hot": "Top Searches",
|
||||
"setting__setting__desktop_lyric_font_weight_extended": "translation, romanized lyrics",
|
||||
"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...",
|
||||
@@ -462,9 +528,13 @@
|
||||
"setting__update_init": "Processing update...",
|
||||
"setting__update_latest": "The software is up-to-date, enjoy yourself!🥂",
|
||||
"setting__update_latest_label": "Latest version: ",
|
||||
"setting__update_open_version_modal_btn": "Open the update window🚀",
|
||||
"setting__update_progress": "Download progress: ",
|
||||
"setting__update_new_version": "Found a new version, hurry up and update~🚀🚀",
|
||||
"setting__update_open_version_modal_btn": "open update window",
|
||||
"setting__update_progress": "state:",
|
||||
"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",
|
||||
@@ -492,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",
|
||||
@@ -499,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",
|
||||
@@ -567,6 +640,14 @@
|
||||
"theme_selector_modal__title_tip": "Note: You can set a light theme and a dark theme in advance, and then it will automatically switch to the corresponding theme you set in advance according to the light and dark theme colors of the system.",
|
||||
"toggle_source_failed": "Failed to change the source, please try to manually search for the song in other sources to play",
|
||||
"toggle_source_try": "Try switching to another source...",
|
||||
"update__downgrade_tip": "We found that you have downgraded the version ({ver}). If you encounter problems when using the new version, please try to read the FAQ first. If the problem you encounter is not recorded in the FAQ or cannot be solved, you can refer to \nGo to the feedback channel to give us feedback 😘!\nNote: When downgrading from the new version to the old version, it is recommended to back up the playlist first. If there is an exception, it can be solved by cleaning the data. The data directory path document has records.",
|
||||
"update__error_top": "The automatic download of the new version failed, you can try to download the update again or manually download the update,\nThe address of the new version is written under the update pop-up window. Download the new version and overwrite the installation directly. If the installation fails, see FAQs.\nNote: Currently only the Windows installation version can be automatically updated (Linux AppImage, deb version seems to be also available, not tested), other versions please download and update manually!",
|
||||
"update__ignore_cancel": "I don't want to update 🤨",
|
||||
"update__ignore_confirm": "Ok, let's update it ❤️",
|
||||
"update__ignore_confirm_tip": "At present, only the Windows installation version can be automatically updated (Linux AppImage, deb version seems to be also available, not tested), other versions please download and update manually,\nThe address of the new version is written under the update pop-up window. Download the new version and overwrite the installation directly. If the installation fails, see FAQ.",
|
||||
"update__ignore_confirm_tip_confirm": "OK understood",
|
||||
"update__ignore_tip": "The version you are using now is behind the latest version by {num} versions🤪, for a better user experience, it is recommended to update to the latest version~!\nNote: If you encounter problems when using the new version, please try to read the FAQ first. If the problem you encounter is not recorded in the FAQ or cannot be solved, you can give us feedback through the feedback channels mentioned in the document 😘!",
|
||||
"update__timeout_top": "Download time is too long prompt\n\nYour current network access to GitHub is slow, and the new version has been downloaded for an hour and has not been completed yet😳, you can still choose to continue waiting, but the wall crack recommends manually updating the version!",
|
||||
"user_api__allow_show_update_alert": "Allow update popup to show",
|
||||
"user_api__btn_export": "Export",
|
||||
"user_api__btn_import": "Import",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, ref } from 'vue'
|
||||
import { type App, ref } from 'vue'
|
||||
import { messages } from './index'
|
||||
import type { Messages, Message } from './index'
|
||||
|
||||
@@ -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": "最新评论",
|
||||
@@ -64,6 +65,7 @@
|
||||
"download_status_error_check_path": "检查下载路径出错,请检查设置的下载目录是否正常",
|
||||
"download_status_error_check_path_exist": "存在同名文件,跳过下载",
|
||||
"download_status_error_refresh_url": "链接失效,正在刷新链接",
|
||||
"download_status_error_response": "下载失败:",
|
||||
"download_status_error_url_failed": "获取音乐链接失败",
|
||||
"download_status_error_write": "歌曲保存位置被占用或没有写入权限,请尝试更改歌曲保存目录或重启软件或重启电脑,错误详情:",
|
||||
"download_status_start": "开始下载",
|
||||
@@ -91,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": "暂停任务",
|
||||
@@ -204,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": "专辑名:",
|
||||
@@ -218,9 +223,45 @@
|
||||
"player__play_toggle_mode_off": "禁用",
|
||||
"player__play_toggle_mode_random": "列表随机",
|
||||
"player__play_toggle_mode_single_loop": "单曲循环",
|
||||
"player__playback_rate": "当前播放速率:",
|
||||
"player__playback_rate_reset_btn": "重置",
|
||||
"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": "静音",
|
||||
@@ -318,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": "加大间距",
|
||||
@@ -325,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": "顶部",
|
||||
@@ -335,6 +379,8 @@
|
||||
"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": "歌词下载",
|
||||
"setting__download_lyric_format": "下载的歌词文件编码格式",
|
||||
@@ -396,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": "这将清理所有你之前已调整过偏移时间的歌词,是否确认清理?(手抖确认🤪)",
|
||||
@@ -436,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": "记住播放进度",
|
||||
@@ -448,14 +497,28 @@
|
||||
"setting__search_focus_search_box": "启动时自动聚焦搜索框",
|
||||
"setting__search_history": "显示历史搜索记录",
|
||||
"setting__search_hot": "显示热门搜索",
|
||||
"setting__setting__desktop_lyric_font_weight_extended": "翻译、罗马音歌词",
|
||||
"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": "检查更新中...",
|
||||
@@ -464,9 +527,13 @@
|
||||
"setting__update_init": "处理更新中...",
|
||||
"setting__update_latest": "软件已是最新,尽情地体验吧~🥂",
|
||||
"setting__update_latest_label": "最新版本:",
|
||||
"setting__update_open_version_modal_btn": "打开更新窗口 🚀",
|
||||
"setting__update_progress": "下载进度:",
|
||||
"setting__update_new_version": "发现新版本,赶快去更新吧~🚀🚀",
|
||||
"setting__update_open_version_modal_btn": "打开更新窗口",
|
||||
"setting__update_progress": "状态:",
|
||||
"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": "打开歌单",
|
||||
@@ -494,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": "合并",
|
||||
@@ -501,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": "本机列表 覆盖 远程列表",
|
||||
@@ -511,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",
|
||||
@@ -569,6 +639,14 @@
|
||||
"theme_selector_modal__title_tip": "注:你可以预先设置一个亮色主题及暗色主题,此后将根据系统的亮、暗主题色自动切换为你预先设置的相应主题。",
|
||||
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
|
||||
"toggle_source_try": "尝试切换到其他源...",
|
||||
"update__downgrade_tip": "我们发现你降级了版本({ver}),若使用新版本时遇到问题,请先尝试阅读常见问题解决,若你遇到的问题在常见问题中未记录或无法解决,可以通过文档中提到的反馈渠道给我们反馈😘!\n注意:从新版本降级旧版时建议先备份歌单,若出现异常则可通过清理数据解决,数据目录路径文档有记录。",
|
||||
"update__error_top": "自动下载新版本失败,你可以尝试重新下载更新或者手动去下载更新,\n新版地址在更新弹窗下面有写,下载新版直接覆盖安装即可,若安装失败则看常见问题解决,\n注意:目前只有Windows安装版可以自动更新(Linux的AppImage、deb版似乎也可以,未测试),其他版本请手动下载更新!",
|
||||
"update__ignore_cancel": "我就不想更新🤨",
|
||||
"update__ignore_confirm": "好 去更新看看❤️",
|
||||
"update__ignore_confirm_tip": "目前只有Windows安装版可以自动更新(Linux的AppImage、deb版似乎也可以,未测试),其他版本请手动下载更新,\n新版地址在更新弹窗下面有写,下载新版直接覆盖安装即可,若安装失败看常见问题解决。",
|
||||
"update__ignore_confirm_tip_confirm": "OK 已了解",
|
||||
"update__ignore_tip": "你现在使用的版本距离最新版本已经落后了 {num} 个版本🤪,为了更好的使用体验,建议更新到最新版本哦~!\n注:若使用新版本时遇到问题,请先尝试阅读常见问题解决,若你遇到的问题在常见问题中未记录或无法解决,可以通过文档中提到的反馈渠道给我们反馈😘!",
|
||||
"update__timeout_top": "下载时间过长提示\n\n你当前所在网络访问GitHub较慢,新版本已经下了一个钟了还没完成😳,你仍可选择继续等,但墙裂建议手动更新版本!",
|
||||
"user_api__allow_show_update_alert": "允许显示更新弹窗",
|
||||
"user_api__btn_export": "导出",
|
||||
"user_api__btn_import": "导入",
|
||||
|
||||
@@ -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": "最新評論",
|
||||
@@ -64,6 +65,7 @@
|
||||
"download_status_error_check_path": "檢查下載路徑出錯,請檢查設置的下載目錄是否正常",
|
||||
"download_status_error_check_path_exist": "存在同名文件,跳過下載",
|
||||
"download_status_error_refresh_url": "鏈接失效,正在刷新鏈接",
|
||||
"download_status_error_response": "下載失敗:",
|
||||
"download_status_error_url_failed": "獲取音樂鏈接失敗",
|
||||
"download_status_error_write": "歌曲保存位置被佔用或沒有寫入權限,請嘗試更改歌曲保存目錄或重啟軟件或重啟電腦,錯誤詳情:",
|
||||
"download_status_start": "開始下載",
|
||||
@@ -91,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 +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": "歌曲名:",
|
||||
@@ -218,9 +223,46 @@
|
||||
"player__play_toggle_mode_off": "禁用",
|
||||
"player__play_toggle_mode_random": "列表隨機",
|
||||
"player__play_toggle_mode_single_loop": "單曲循環",
|
||||
"player__playback_rate": "當前播放速率:",
|
||||
"player__playback_rate_reset_btn": "重置",
|
||||
"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": "靜音",
|
||||
@@ -307,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": "歌詞顯示方向",
|
||||
@@ -317,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": "加大間距",
|
||||
@@ -324,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": "頂部",
|
||||
@@ -334,6 +380,8 @@
|
||||
"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": "歌詞下載",
|
||||
"setting__download_lyric_format": "下載的歌詞文件編碼格式",
|
||||
@@ -395,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(手抖確認🤪)",
|
||||
@@ -435,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": "記住播放進度",
|
||||
@@ -447,14 +498,28 @@
|
||||
"setting__search_focus_search_box": "啟動時自動聚焦搜索框",
|
||||
"setting__search_history": "顯示歷史搜索記錄",
|
||||
"setting__search_hot": "顯示熱門搜索",
|
||||
"setting__setting__desktop_lyric_font_weight_extended": "翻譯、羅馬音歌詞",
|
||||
"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": "檢查更新中...",
|
||||
@@ -463,9 +528,13 @@
|
||||
"setting__update_init": "處理更新中...",
|
||||
"setting__update_latest": "軟件已是最新,盡情地體驗吧~🥂",
|
||||
"setting__update_latest_label": "最新版本:",
|
||||
"setting__update_open_version_modal_btn": "打開更新窗口 🚀",
|
||||
"setting__update_progress": "下載進度:",
|
||||
"setting__update_new_version": "發現新版本,趕快去更新吧~🚀🚀",
|
||||
"setting__update_open_version_modal_btn": "打開更新窗口",
|
||||
"setting__update_progress": "狀態:",
|
||||
"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",
|
||||
@@ -492,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": "合併",
|
||||
@@ -499,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": "本機列表 覆蓋 遠程列表",
|
||||
@@ -509,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",
|
||||
@@ -567,6 +639,14 @@
|
||||
"theme_selector_modal__title_tip": "注:你可以預先設置一個亮色主題及暗色主題,此後將根據系統的亮、暗主題色自動切換為你預先設置的相應主題。",
|
||||
"toggle_source_failed": "換源失敗,請嘗試手動在其他源搜索該歌曲播放",
|
||||
"toggle_source_try": "嘗試切換到其他源...",
|
||||
"update__downgrade_tip": "我們發現你降級了版本({ver}),若使用新版本時遇到問題,請先嘗試閱讀常見問題解決,若你遇到的問題在常見問題中未記錄或無法解決,可以通過文檔中提到的反饋渠道給我們反饋😘!\n注意:從新版本降級舊版時建議先備份歌單,若出現異常則可通過清理數據解決,數據目錄路徑文檔有記錄。",
|
||||
"update__error_top": "自動下載新版本失敗,你可以嘗試重新下載更新或者手動去下載更新,\n新版地址在更新彈窗下面有寫,下載新版直接覆蓋安裝即可,若安裝失敗則看常見問題解決,\n注意:目前只有Windows安裝版可以自動更新(Linux的AppImage、deb版似乎也可以,未測試),其他版本請手動下載更新!",
|
||||
"update__ignore_cancel": "我就不想更新🤨",
|
||||
"update__ignore_confirm": "好 去更新看看❤️",
|
||||
"update__ignore_confirm_tip": "目前只有Windows安裝版可以自動更新(Linux的AppImage、deb版似乎也可以,未測試),其他版本請手動下載更新,\n新版地址在更新彈窗下面有寫,下載新版直接覆蓋安裝即可,若安裝失敗看常見問題解決。",
|
||||
"update__ignore_confirm_tip_confirm": "OK 已了解",
|
||||
"update__ignore_tip": "你現在使用的版本距離最新版本已經落後了 {num} 個版本🤪,為了更好的使用體驗,建議更新到最新版本哦~!\n注:若使用新版本時遇到問題,請先嘗試閱讀常見問題解決,若你遇到的問題在常見問題中未記錄或無法解決,可以通過文檔中提到的反饋渠道給我們反饋😘!",
|
||||
"update__timeout_top": "下載時間過長提示\n\n你當前所在網絡訪問GitHub較慢,新版本已經下了一個鐘了還沒完成😳,你仍可選擇繼續等,但牆裂建議手動更新版本!",
|
||||
"user_api__allow_show_update_alert": "允許顯示更新彈窗",
|
||||
"user_api__btn_export": "導出",
|
||||
"user_api__btn_import": "導入",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join, dirname } from 'path'
|
||||
import { existsSync, mkdirSync } from 'fs'
|
||||
import { app, shell, screen, nativeTheme } from 'electron'
|
||||
import { existsSync, mkdirSync, renameSync } from 'fs'
|
||||
import { app, shell, screen, nativeTheme, dialog } from 'electron'
|
||||
import { URL_SCHEME_RXP } from '@common/constants'
|
||||
import { getTheme, initHotKey, initSetting, parseEnvParams } from './utils'
|
||||
import { navigationUrlWhiteList } from '@common/config'
|
||||
@@ -10,6 +10,7 @@ import { createAppEvent, createListEvent } from '@main/event'
|
||||
import { isMac, log } from '@common/utils'
|
||||
import createWorkers from './worker'
|
||||
import { migrateDBData } from './utils/migrate'
|
||||
import { openDirInExplorer } from '@common/utils/electron'
|
||||
|
||||
export const initGlobalData = () => {
|
||||
global.isDev = process.env.NODE_ENV !== 'production'
|
||||
@@ -237,7 +238,18 @@ export const initAppSetting = async() => {
|
||||
}
|
||||
|
||||
if (!isInitialized) {
|
||||
const dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)
|
||||
let dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)
|
||||
if (dbFileExists === null) {
|
||||
const backPath = join(global.lxDataPath, `lx.data.db.${Date.now()}.bak`)
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
message: 'Database verify failed',
|
||||
detail: `数据库表结构校验失败,我们将把有问题的数据库备份到:${backPath}\n若此问题导致你的数据丢失,你可以尝试从备份文件找回它们。\n\nThe database table structure verification failed, we will back up the problematic database to: ${backPath}\nIf this problem causes your data to be lost, you can try to retrieve them from the backup file.`,
|
||||
})
|
||||
renameSync(join(global.lxDataPath, 'lx.data.db'), backPath)
|
||||
openDirInExplorer(backPath)
|
||||
dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)
|
||||
}
|
||||
global.lx.appSetting = (await initSetting()).setting
|
||||
if (!dbFileExists) await migrateDBData().catch(err => { log.error(err) })
|
||||
initTheme()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Event as App, Type as AppType } from './AppEvent'
|
||||
import { Event as List, Type as ListType } from './ListEvent'
|
||||
import { Event as App, type Type as AppType } from './AppEvent'
|
||||
import { Event as List, type Type as ListType } from './ListEvent'
|
||||
|
||||
export type {
|
||||
AppType,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { app, Menu } from 'electron'
|
||||
import { isMac } from '@common/utils'
|
||||
import { quitApp } from '@main/app'
|
||||
|
||||
|
||||
export default () => {
|
||||
@@ -17,7 +16,8 @@ export default () => {
|
||||
label: '退出',
|
||||
accelerator: 'Command+Q',
|
||||
click() {
|
||||
quitApp()
|
||||
global.lx.isSkipTrayQuit = true
|
||||
app.quit()
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
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 http from 'http'
|
||||
import { SYNC_CODE } from './config'
|
||||
import {
|
||||
aesEncrypt,
|
||||
aesDecrypt,
|
||||
createClientKeyInfo,
|
||||
getClientKeyInfo,
|
||||
setClientKeyInfo,
|
||||
rsaEncrypt,
|
||||
} from './utils'
|
||||
import type http from 'http'
|
||||
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, Socket } from 'socket.io'
|
||||
import { createHttpTerminator, 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 { 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: Map<string, Promise<void>> = new Map()
|
||||
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
|
||||
@@ -136,7 +132,7 @@ const handleMergeList = (
|
||||
newList = [...sourceList, ...targetList]
|
||||
break
|
||||
}
|
||||
const map: Map<string | number, LX.Music.MusicInfo> = new Map()
|
||||
const map = new Map<string | number, LX.Music.MusicInfo>()
|
||||
const ids: Array<string | number> = []
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
@@ -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 = (
|
||||
@@ -313,9 +295,9 @@ const mergeListDataFromSnapshot = (
|
||||
snapshotList: LX.Music.MusicInfo[],
|
||||
addMusicLocationType: LX.AddMusicLocationType,
|
||||
): LX.Music.MusicInfo[] => {
|
||||
const removedListIds: Set<string | number> = new Set()
|
||||
const sourceListItemIds: Set<string | number> = new Set()
|
||||
const targetListItemIds: Set<string | number> = new Set()
|
||||
const removedListIds = new Set<string | number>()
|
||||
const sourceListItemIds = new Set<string | number>()
|
||||
const targetListItemIds = new Set<string | number>()
|
||||
for (const m of sourceList) sourceListItemIds.add(m.id)
|
||||
for (const m of targetList) targetListItemIds.add(m.id)
|
||||
if (snapshotList) {
|
||||
@@ -325,7 +307,7 @@ const mergeListDataFromSnapshot = (
|
||||
}
|
||||
|
||||
let newList
|
||||
const map: Map<string | number, LX.Music.MusicInfo> = new Map()
|
||||
const map = new Map<string | number, LX.Music.MusicInfo>()
|
||||
const ids = []
|
||||
switch (addMusicLocationType) {
|
||||
case 'top':
|
||||
@@ -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 = {
|
||||
@@ -362,9 +353,9 @@ const handleMergeListDataFromSnapshot = async(socket: LX.Sync.Socket, snapshot:
|
||||
const localUserListData = createUserListDataObj(localListData)
|
||||
const remoteUserListData = createUserListDataObj(remoteListData)
|
||||
const snapshotUserListData = createUserListDataObj(snapshot)
|
||||
const removedListIds: Set<string | number> = new Set()
|
||||
const localUserListIds: Set<string | number> = new Set()
|
||||
const remoteUserListIds: Set<string | number> = new Set()
|
||||
const removedListIds = new Set<string | number>()
|
||||
const localUserListIds = new Set<string | number>()
|
||||
const remoteUserListIds = new Set<string | number>()
|
||||
|
||||
for (const l of localListData.userList) localUserListIds.add(l.id)
|
||||
for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
|
||||
@@ -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 { 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
|
||||
}
|
||||
@@ -52,6 +52,7 @@ export const createWindow = async(userApi: LX.UserApi.UserApiInfo) => {
|
||||
// worldSafeExecuteJavaScript: true,
|
||||
nodeIntegration: false,
|
||||
nodeIntegrationInWorker: false,
|
||||
sandbox: false,
|
||||
|
||||
spellcheck: false,
|
||||
autoplayPolicy: 'document-user-activation-required',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ import { sendShowUpdateAlert, sendStatusChange } from '@main/modules/winMain'
|
||||
let userApi: LX.UserApi.UserApiInfo
|
||||
let apiStatus: LX.UserApi.UserApiStatus = { status: true }
|
||||
const requestQueue = new Map()
|
||||
const timeouts: Map<string, NodeJS.Timeout> = new Map()
|
||||
const timeouts = new Map<string, NodeJS.Timeout>()
|
||||
interface InitParams {
|
||||
params: {
|
||||
status: boolean
|
||||
|
||||
@@ -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
|
||||
@@ -55,13 +55,21 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
|
||||
isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']
|
||||
if (global.lx.appSetting['desktopLyric.isLockScreen']) {
|
||||
setBounds(getLyricWindowBounds(getBounds(), {
|
||||
x: null,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: global.lx.appSetting['desktopLyric.width'],
|
||||
h: global.lx.appSetting['desktopLyric.height'],
|
||||
}))
|
||||
}
|
||||
}
|
||||
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, offset } from './utils'
|
||||
import { initWindowSize } from './utils'
|
||||
import { mainSend } from '@common/mainIpc'
|
||||
import { encodePath } from '@common/utils/electron'
|
||||
|
||||
@@ -9,10 +9,11 @@ import { encodePath } from '@common/utils/electron'
|
||||
// require('./rendererEvent')
|
||||
|
||||
let browserWindow: Electron.BrowserWindow | null = null
|
||||
let isWinBoundsUpdateing = false
|
||||
|
||||
|
||||
const setLyricsConfig = debounce((config: Partial<LX.AppSetting>) => {
|
||||
const saveBoundsConfig = debounce((config: Partial<LX.AppSetting>) => {
|
||||
global.lx.event_app.update_config(config)
|
||||
if (isWinBoundsUpdateing) isWinBoundsUpdateing = false
|
||||
}, 500)
|
||||
|
||||
const winEvent = () => {
|
||||
@@ -31,21 +32,31 @@ const winEvent = () => {
|
||||
|
||||
browserWindow.on('move', () => {
|
||||
// bounds = browserWindow.getBounds()
|
||||
// console.log(bounds)
|
||||
const bounds = browserWindow!.getBounds()
|
||||
setLyricsConfig({
|
||||
'desktopLyric.x': bounds.x,
|
||||
'desktopLyric.y': bounds.y,
|
||||
'desktopLyric.width': bounds.width,
|
||||
'desktopLyric.height': bounds.height,
|
||||
})
|
||||
// console.log('move', isWinBoundsUpdateing)
|
||||
if (isWinBoundsUpdateing) {
|
||||
const bounds = browserWindow!.getBounds()
|
||||
saveBoundsConfig({
|
||||
'desktopLyric.x': bounds.x,
|
||||
'desktopLyric.y': bounds.y,
|
||||
'desktopLyric.width': bounds.width,
|
||||
'desktopLyric.height': bounds.height,
|
||||
})
|
||||
} else {
|
||||
// 非主动调整窗口触发的窗口位置变化将重置回设置值
|
||||
browserWindow!.setBounds({
|
||||
x: global.lx.appSetting['desktopLyric.x'] ?? 0,
|
||||
y: global.lx.appSetting['desktopLyric.y'] ?? 0,
|
||||
width: global.lx.appSetting['desktopLyric.width'],
|
||||
height: global.lx.appSetting['desktopLyric.height'],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
browserWindow.on('resize', () => {
|
||||
// bounds = browserWindow.getBounds()
|
||||
// console.log(bounds)
|
||||
const bounds = browserWindow!.getBounds()
|
||||
setLyricsConfig({
|
||||
saveBoundsConfig({
|
||||
'desktopLyric.x': bounds.x,
|
||||
'desktopLyric.y': bounds.y,
|
||||
'desktopLyric.width': bounds.width,
|
||||
@@ -66,10 +77,11 @@ const winEvent = () => {
|
||||
browserWindow!.setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })
|
||||
}
|
||||
// linux下每次重开时貌似要重新设置置顶
|
||||
if (isLinux && global.lx.appSetting['desktopLyric.isAlwaysOnTop']) {
|
||||
browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')
|
||||
}
|
||||
// if (isLinux && global.lx.appSetting['desktopLyric.isAlwaysOnTop']) {
|
||||
// browserWindow!.setAlwaysOnTop(global.lx.appSetting['desktopLyric.isAlwaysOnTop'], 'screen-saver')
|
||||
// }
|
||||
if (global.lx.appSetting['desktopLyric.isAlwaysOnTop'] && global.lx.appSetting['desktopLyric.isAlwaysOnTopLoop']) alwaysOnTopTools.startLoop()
|
||||
browserWindow!.blur()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,20 +93,16 @@ export const createWindow = () => {
|
||||
let width = global.lx.appSetting['desktopLyric.width']
|
||||
let height = global.lx.appSetting['desktopLyric.height']
|
||||
let isAlwaysOnTop = global.lx.appSetting['desktopLyric.isAlwaysOnTop']
|
||||
let isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']
|
||||
// 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) {
|
||||
x = screenWidth - width + offset
|
||||
y = screenHeight - height + offset
|
||||
}
|
||||
if (isLockScreen) {
|
||||
let bounds = getLyricWindowBounds({ x, y, width, height }, { x: null, y: 0, w: width, h: height })
|
||||
x = bounds.x
|
||||
y = bounds.y
|
||||
width = bounds.width
|
||||
height = bounds.height
|
||||
}
|
||||
// let { width: screenWidth, height: screenHeight } = global.envParams.workAreaSize
|
||||
const winSize = initWindowSize(x, y, width, height)
|
||||
global.lx.event_app.update_config({
|
||||
'desktopLyric.x': winSize.x,
|
||||
'desktopLyric.y': winSize.y,
|
||||
'desktopLyric.width': winSize.width,
|
||||
'desktopLyric.height': winSize.height,
|
||||
})
|
||||
|
||||
const { shouldUseDarkColors, theme } = global.lx.theme
|
||||
|
||||
@@ -102,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,
|
||||
@@ -161,6 +169,7 @@ export const getBounds = (): Electron.Rectangle => {
|
||||
|
||||
export const setBounds = (bounds: Electron.Rectangle) => {
|
||||
if (!browserWindow) return
|
||||
isWinBoundsUpdateing = true
|
||||
browserWindow.setBounds(bounds)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +1,52 @@
|
||||
// 设置窗口位置、大小
|
||||
let winX
|
||||
let winY
|
||||
let wasW
|
||||
let wasH
|
||||
export const offset = 8
|
||||
let minWidth = 80
|
||||
let minHeight = 50
|
||||
export const padding = 8
|
||||
export let minWidth = 80
|
||||
export let minHeight = 50
|
||||
|
||||
|
||||
// const updateBounds = (bounds: Bounds) => {
|
||||
// bounds.x = bounds.x
|
||||
// return bounds
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
* @param bounds 当前设置
|
||||
* @param param 新设置(相对于当前设置)
|
||||
* @returns
|
||||
*/
|
||||
export const getLyricWindowBounds = (bounds: Electron.Rectangle, { x = 0, y = 0, w = 0, h = 0 }: LX.DesktopLyric.NewBounds): Electron.Rectangle => {
|
||||
if (w < minWidth) w = minWidth
|
||||
if (h < minHeight) h = minHeight
|
||||
|
||||
if (global.lx.appSetting['desktopLyric.isLockScreen']) {
|
||||
if (!global.envParams.workAreaSize) return bounds
|
||||
wasW = (global.envParams.workAreaSize.width ?? 0) + offset
|
||||
wasH = (global.envParams.workAreaSize.height ?? 0) + offset
|
||||
const maxWinW = global.envParams.workAreaSize.width + padding * 2
|
||||
const maxWinH = global.envParams.workAreaSize.height + padding * 2
|
||||
|
||||
if (w > wasW + offset) w = wasW + offset
|
||||
if (h > wasH + offset) h = wasH + offset
|
||||
if (x == null) {
|
||||
if (bounds.x > wasW - w) {
|
||||
x = wasW - w - bounds.x
|
||||
} else if (bounds.x < -offset) {
|
||||
x = bounds.x + offset
|
||||
} else {
|
||||
x = 0
|
||||
}
|
||||
if (bounds.y > wasH - h) {
|
||||
y = wasH - h - bounds.y
|
||||
} else if (bounds.y < -offset) {
|
||||
y = bounds.y + offset
|
||||
} else {
|
||||
y = 0
|
||||
}
|
||||
}
|
||||
winX = bounds.x + x
|
||||
winY = bounds.y + y
|
||||
if (w > maxWinW) w = maxWinW
|
||||
if (h > maxWinH) h = maxWinH
|
||||
|
||||
if (x != 0) {
|
||||
if (winX < -offset) {
|
||||
winX = -offset
|
||||
} else if (winX + w > wasW) {
|
||||
winX = wasW - w
|
||||
}
|
||||
}
|
||||
if (y != 0) {
|
||||
if (winY < -offset) {
|
||||
winY = -offset
|
||||
} else if (winY + h > wasH) {
|
||||
winY = wasH - h
|
||||
}
|
||||
}
|
||||
const maxX = global.envParams.workAreaSize.width + padding - w
|
||||
const minX = -padding
|
||||
const maxY = global.envParams.workAreaSize.height + padding - h
|
||||
const minY = -padding
|
||||
|
||||
x = winX
|
||||
y = winY
|
||||
x += bounds.x
|
||||
y += bounds.y
|
||||
|
||||
if (x + w > wasW) w = wasW - x
|
||||
if (y + h > wasH) h = wasH - y
|
||||
if (x > maxX) x = maxX
|
||||
else if (x < minX) x = minX
|
||||
|
||||
if (y > maxY) y = maxY
|
||||
else if (y < minY) y = minY
|
||||
} else {
|
||||
if (x == null) {
|
||||
x = 0
|
||||
y = 0
|
||||
}
|
||||
y += bounds.y
|
||||
x += bounds.x
|
||||
}
|
||||
|
||||
bounds.width = w
|
||||
bounds.height = h
|
||||
bounds.x = x
|
||||
bounds.y = y
|
||||
// console.log('util bounds', bounds)
|
||||
return bounds
|
||||
return { width: w, height: h, x, y }
|
||||
}
|
||||
|
||||
|
||||
@@ -101,11 +76,15 @@ export const watchConfigKeys = [
|
||||
// 'desktopLyric.style.fontWeight',
|
||||
'desktopLyric.style.opacity',
|
||||
'desktopLyric.style.ellipsis',
|
||||
'desktopLyric.style.isFontWeightFont',
|
||||
'desktopLyric.style.isFontWeightLine',
|
||||
'desktopLyric.style.isFontWeightExtended',
|
||||
'desktopLyric.style.isZoomActiveLrc',
|
||||
'common.langId',
|
||||
'player.isShowLyricTranslation',
|
||||
'player.isShowLyricRoma',
|
||||
'player.isPlayLxlrc',
|
||||
'player.playbackRate',
|
||||
] as const
|
||||
|
||||
export const buildLyricConfig = (appSetting: Partial<LX.AppSetting>): Partial<LX.DesktopLyric.Config> => {
|
||||
@@ -116,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,11 @@ import { isExistWindow, sendEvent } from './index'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
|
||||
autoUpdater.logger = log
|
||||
autoUpdater.autoDownload = false
|
||||
// autoUpdater.forceDevUpdateConfig = true
|
||||
// autoUpdater.autoDownload = false
|
||||
|
||||
let isFirstCheckedUpdate = true
|
||||
// let isFirstCheckedUpdate = true
|
||||
|
||||
log.info('App starting...')
|
||||
|
||||
@@ -65,14 +67,12 @@ interface WaitEvent {
|
||||
info: any
|
||||
}
|
||||
|
||||
let waitEvent: WaitEvent[] = []
|
||||
// let waitEvent: WaitEvent[] = []
|
||||
const handleSendEvent = (action: WaitEvent) => {
|
||||
if (isExistWindow()) {
|
||||
setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
|
||||
sendEvent(action.type, action.info)
|
||||
}, 2000)
|
||||
} else {
|
||||
waitEvent.push(action)
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +105,15 @@ export default () => {
|
||||
})
|
||||
|
||||
mainOn(WIN_MAIN_RENDERER_EVENT_NAME.update_check, () => {
|
||||
console.log('check')
|
||||
checkUpdate()
|
||||
})
|
||||
|
||||
mainOn(WIN_MAIN_RENDERER_EVENT_NAME.update_download_update, () => {
|
||||
if (!autoUpdater.isUpdaterActive()) return
|
||||
void autoUpdater.downloadUpdate()
|
||||
})
|
||||
|
||||
mainOn(WIN_MAIN_RENDERER_EVENT_NAME.quit_update, () => {
|
||||
global.lx.isSkipTrayQuit = true
|
||||
|
||||
@@ -117,24 +123,25 @@ export default () => {
|
||||
})
|
||||
}
|
||||
|
||||
export const checkUpdate = () => {
|
||||
if (!isFirstCheckedUpdate) {
|
||||
if (waitEvent.length) {
|
||||
waitEvent.forEach((event, index) => {
|
||||
setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
|
||||
sendEvent(event.type, event.info)
|
||||
}, 2000 * (index + 1))
|
||||
})
|
||||
waitEvent = []
|
||||
}
|
||||
return
|
||||
}
|
||||
isFirstCheckedUpdate = false
|
||||
const checkUpdate = () => {
|
||||
// if (!isFirstCheckedUpdate) {
|
||||
// if (waitEvent.length) {
|
||||
// waitEvent.forEach((event, index) => {
|
||||
// setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成
|
||||
// sendEvent(event.type, event.info)
|
||||
// }, 2000 * (index + 1))
|
||||
// })
|
||||
// waitEvent = []
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
// isFirstCheckedUpdate = false
|
||||
|
||||
// 由于集合安装包中不包含win arm版,这将会导致arm版更新失败
|
||||
if (isWin && process.arch.includes('arm')) {
|
||||
handleSendEvent({ type: WIN_MAIN_RENDERER_EVENT_NAME.update_error, info: 'failed' })
|
||||
} else {
|
||||
autoUpdater.autoDownload = global.lx.appSetting['common.tryAutoUpdate']
|
||||
void autoUpdater.checkForUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@ import { registerRendererEvents as list } from '@main/modules/commonRenderers/li
|
||||
import app, { sendConfigChange } from './app'
|
||||
import hotKey from './hotKey'
|
||||
import kw_decodeLyric from './kw_decodeLyric'
|
||||
import tx_decodeLyric from './tx_decodeLyric'
|
||||
import userApi from './userApi'
|
||||
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'
|
||||
@@ -26,11 +28,13 @@ export default () => {
|
||||
app()
|
||||
hotKey()
|
||||
kw_decodeLyric()
|
||||
tx_decodeLyric()
|
||||
userApi()
|
||||
sync()
|
||||
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' })
|
||||
|
||||
47
src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts
Normal file
47
src/main/modules/winMain/rendererEvent/tx_decodeLyric.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { inflate } from 'zlib'
|
||||
// import path from 'path'
|
||||
import { mainHandle } from '@common/mainIpc'
|
||||
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/quotes
|
||||
// const require = module[`require`].bind(module)
|
||||
|
||||
let qrc_decode: (buf: Buffer, len: number) => Buffer
|
||||
|
||||
const decode = async(str: string): Promise<string> => {
|
||||
if (!str) return ''
|
||||
const buf = Buffer.from(str, 'hex')
|
||||
return new Promise((resolve, reject) => {
|
||||
inflate(qrc_decode(buf, buf.length), (err, lrc) => {
|
||||
if (err) reject(err)
|
||||
else resolve(lrc.toString())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 感谢某位不愿透露姓名的大佬提供的C++算法源码,但由于作者不希望公开,所以将会以预构建二进制文件的形式加入代码仓库中
|
||||
const handleDecode = async(lrc: string, tlrc: string, rlrc: string) => {
|
||||
if (!qrc_decode) {
|
||||
// const nativeBindingPath = path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
// const nativeBindingPath = isDev ? path.join(__dirname, '../build/Release/qrc_decode.node')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const addon = require('qrc_decode.node')
|
||||
// console.log(addon)
|
||||
qrc_decode = addon.qrc_decode
|
||||
}
|
||||
|
||||
const [lyric, tlyric, rlyric] = await Promise.all([decode(lrc), decode(tlrc), decode(rlrc)])
|
||||
return {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default () => {
|
||||
mainHandle<{ lrc: string, tlrc: string, rlrc: string }, { lyric: string, tlyric: string, rlyric: string }>(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, async({ params: { lrc, tlrc, rlrc } }) => {
|
||||
return handleDecode(lrc, tlrc, rlrc)
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { WindowSize, windowSizeList } from '@common/config'
|
||||
import { type WindowSize, windowSizeList } from '@common/config'
|
||||
import { nativeImage } from 'electron'
|
||||
|
||||
export const getWindowSizeInfo = (windowSizeId: number | string): WindowSize => {
|
||||
|
||||
4
src/main/types/app.d.ts
vendored
4
src/main/types/app.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable no-var */
|
||||
// import { Event as WinMainEvent } from '@main/modules/winMain/event'
|
||||
// import { Event as WinLyricEvent } from '@main/modules/winLyric/event'
|
||||
import { AppType, ListType } from '@main/event'
|
||||
import { DBSeriveTypes } from '@main/worker/utils'
|
||||
import { type AppType, type ListType } from '@main/event'
|
||||
import { type DBSeriveTypes } from '@main/worker/utils'
|
||||
|
||||
interface Lx {
|
||||
appSetting: LX.AppSetting
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
src/main/types/worker.d.ts
vendored
2
src/main/types/worker.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { workerDBSeriveTypes } from '@main/worker/dbService'
|
||||
import { type workerDBSeriveTypes } from '@main/worker/dbService'
|
||||
|
||||
declare global {
|
||||
// interface WorkerDBSeriveTypes {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user