Compare commits

..

182 Commits

Author SHA1 Message Date
lyswhut
3fe34545b9 添加自定义源二进制数据传输支持 2023-05-13 11:53:56 +08:00
lyswhut
da4be33a04 添加上限设置 2023-05-12 20:13:35 +08:00
lyswhut
47e2a5a460 更新依赖 2023-05-08 18:07:26 +08:00
lyswhut
04337ef4b8 添加音效用户预设 2023-05-08 18:04:04 +08:00
lyswhut
df58c8eb80 添加强行构建环境变量参数 2023-05-06 16:36:33 +08:00
lyswhut
b0bf1bf4be 添加繁忙重试重试 2023-05-05 19:01:57 +08:00
lyswhut
1a3d84d4c4 更新版本号 2023-05-04 19:15:52 +08:00
lyswhut
735f629ca1 添加更多环境混响音效 2023-05-04 19:14:00 +08:00
lyswhut
9dc927b50b 修复名称显示 2023-05-04 19:13:48 +08:00
lyswhut
47a336eb64 修复 2023-05-03 18:16:53 +08:00
lyswhut
33b66aaea0 改名 2023-05-03 17:56:32 +08:00
lyswhut
7b9eefdf2d 新增音效设置 2023-05-03 17:45:43 +08:00
lyswhut
ea5e8b56dd 发布v2.2.2 2023-05-01 17:44:11 +08:00
lyswhut
e52fff4d0e 修复添加歌曲弹窗默认列表名字显示问题 2023-05-01 16:15:39 +08:00
lyswhut
8d108509c9 修改默认设置 2023-05-01 16:04:41 +08:00
lyswhut
d3cc630501 更新版本号 2023-05-01 15:32:59 +08:00
lyswhut
e74f297f9e 修复在低版本Linux amd64系统上无法启动的问题 2023-05-01 14:55:35 +08:00
lyswhut
8be9c75734 修改安装依赖的命令 2023-05-01 12:27:46 +08:00
lyswhut
851851a7e6 更新 2023-05-01 11:47:34 +08:00
lyswhut
1cbe46dc3b 发布v2.2.1 2023-05-01 11:13:24 +08:00
lyswhut
675a943d59 更新依赖 2023-04-29 15:19:57 +08:00
Folltoshe
908815def3 更新音源API (#1352)
* 更新API

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2023-04-29 15:09:17 +08:00
lyswhut
200ca1eeda 移除多余翻译 2023-04-29 14:16:38 +08:00
Folltoshe
b81cc139b3 优化UI (#1353)
* 优化UI

* 更新

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2023-04-29 13:10:56 +08:00
lyswhut
f8ae906326 支持wy热门评论翻页 2023-04-28 19:49:53 +08:00
lyswhut
e6f55804f4 更新依赖 2023-04-27 14:04:01 +08:00
lyswhut
1432a19d7d 更新版本号 2023-04-24 13:13:10 +08:00
lyswhut
74fbfb130c 修复歌单加载 2023-04-24 13:05:11 +08:00
lyswhut
3cf45f602f 修复对存在错误时间标签的歌词的解析 2023-04-22 18:32:49 +08:00
lyswhut
7a9dcc71fd 更新导航栏样式 2023-04-20 23:35:57 +08:00
lyswhut
fb5249a979 修复 2023-04-16 15:02:30 +08:00
lyswhut
3f694b75f8 修复列表加载问题 2023-04-16 14:52:22 +08:00
彭狸花喵
0e1ee66dd6 更新kg获取歌单gid接口 & 修复歌单介绍显示 (#1328) 2023-04-16 13:28:55 +08:00
lyswhut
920e002247 优化搜索框背景配色,使其适应高透明主题 2023-04-15 18:22:54 +08:00
Folltoshe
ab0b352d36 修复tx热门评论回复图片显示异常 (#1326) 2023-04-15 17:59:52 +08:00
lyswhut
aa4b08a156 更新依赖 2023-04-14 19:46:05 +08:00
lyswhut
66514908d2 更新版本号 2023-04-13 15:07:47 +08:00
lyswhut
60f0801a7c 修复评论图片显示问题 2023-04-13 15:07:33 +08:00
lyswhut
7149d0d43f 优化更新弹窗弹出时机 2023-04-13 14:34:18 +08:00
Folltoshe
d970687ebe 音源API更新 & 其他改动 (#1295)
## API

### kg源

- 增加歌单歌曲flac24bit显示
- 修复酷狗码只能导入500首的问题
- 更新排行榜

### mg源

- 修复搜索不显示时长的问题
- 修复评论加载失败的问题
- 更新排行榜

### tx源

- 增加热门评论图片显示
- 更新排行榜

### wy源

- 更新排行榜

## 其他

- 添加自定义源zlib模块的支持

---------

Co-authored-by: 彭狸花喵 <94218819+helloplhm-qwq@users.noreply.github.com>
Co-authored-by: lyswhut <lyswhut@qq.com>
2023-04-13 14:32:59 +08:00
lyswhut
d0425fc0d6 更新版本号 2023-04-09 15:36:52 +08:00
lyswhut
bdd5b46708 更新依赖 2023-04-09 15:35:00 +08:00
lyswhut
6d949d7e4d 更新依赖 2023-03-31 20:12:14 +08:00
lyswhut
5d72d092fc 优化&修复同步机制 2023-03-30 17:46:37 +08:00
lyswhut
667fc8c8a6 修复缺失的翻译 2023-03-30 17:45:58 +08:00
lyswhut
f7a2d9fd06 启用桌面歌词时,取消对歌词窗口的聚焦(#1273) 2023-03-29 12:49:30 +08:00
lyswhut
85946efd0b 修复启用全局快捷键时与Media Session注册冲突的问题 2023-03-27 19:57:31 +08:00
lyswhut
f6a853bfe9 发布v2.2.0 2023-03-26 12:14:30 +08:00
lyswhut
4bfbdeb9a5 添加搜索重试 2023-03-26 12:00:29 +08:00
lyswhut
1e53508d67 修复聚合搜索跳转到最后一页后由于列表是空的导致翻页组件隐藏的问题(#1217) 2023-03-26 12:00:20 +08:00
lyswhut
b8fca59cbb 更新依赖 2023-03-25 10:34:41 +08:00
lyswhut
9ada6fe4a7 更新默认设置 2023-03-25 10:04:07 +08:00
lyswhut
b1593a2053 播放速率的粒度调整为0.01,范围0.6-2.0x 2023-03-25 10:04:07 +08:00
彭狸花喵
22a5125a11 修复一个拼写错误导致kg搜索AlbumID为undefined的问题 (#1255) 2023-03-25 10:03:32 +08:00
lyswhut
dfd9a0019b 更新版号 2023-03-23 17:34:01 +08:00
lyswhut
e1d81c199b 更新文案 2023-03-23 17:32:15 +08:00
lyswhut
e8f574ee9c 修复潜在问题 2023-03-23 17:31:59 +08:00
lyswhut
472af1bca6 更新版本号 2023-03-21 17:56:04 +08:00
lyswhut
671346bcdd 修复连续选择时的初始选择歌曲位置被意外改变的问题 2023-03-20 16:33:18 +08:00
lyswhut
9132abb3fe 移除动画预热 2023-03-18 11:13:04 +08:00
lyswhut
1c681e9f82 修复标签显示 2023-03-17 14:50:07 +08:00
lyswhut
19286ef99d 更新依赖 2023-03-17 14:40:56 +08:00
lyswhut
37ad2f522a 更新日志 2023-03-15 17:47:16 +08:00
lyswhut
1e1aa65fc0 新增设置-其他-列表数据清理功能 2023-03-15 17:44:20 +08:00
lyswhut
ec6e067938 新增设置-桌面歌词设置-重置窗口设置功能 2023-03-15 17:44:20 +08:00
Folltoshe
ea515fdd85 增加kg排行榜flac24bit显示 (#1238)
* 增加kg排行榜flac24bit显示
---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2023-03-15 17:42:40 +08:00
lyswhut
c11c202248 更新版本号 2023-03-12 14:13:15 +08:00
lyswhut
1cfee31483 修复企鹅歌单列表歌单名字不显示的问题 2023-03-12 14:12:06 +08:00
lyswhut
00a5aa01c7 更新依赖 2023-03-12 11:31:25 +08:00
lyswhut
fc61cf16ed 修复快照被意外删除的问题 2023-03-09 16:53:35 +08:00
lyswhut
0684e29098 更新字段获取 2023-03-07 16:44:09 +08:00
lyswhut
66d29293cd 更新说明 2023-03-05 20:04:18 +08:00
lyswhut
e9fd657852 优化tx歌单打开 2023-03-05 18:29:57 +08:00
lyswhut
bd7f51358c 更新版本号 2023-03-05 17:34:30 +08:00
lyswhut
371b8fad05 kg源数据更新 (#1232)
* kg源评论接口更新,热门评论完整支持
* kg搜索HiRes支持
---------

Co-authored-by: 彭狸花喵 <94218819+helloplhm-qwq@users.noreply.github.com>
2023-03-05 17:27:17 +08:00
lyswhut
fe259747dc 音量、播放模式等设置弹出式按钮在鼠标移到按钮上时将自动弹出设置内容 2023-03-05 14:30:15 +08:00
lyswhut
d4d94ddbee 修复tx源某些歌单因为歌曲信息缺失导致打开失败的问题 2023-03-04 21:39:08 +08:00
lyswhut
f62d773246 更新依赖 2023-03-04 17:48:13 +08:00
lyswhut
20294233e6 快捷键调整音量时每次加减2%音量改为4% 2023-03-04 16:26:39 +08:00
lyswhut
cb4ab411f9 新增全屏时自动关闭歌词设置 2023-03-04 16:10:34 +08:00
lyswhut
7f008d1f9c 优化自动重连 2023-03-04 13:22:08 +08:00
lyswhut
fe0487c3f9 更新版本号 2023-03-02 17:44:48 +08:00
lyswhut
a54e810af1 修复新设备同步时快照key没有被保存导致的下次同步仍然需要选择同步模式的问题 2023-03-01 22:46:24 +08:00
lyswhut
5cc1f20fac 更新依赖 2023-03-01 18:51:17 +08:00
lyswhut
da804e1f69 更新名字 2023-03-01 18:47:18 +08:00
lyswhut
3399333360 完善同步 2023-03-01 18:44:51 +08:00
lyswhut
e75ab3a7d7 更新版本号 2023-02-28 18:29:42 +08:00
lyswhut
3b70acc3ca 重构数据同步功能,新增客户端模式 2023-02-28 18:01:56 +08:00
lyswhut
b42092884e 支持wy源flac hires歌曲类型的显示 2023-02-26 17:20:05 +08:00
lyswhut
fea2db0e29 修复使用酷狗码无法打开某些类型的歌单的问题 2023-02-26 16:58:27 +08:00
lyswhut
fe7bc924da 修复记住播放进度的情况下,使用Scheme URL打开应用播放的歌曲进度没有被重置的问题 2023-02-22 15:43:45 +08:00
lyswhut
60b357e6f9 修复同步连接的处理问题 2023-02-22 14:44:34 +08:00
lyswhut
5544a469c4 优化wy翻译、罗马音歌词解析 2023-02-19 17:12:19 +08:00
lyswhut
067ca91c4d 发布v2.1.2 2023-02-18 19:57:55 +08:00
lyswhut
afd8d692fd 发布v2.1.1 2023-02-18 19:14:01 +08:00
lyswhut
a9a8fa2857 发布v2.1.0 2023-02-18 13:50:31 +08:00
lyswhut
4b3a624ed9 更新electron到22.3.0 2023-02-17 23:10:16 +08:00
lyswhut
549ea19c26 移除不需要的代码 2023-02-15 09:12:55 +08:00
lyswhut
c0515c9c4f 修复去重列表移除歌曲后列表没有刷新的问题 2023-02-10 21:06:09 +08:00
lyswhut
e81d594d01 修复重启后歌词快进不生效的问题 2023-02-10 19:44:00 +08:00
lyswhut
3d469a4986 更新依赖 2023-02-10 18:07:19 +08:00
lyswhut
b91bd7ede0 修复竖向桌面歌词无法加粗的问题 2023-02-10 13:09:18 +08:00
lyswhut
6048af99bc 添加下载失败信息状态 2023-02-10 08:58:24 +08:00
lyswhut
ef9427122e 新增是否加粗扩展歌词设置 2023-02-09 17:52:58 +08:00
lyswhut
02402f5207 新增设置-下载设置-是否嵌入翻译歌词、罗马音歌词设置 2023-02-09 17:52:23 +08:00
lyswhut
80197ee18c 降级glibc版本编译 2023-02-08 20:23:39 +08:00
lyswhut
8989b05a69 更新版本号 2023-02-08 18:28:30 +08:00
lyswhut
f809782559 新增播放速率调整功能 2023-02-08 18:27:35 +08:00
lyswhut
9522e975a0 修复API初始化失败的问题 2023-02-08 18:07:00 +08:00
lyswhut
8854fb69d8 更新native module 2023-02-08 18:06:31 +08:00
lyswhut
10bd75ea7b 更新Electron到v22.2.0 2023-02-08 00:27:31 +08:00
lyswhut
f33ffb55a7 修复 2023-02-08 00:11:00 +08:00
lyswhut
3bff44da0c 防止桌面歌词窗口在屏幕分辨率变小时,窗口位置跟随分辨率变化的问题 2023-02-07 11:56:19 +08:00
lyswhut
cf960e3695 添加启动时的数据库表及表结构完整性校验 2023-02-06 17:33:21 +08:00
lyswhut
c8eac2690f 修复某些kg歌单链接无法打开的问题 2023-02-06 14:21:16 +08:00
lyswhut
8af458e61a 更新版本号 2023-02-05 20:41:33 +08:00
lyswhut
07899327f1 新增是否在更新版本的首次启动时显示更新日志弹窗设置 2023-02-05 20:40:30 +08:00
lyswhut
dd5bffe071 为所有文本输入框添加右键快速粘贴的功能 2023-02-05 15:32:11 +08:00
lyswhut
83000b75ca 添加忽略二次提示 2023-02-05 13:38:25 +08:00
lyswhut
e6c3815493 完善歌词获取流程 2023-02-05 13:28:43 +08:00
lyswhut
32626ef773 完善lib替换 2023-02-04 18:56:47 +08:00
lyswhut
ab1d18debe 添加x源逐字歌词的支持 2023-02-04 18:38:06 +08:00
lyswhut
62330571a3 更新版号 2023-02-03 19:42:07 +08:00
lyswhut
a8827c361c 优化 2023-02-03 19:41:34 +08:00
lyswhut
4635481c77 修改列表响应式更新机制,尝试修复偶现的删除歌曲列表未更新的问题 2023-02-03 19:41:22 +08:00
lyswhut
c6f9e421c1 更新版本号 2023-02-03 01:23:10 +08:00
lyswhut
448e1cd0c3 修复较旧Linux arm64系统下无法启动软件的问题 2023-02-03 01:00:31 +08:00
lyswhut
5ee7c5a319 优化 2023-02-02 14:10:05 +08:00
lyswhut
89625188f1 更新版本号 2023-02-01 17:30:54 +08:00
lyswhut
fdd19068a8 fix 2023-02-01 16:50:13 +08:00
lyswhut
ef7033cac0 调整了桌面歌词在启用滚动到顶部时的距离 2023-02-01 16:23:41 +08:00
lyswhut
65840e45cd 优化 2023-02-01 14:12:52 +08:00
lyswhut
6075f413dc lint fix 2023-02-01 12:50:52 +08:00
lyswhut
ba3b2f613a 修复更新日志弹窗显示 2023-02-01 12:50:38 +08:00
lyswhut
165832e703 完善支持wy歌词获取 2023-02-01 11:24:58 +08:00
lyswhut
7e27ec488b 添加wy源逐字歌词的支持 2023-01-31 23:40:25 +08:00
lyswhut
4e3356ccb2 更新依赖 2023-01-31 18:35:34 +08:00
lyswhut
6b69cdf12f 优化版本更新操作 2023-01-31 17:48:38 +08:00
lyswhut
c24b29a42c 新增桌面歌词设置字体加粗设置&优化歌词字体显示 2023-01-30 17:48:47 +08:00
lyswhut
c013d19530 修复播放下载列表的歌曲时,调整歌词偏移时间功能异常的问题 2023-01-25 20:14:49 +08:00
lyswhut
5a1e1864d9 修正错别字 2023-01-18 22:31:10 +08:00
lyswhut
7c56b2fce2 发布v2.0.5 2023-01-18 22:14:29 +08:00
lyswhut
7266352ca0 修复字母被截取的问题 2023-01-18 21:41:42 +08:00
lyswhut
c1cff1e3c2 修复macOS下的软件系统菜单中的退出功能不会完全退出软件的问题(#1148) 2023-01-18 17:25:51 +08:00
lyswhut
c003efe1e9 fix type 2023-01-18 15:57:06 +08:00
lyswhut
68d847aca5 修复随机播放模式下使用稍后播放功能播放我的列表的歌曲时,切换下一曲永远是当前歌曲的问题(#1147) 2023-01-18 15:56:05 +08:00
lyswhut
82bea640c4 重写桌面歌词窗口坐标的计算逻辑 2023-01-18 15:43:26 +08:00
lyswhut
4fa88dd7a0 修复声音输出设备更改时后的自动暂停播放设置无效的问题 2023-01-18 14:04:04 +08:00
lyswhut
900bdd3afe 发布v2.0.4 2023-01-15 15:15:00 +08:00
lyswhut
8dff457422 优化虚拟列表 2023-01-14 16:48:20 +08:00
lyswhut
850a429243 优化代码逻辑,修复潜在播放问题 2023-01-14 16:03:56 +08:00
lyswhut
d3c8f16aed 更新依赖 2023-01-14 00:50:43 +08:00
lyswhut
a43e127dd0 修复备份与恢复的列表导入列表信息设置逻辑问题与潜在问题 2023-01-14 00:50:29 +08:00
lyswhut
84e52f5525 修复Linux armv7l系统(如树莓派)下无法启动的问题 2023-01-14 00:06:04 +08:00
lyswhut
1dce2c1f7a 更新 2023-01-12 14:08:28 +08:00
lyswhut
1c802ad5f5 fix type 2023-01-11 16:47:57 +08:00
lyswhut
db38db9256 修复桌面歌词启用歌词缩放后的阴影显示问题;就放桌面歌词在启用卡拉OK歌词后字体边缘可能被截断的问题 2023-01-11 13:08:28 +08:00
lyswhut
cb9fa62dac 修复从搜索界面进入歌单详情后,若启用强迫症设置的清空功能会导致意外清空搜索框、搜索列表的问题 2023-01-11 10:53:17 +08:00
lyswhut
9cc36e2a6a 优化临时播放逻辑 2023-01-11 09:28:31 +08:00
lyswhut
bf310adc12 fix type 2023-01-11 08:33:13 +08:00
lyswhut
7273b6ad67 修复备份文件导入指引无法识别v2配置的问题 2023-01-11 08:32:49 +08:00
lyswhut
a02f9e1151 发布v2.0.3 2023-01-08 13:25:39 +08:00
lyswhut
6ef8015179 修复歌词阴影问题 2023-01-07 18:59:44 +08:00
lyswhut
db38b27e52 修复桌面歌词开启不允许换行后出现字体截断的问题(#1106) 2023-01-07 15:59:30 +08:00
lyswhut
cccb5b191d 修复修改播放设置-音频输出设置后,所做的更改没有被保存的问题(#1116) 2023-01-07 15:17:07 +08:00
lyswhut
3c589743b1 更新版本号 2023-01-06 17:47:21 +08:00
lyswhut
65270e02da 优化定时停止 2023-01-06 14:49:09 +08:00
lyswhut
27c1bf8eb6 eslint 2023-01-06 14:39:44 +08:00
lyswhut
1db54195f5 修复禁用切歌时歌曲播放完毕后的歌曲信息显示问题 2023-01-06 14:39:08 +08:00
lyswhut
399208429d 修复暂停按钮图标显示 2023-01-06 14:35:52 +08:00
lyswhut
b40e57f191 修复某些情况下歌词的滚动问题 2023-01-06 14:22:17 +08:00
lyswhut
be350e49f0 更新版本号 2023-01-05 19:04:50 +08:00
lyswhut
bae732016c 修复桌面歌词使用斜体出现截断的问题(#1106) 2023-01-05 18:49:50 +08:00
lyswhut
e50a67f0e4 点击打开歌单弹窗背景可以关闭弹窗(#1096) 2023-01-05 18:49:08 +08:00
lyswhut
26e8e0a1b1 修复Linux arm64系统下无法启动的问题(#1102) 2023-01-05 18:47:27 +08:00
lyswhut
c8210e0516 修复初始设置的桌面歌词窗口没有完全居右下角的问题 2023-01-05 11:57:44 +08:00
lyswhut
95590dd07f 修复列表备份文件存在异常歌曲信息时无法导入的问题 2023-01-02 18:35:42 +08:00
lyswhut
7e94e96bcc 优化迁移 2023-01-02 17:57:01 +08:00
lyswhut
5009494e89 发布v2.0.2 2023-01-02 17:52:40 +08:00
lyswhut
e58c2f86aa 修复无效的歌曲信息导致我的列表数据迁移失败的问题(#1095) 2023-01-02 17:52:19 +08:00
lyswhut
fcea542def 发布v2.0.1 2023-01-02 16:41:25 +08:00
lyswhut
44407585fa 合并sql语句 2023-01-02 13:40:46 +08:00
356 changed files with 12957 additions and 6547 deletions

View File

@@ -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",
},

View File

@@ -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

View File

@@ -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

View File

@@ -4,10 +4,10 @@ module.exports = {
'electron',
'chalk',
'del',
'comlink',
],
// target: 'newest',
// filter: [
// /^vue/,
// 'electron-builder',
// 'electron-updater',
// ],

View File

@@ -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
}

View File

@@ -6,6 +6,197 @@ 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
### 修复
- 修复备份文件导入指引无法识别v2配置的问题
- 修复从搜索界面进入歌单详情后,若启用强迫症设置的清空功能会导致意外清空搜索框、搜索列表的问题
- 修复桌面歌词在启用卡拉OK歌词后字体边缘可能被截断的问题特别是纵向歌词某些字的边角被截断导致后面的阴影露出来或阴影不均匀的问题
- 修复桌面歌词启用歌词缩放后的阴影显示问题
- 修复Linux armv7l系统如树莓派下无法启动的问题与修复Linux arm64的方法一样采用内置预编译模块的方式修复
- 修复备份与恢复的列表导入列表信息设置逻辑问题与潜在导入问题
## [2.0.3](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.2...v2.0.3) - 2023-01-08
### 修复
- 修复初始设置的桌面歌词窗口没有完全居右下角的问题
- 修复Linux arm64系统下无法启动的问题#1102
- 修复桌面歌词使用斜体出现截断的问题(#1106
- 修复某些情况下歌词的滚动问题
- 修复禁用切歌时歌曲播放完毕后的歌曲信息显示问题
- 修复修改播放设置-音频输出设置后,所做的更改没有被保存的问题
### 优化
- 点击打开歌单弹窗背景可以关闭弹窗(#1096
## [2.0.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.1...v2.0.2) - 2023-01-02
若你更新v2.0.0后,出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表播放的问题,可以按以下方式解决:
1. 根据你的平台类型,进入软件数据目录
- Windows`%APPDATA%/lx-music-desktop`
- Linux`$XDG_CONFIG_HOME/lx-music-desktop``~/.config/lx-music-desktop`
- macOS`~/Library/Application Support/lx-music-desktop`
2. 进入`LxDatas`目录退出LX删除`lx.data.db`文件,再启动软件即可
若以上操作仍然不行可以加交流群或者在GitHub开issue反馈
### 修复
- 修复无效的歌曲信息导致我的列表数据迁移失败的问题
## [2.0.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.0.0...v2.0.1) - 2023-01-02
若你更新v2.0.0后,出现之前收藏的歌曲全部丢失或者歌曲无法添加到列表播放的问题,可以按以下方式解决:
1. 根据你的平台类型,进入软件数据目录
- Windows`%APPDATA%/lx-music-desktop`
- Linux`$XDG_CONFIG_HOME/lx-music-desktop``~/.config/lx-music-desktop`
- macOS`~/Library/Application Support/lx-music-desktop`
2. 进入`LxDatas`目录退出LX删除`lx.data.db`文件,再启动软件即可
若以上操作仍然不行可以加交流群或者在GitHub开issue反馈
### 优化
- 单次执行所有sql语句尝试解决某些情况下某些表没有成功创建的问题
## [2.0.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.22.3...v2.0.0) - 2023-01-01

3
FAQ.md
View File

@@ -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讨论。 -->

View File

@@ -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>

View File

@@ -0,0 +1,75 @@
const fs = require('fs')
const fsPromises = require('fs').promises
const path = require('path')
const { Arch } = require('electron-builder')
const better_sqlite3_fileNameMap = {
[Arch.x64]: 'electron-v110-linux-x64',
[Arch.arm64]: 'electron-v110-linux-arm64',
[Arch.armv7l]: 'electron-v110-linux-arm',
}
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_${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
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...')
await fsPromises.rename(bindingFilePath, bindingBakFilePath)
}
await replaceSqliteLib(arch)
break
default:
if (fs.existsSync(bindingFilePath)) return
// console.log('restore binding file...')
await fsPromises.rename(bindingBakFilePath, bindingFilePath)
break
}
}

View File

@@ -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'),

View File

@@ -0,0 +1,2 @@
const { Arch } = require('electron-builder')
require('./build-before-pack')({ electronPlatformName: process.platform, arch: Arch[process.arch] })

View File

@@ -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: {

View File

@@ -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) => {

6291
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "2.0.0",
"version": "2.3.0-beta.3",
"description": "一个免费的音乐查找助手",
"main": "./dist/main.js",
"productName": "lx-music-desktop",
@@ -64,14 +64,14 @@
"build:renderer-lyric": "cross-env NODE_ENV=production webpack --config build-config/renderer-lyric/webpack.config.prod.js --progress",
"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 .js,.vue -f node_modules/eslint-formatter-friendly src",
"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",
@@ -79,6 +79,7 @@
},
"build": {
"appId": "cn.toside.music.desktop",
"beforePack": "./build-config/build-before-pack.js",
"afterPack": "./build-config/build-after-pack.js",
"protocols": {
"name": "lx-music-protocol",
@@ -99,6 +100,7 @@
"node_modules/node-gyp-build",
"node_modules/bufferutil",
"node_modules/utf-8-validate",
"build/Release/qrc_decode.node",
"dist/**/*"
],
"asar": {
@@ -203,98 +205,96 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.20.7",
"@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.47.1",
"@typescript-eslint/parser": "^5.47.1",
"@volar/vue-language-plugin-pug": "^1.0.19",
"babel-loader": "^9.1.0",
"browserslist": "^4.21.4",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"@volar/vue-language-plugin-pug": "^1.6.4",
"babel-loader": "^9.1.2",
"browserslist": "^4.21.5",
"chalk": "^4.1.2",
"changelog-parser": "^3.0.1",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.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.8",
"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.6",
"eslint": "^8.31.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": "^24.0.0",
"eslint-config-standard-with-typescript": "^34.0.1",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^7.1.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.8.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.20",
"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": "^3.0.2",
"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.0",
"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": "^5.0.10",
"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",
@@ -305,6 +305,7 @@
},
"svg-baker": {
"postcss": "latest"
}
},
"http-cache-semantics": "latest"
}
}

View File

@@ -1,59 +1,7 @@
### 不兼容性变更说明
- 数据迁移,升级此版本时,会使用旧版本的我的列表、下载设置、快捷键设置、自定义源等数据会自动迁移到新的数据格式版本,旧的数据仍然会保留,但下载列表的数据不做迁移
- 备份文件v2.0.0及以后版本导出的列表、配置不支持导入v2.0.0之前版本但v2.0.0之前版本导出的列表、配置支持导入v2.0.0以及以后版本移动端需v0.15.0起才支持导入PC端v2生成的备份数据
- 同步功能该功能不支持与移动端v1.0.0之前版本的使用需等待后面的新版移动端目前移动端v1的开发工作已在进行中
### 新增
- 新增自定义主题功能
- 新增歌单搜索功能
- 新增将本地歌曲添加到我的列表的支持,此功能可以在列表的右击菜单中使用(本地歌曲的歌词优先尝试读取相同路径下的同名歌词文件,若文件不存在则尝试读取歌曲文件内的歌词,若还是找不到歌词则尝试利用换源功能获取在线歌词,歌曲封面则是尝试读取歌曲文件内的封面,若不存在则利用换源功能获取在线封面)
- 启动软件时自动回到上次的界面,例如上次退出软件时在我的列表,下次启动软件时会自动进入我的列表
- 新增启动软件时自动播放音乐设置,默认关闭,可去设置-播放设置开启
- 新增“蛋雅深藍”、“近墨者黑”皮肤
- 新增下载歌词时是否同时下载歌词翻译、罗马音设置,默认关闭,可以去设置-下载设置开启(#344
- 新增下载时,若目录存在同名的文件时是否跳过下载此任务的设置(默认跳过,可以去设置-下载设置更改)
- 新增界面字体大小设置
- 桌面歌词新增竖排歌词显示功能(#971
- 桌面歌词新增歌词对齐方式、是否不允许歌词换行、歌词颜色、滚动对齐方式、歌词间距设置
- 桌面歌词新增歌曲频谱显示得益于主窗口与桌面歌词进程通信的改进可以将此功能以CPU使用率“相对较低”的方式带到桌面歌词中
- 桌面歌词新增在任务栏显示歌词进程设置(此设置用于在录屏软件无法捕获歌词窗口时的变通解决方法)(#1063
- 添加kg源罗马音歌词的支持感谢@helloplhm-qwq
- 支持打开波点音乐歌单(需在酷我源打开)
- 新增设置-基本设置-播放栏进度条样式设置(此版本默认使用迷你进度条样式,对于某些不喜欢该样式的人可以将其换成其他样式)
- 添加kg源评论图片展示感谢@helloplhm-qwq
### 优化(界面/交互/功能)
- 调整软件界面及配色,使其更加清爽
- 处于单曲循环、顺序播放、禁用切歌模式时,手动切歌将会按列表循环模式的逻辑处理切歌(#864
- 歌单右键菜单的“重复歌曲”扫描功能现在会将歌曲名字内的括号内容移除再对比,这可以有效找出歌曲的变体,例如:`突然的自我``突然的自我(Live)``突然的自我(女生版)``突然的自我(DJ版)`等都会被找出来(#987
- 允许更小的桌面歌词窗口高度,可以取消“不允许拖动到主屏幕之外”设置后,再启用“不允许歌词换行”、“置顶歌词”与“自动刷新置顶”等设置,把它拖动到任务栏上,当做任务栏歌词使用(具体可以按你想要的显示方式使用这些设置组合去调)
### 优化(程序)
- 优化程序启动性能,优化与程序交互的流畅度
- 重构整个程序重新梳理了程序逻辑使其更容易扩展及维护将大部分代码从JavaScript迁移到TypeScript
- 重写配置管理、列表管理功能列表、歌词数据从json文件迁移到sqlite3存储这应该能解决因为意外的字符编码导致的数据文件损坏问题
### 变更
- 列表右侧的操作按钮栏默认不再显示,歌曲的操作可以使用右键菜单代替,若想恢复它们的显示,可以去设置-列表设置-启用操作按钮栏开启
- 窗口大小设置时不再自动调整字体大小,想要调整字体大小可以使用新增的字体大小设置调整
### 修复
- 修复Linux、macOS下若程序路径存在百分号时会导致软件无法启动的问题#963
- 支持单行多时间标签歌词解析,修复某些歌词会出现时间标签的问题
### 移除
- 移除“信口雌黄”皮肤(由于该皮肤的配色有点刺眼),若你正在使用该皮肤,可以使用自定义主题功能恢复它
- 移除Linux deb x86包构建Electron/Chromium已不再支持 32-bit Linuxelectron/electron#34787
- 移除桌面歌词主题设置,改用桌面歌词字体颜色设置功能代替
- 新增音效设置实验性功能支持10段均衡器设置、内置的一些环境混响音效、3D立体环绕音效
### 其他
- 更新Electron到v19.1.9
- 更新 electron 到 v22.3.8

File diff suppressed because one or more lines are too long

View File

@@ -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 = {
@@ -23,6 +24,7 @@ export const LIST_IDS = {
LOVE: 'love',
TEMP: 'temp',
DOWNLOAD: 'download',
PLAY_LATER: null,
} as const
export const DATA_KEYS = {
@@ -37,6 +39,8 @@ export const DATA_KEYS = {
leaderboardSetting: 'leaderboardSetting',
songListSetting: 'songListSetting',
searchSetting: 'searchSetting',
lastStartInfo: 'lastStartInfo',
} as const
export const DEFAULT_SETTING = {
@@ -72,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

View File

@@ -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,

View File

@@ -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,
@@ -61,11 +84,14 @@ const defaultSetting: LX.AppSetting = {
'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)'
}

View File

@@ -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',

View File

@@ -36,7 +36,7 @@ exports.createThemeColors = (rgbaColor, fontRgbaColor, isDark) => {
const createFontColors = (rgbaColor, isDark) => {
// rgb(238, 238, 238)
// let prec = 'rgb(255, 255, 255)'
if (rgbaColor == null) rgbaColor = isDark ? 'rgb(229, 229, 229)' : 'rgb(33, 33, 33)'
rgbaColor ??= isDark ? 'rgb(229, 229, 229)' : 'rgb(33, 33, 33)'
if (isDark) return createFontDarkColors(rgbaColor)
let colors = {

View File

@@ -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
/**
* 是否在离开搜索界面时自动清空搜索框

View File

@@ -65,9 +65,7 @@ declare namespace LX {
interface HotKeyConfig {
enable: boolean
keys: {
[key: string]: HotKey
}
keys: Record<string, HotKey>
}
interface HotKeyConfigAll {
local: HotKeyConfig
@@ -104,6 +102,7 @@ declare namespace LX {
prev: boolean
}
type UpdateStatus = 'downloaded' | 'downloading' | 'error' | 'checking' | 'idle'
interface VersionInfo {
version: string
desc: string

View File

@@ -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
}

View File

@@ -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
}
}
}
}

View File

@@ -4,7 +4,7 @@ declare namespace LX {
id: string
name: string
// list: LX.Music.MusicInfo[]
source?: LX.Source
source?: LX.OnlineSource
sourceListId?: string
// position?: number
locationUpdateTime: number | null
@@ -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[]
}

View File

@@ -1,5 +1,5 @@
import {
IAudioMetadata as iAudioMetadata,
type IAudioMetadata as iAudioMetadata,
} from 'music-metadata'
declare global {

View File

@@ -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
View 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
}
}
}

View File

@@ -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'
}

View File

@@ -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
}

View File

@@ -176,7 +176,7 @@ export const similar = (a: string, b: string) => {
* @param arr
* @param data
*/
export const sortInsert = (arr: Array<{ num: number, data: any }>, data: { num: number, data: any }) => {
export const sortInsert = <T>(arr: Array<{ num: number, data: T }>, data: { num: number, data: T }) => {
let key = data.num
let left = 0
let right = arr.length - 1

View File

@@ -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
@@ -135,12 +135,13 @@ class Task extends EventEmitter {
try {
this.__initDownload(response)
} catch (error: any) {
return this.__handleError(error)
this.__handleError(error)
return
}
this.status = STATUS.running
response
.on('data', this.__handleWriteData.bind(this))
.on('error', err => this.__handleError(err))
.on('error', err => { this.__handleError(err) })
.on('end', () => {
if (response.complete) {
this.__handleComplete()
@@ -150,7 +151,7 @@ class Task extends EventEmitter {
}
})
})
.on('error', err => this.__handleError(err))
.on('error', err => { this.__handleError(err) })
.on('close', () => {
void this.__closeWriteStream()
})
@@ -159,7 +160,10 @@ class Task extends EventEmitter {
__initDownload(response: http.IncomingMessage) {
this.progress.total = response.headers['content-length'] ? parseInt(response.headers['content-length']) : 0
if (!this.progress.total) return this.__handleError(new Error('Content length is 0'))
if (!this.progress.total) {
this.__handleError(new Error('Content length is 0'))
return
}
let options: any = {}
let isResumable = this.options.forceResume ||
response.headers['accept-ranges'] !== 'none' ||
@@ -170,11 +174,17 @@ class Task extends EventEmitter {
options.flags = 'a'
if (this.progress.downloaded) this.progress.total -= 10
} else {
if (this.chunkInfo.startByte != '0') return this.__handleError(new Error('The resource cannot be resumed download.'))
if (this.chunkInfo.startByte != '0') {
this.__handleError(new Error('The resource cannot be resumed download.'))
return
}
}
this.progress.total += this.progress.downloaded
this.statsEstimate.prevBytes = this.progress.downloaded
if (!this.chunkInfo.path) return this.__handleError(new Error('Chunk save Path is not set.'))
if (!this.chunkInfo.path) {
this.__handleError(new Error('Chunk save Path is not set.'))
return
}
this.ws = fs.createWriteStream(this.chunkInfo.path, options)
this.ws.on('finish', () => {
@@ -216,7 +226,10 @@ class Task extends EventEmitter {
async __closeWriteStream() {
return new Promise<void>((resolve, reject) => {
if (!this.ws) return resolve()
if (!this.ws) {
resolve()
return
}
// console.log('close write stream')
this.ws.close(err => {
if (err) {
@@ -251,7 +264,10 @@ class Task extends EventEmitter {
// this.__handleError(err)
this.chunkInfo.startByte = '0'
this.resumeLastChunk = null
if (unlinkErr && unlinkErr.code !== 'ENOENT') return this.__handleError(unlinkErr)
if (unlinkErr && unlinkErr.code !== 'ENOENT') {
this.__handleError(unlinkErr)
return
}
void this.start()
})
})
@@ -259,7 +275,10 @@ class Task extends EventEmitter {
}
}
// console.log('data', chunk)
if (this.status == STATUS.stopped || this.ws == null) return console.log('cancel write')
if (this.status == STATUS.stopped || this.ws == null) {
console.log('cancel write')
return
}
this.__calculateProgress(chunk.length)
this.ws.write(chunk, err => {
if (!err) return

View File

@@ -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 = {

View File

@@ -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,12 +139,12 @@ 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
if (this.shadowContent) {
if (!lrcShadowContent) lrcShadowContent = document.createElement('div')
lrcShadowContent ??= document.createElement('div')
const shadowDom = document.createElement('span')
shadowDom.textContent = text
lrcShadowContent.appendChild(shadowDom)
@@ -159,7 +162,7 @@ module.exports = class FontPlayer {
}
if (this.shadowContent && lrcShadowContent) {
lrcShadowContent.style = 'position:absolute;top:0;left:0;width:100%;z-index:-1;'
lrcShadowContent.style = 'position:absolute;top:0;left:0;right:0;z-index:-1;'
lrcShadowContent.className = this.shadowClassName
this.line.appendChild(lrcShadowContent)
}
@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -20,12 +20,12 @@ 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) {
let scroll = setting.list.scroll
if (setting.list.isSaveScrollLocation) setting.list.isSaveScrollLocation = scroll.enable
setting.list.isSaveScrollLocation &&= scroll.enable
delete 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
}

View File

@@ -16,9 +16,15 @@ export const dirname = (p: string): string => path.dirname(p)
*/
export const checkPath = async(path: string): Promise<boolean> => {
return await new Promise(resolve => {
if (!path) return resolve(false)
if (!path) {
resolve(false)
return
}
fs.access(path, fs.constants.F_OK, err => {
if (err) return resolve(false)
if (err) {
resolve(false)
return
}
resolve(true)
})
})
@@ -26,9 +32,15 @@ export const checkPath = async(path: string): Promise<boolean> => {
export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
return await new Promise(resolve => {
if (!path) return resolve(null)
if (!path) {
resolve(null)
return
}
fs.stat(path, (err, stats) => {
if (err) return resolve(null)
if (err) {
resolve(null)
return
}
resolve(stats)
})
})
@@ -39,29 +51,37 @@ export const getFileStats = async(path: string): Promise<fs.Stats | null> => {
* @param path
* @returns
*/
export const createDir = async(path: string): Promise<void> => {
return await new Promise((resolve, reject) => {
fs.access(path, fs.constants.F_OK | fs.constants.W_OK, err => {
if (err) {
if (err.code === 'ENOENT') {
fs.mkdir(path, { recursive: true }, err => {
if (err) return reject(err)
resolve()
})
return
}
return reject(err)
export const createDir = async(path: string) => new Promise<void>((resolve, reject) => {
fs.access(path, fs.constants.F_OK | fs.constants.W_OK, err => {
if (err) {
if (err.code === 'ENOENT') {
fs.mkdir(path, { recursive: true }, err => {
if (err) {
reject(err)
return
}
resolve()
})
return
}
resolve()
})
reject(err)
return
}
resolve()
})
}
})
export const removeFile = async(path: string) => new Promise<void>((resolve, reject) => {
fs.access(path, fs.constants.F_OK, err => {
if (err) return err.code == 'ENOENT' ? resolve() : reject(err)
if (err) {
err.code == 'ENOENT' ? resolve() : reject(err)
return
}
fs.unlink(path, err => {
if (err) return reject(err)
if (err) {
reject(err)
return
}
resolve()
})
})
@@ -79,7 +99,10 @@ export const toMD5 = (str: string) => crypto.createHash('md5').update(str).diges
export const gzipData = async(str: string): Promise<Buffer> => {
return await new Promise((resolve, reject) => {
gzip(str, (err, result) => {
if (err) return reject(err)
if (err) {
reject(err)
return
}
resolve(result)
})
})
@@ -88,7 +111,10 @@ export const gzipData = async(str: string): Promise<Buffer> => {
export const gunzipData = async(buf: Buffer): Promise<string> => {
return await new Promise((resolve, reject) => {
gunzip(buf, (err, result) => {
if (err) return reject(err)
if (err) {
reject(err)
return
}
resolve(result.toString())
})
})

View File

@@ -45,7 +45,10 @@ const handleScrollY = (element: HTMLElement, to: number, duration = 300, fn = no
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) return fn()
if (cancel) {
fn()
return
}
window.setTimeout(animateScroll, increment)
} else {
fn()
@@ -117,7 +120,10 @@ const handleScrollX = (element: HTMLElement, to: number, duration = 300, fn = ()
element.scrollLeft = val
}
if (currentTime < duration) {
if (cancel) return fn()
if (cancel) {
fn()
return
}
window.setTimeout(animateScroll, increment)
} else {
fn()
@@ -192,7 +198,10 @@ const handleScrollXR = (element: HTMLElement, to: number, duration = 300, fn = (
element.scrollLeft = val
}
if (currentTime < duration) {
if (cancel) return fn()
if (cancel) {
fn()
return
}
window.setTimeout(animateScroll, increment)
} else {
fn()
@@ -235,7 +244,7 @@ export const scrollXRTo = (element: HTMLElement, to: number, duration = 300, fn
*/
let dom_title = document.getElementsByTagName('title')[0]
export const setTitle = (title: string | null) => {
if (!title) title = '洛雪音乐助手'
title ||= '洛雪音乐助手'
dom_title.innerText = title
}

View File

@@ -2,7 +2,7 @@
export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
const meta: Record<string, any> = {
songId: oldMusicInfo.songmid, // 歌曲IDmg源为copyrightIdlocal为文件路径
songId: oldMusicInfo.songmid, // 歌曲IDlocal为文件路径
albumName: oldMusicInfo.albumName, // 歌曲专辑名称
picUrl: oldMusicInfo.img, // 歌曲图片链接
}
@@ -118,3 +118,13 @@ export const fixNewMusicInfoQuality = (musicInfo: LX.Music.MusicInfo) => {
return musicInfo
}
export const filterMusicList = <T extends LX.Music.MusicInfo>(list: T[]): T[] => {
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 = ''
ids.add(s.id)
return true
})
}

View File

@@ -14,9 +14,9 @@ import {
shallowRef,
unref,
markRaw,
ComputedRef,
Ref,
ShallowRef,
type ComputedRef,
type Ref,
type ShallowRef,
defineProps,
defineEmits,
defineComponent,

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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": "导入",

View File

@@ -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": "導入",

View File

@@ -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'
@@ -116,8 +117,14 @@ export const registerDeeplink = (startApp: () => void) => {
export const listenerAppEvent = (startApp: () => void) => {
app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (event, navigationUrl) => {
if (global.isDev) return console.log('navigation to url:', navigationUrl)
if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) return event.preventDefault()
if (global.isDev) {
console.log('navigation to url:', navigationUrl)
return
}
if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) {
event.preventDefault()
return
}
console.log('navigation to url:', navigationUrl)
})
contents.setWindowOpenHandler(({ url }) => {
@@ -231,9 +238,20 @@ 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))
if (!dbFileExists) await migrateDBData().catch(err => { log.error(err) })
initTheme()
}
// global.lx.theme = getTheme()

View File

@@ -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)
}
}

View File

@@ -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,

View File

@@ -17,8 +17,12 @@ electronDebug({
// Install `vue-devtools`
app.on('ready', () => {
installExtension(VUEJS_DEVTOOLS)
.then((name: string) => console.log(`Added Extension: ${name}`))
.catch((err: Error) => console.log('An error occurred: ', err))
.then((name: string) => {
console.log(`Added Extension: ${name}`)
})
.catch((err: Error) => {
console.log('An error occurred: ', err)
})
})
// Require `main` process to boot app

View File

@@ -30,4 +30,6 @@ listenerAppEvent(init)
// https://github.com/electron/electron/issues/16809
void app.whenReady().then(() => isLinux ? setTimeout(init, 300) : init())
void app.whenReady().then(() => {
isLinux ? setTimeout(init, 300) : init()
})

View File

@@ -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()
},
},
],

View 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)
}

View 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

View 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'

View File

@@ -0,0 +1 @@
export { default as list } from './list'

View File

@@ -0,0 +1,7 @@
import initOn from './on'
import initSend from './send'
export default (socket: LX.Sync.Client.Socket) => {
initOn(socket)
initSend(socket)
}

View 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)
})
}

View 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)
}

View 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)
}))
})

View 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))
}

View 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)
})
}

View File

@@ -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()
}
})
}

View 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)
},
}

View File

@@ -1,94 +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' && 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
}
}
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')
}

View File

@@ -1,7 +0,0 @@
export const SYNC_CODE = {
helloMsg: 'Hello~::^-^::~v2~',
idPrefix: 'OjppZDo6',
authMsg: 'lx-music auth::',
msgAuthFailed: 'Auth failed',
msgConnect: 'lx-music connect',
} as const

View File

@@ -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,
}

View 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
}
}

View File

@@ -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,36 +248,49 @@ const handleStartServer = async(port = 9527) => await new Promise((resolve, reje
httpServer.on('listening', () => {
const addr = httpServer.address()
if (!addr) return reject(new Error('address is null'))
// 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...')
return await handleStopServer().then(() => {
status.message = 'stoping...'
sendServerStatus(status)
stopingServer = true
await handleStopServer().then(() => {
console.log('sync server stoped')
status.status = false
status.message = ''
@@ -181,15 +300,18 @@ 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...')
return await handleStartServer(port).then(() => {
log.info('starting sync server')
await handleStartServer(port).then(() => {
console.log('sync server started')
status.status = true
status.message = ''
@@ -204,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,
}

View File

@@ -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,99 +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) return reject(new Error('Get remote list data failed'))
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
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)
return 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
@@ -133,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':
@@ -217,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)
@@ -245,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 = (
@@ -310,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) {
@@ -322,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':
@@ -346,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 = {
@@ -359,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)
@@ -403,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()
return 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)
return await fsPromises.unlink(filePath)
}
export {
_syncList as syncList,
removeSnapshot,
}
// const removeSnapshot = async(keyInfo: LX.Sync.KeyInfo) => {
// const filePath = getSnapshotFilePath(keyInfo)
// await fsPromises.unlink(filePath)
// }

View File

@@ -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)
if (!keyInfos) 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()
}

View File

@@ -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
return 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
}

View File

@@ -27,7 +27,7 @@ const winEvent = () => {
export const createWindow = async(userApi: LX.UserApi.UserApiInfo) => {
await closeWindow()
if (!dir) dir = global.isDev ? webpackUserApiPath : join(encodePath(__dirname), 'userApi')
dir ??= global.isDev ? webpackUserApiPath : join(encodePath(__dirname), 'userApi')
if (!html) {
html = await fs.promises.readFile(join(dir, 'renderer/user-api.html'), 'utf8')
@@ -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',
@@ -72,7 +73,10 @@ export const createWindow = async(userApi: LX.UserApi.UserApiInfo) => {
})
}
browserWindow.webContents.session.setPermissionRequestHandler((webContents, permission, resolve) => {
if (webContents === browserWindow?.webContents) return resolve(false)
if (webContents === browserWindow?.webContents) {
resolve(false)
return
}
resolve(true)
})
browserWindow.webContents.setWindowOpenHandler(() => {

View File

@@ -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

View File

@@ -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
@@ -118,7 +118,9 @@ export const cancelRequest = (requestKey: string) => {
}
export const request = async({ requestKey, data }: LX.UserApi.UserApiRequestParams): Promise<any> => await new Promise((resolve, reject) => {
if (!userApi) return reject(new Error('user api is not load'))
if (!userApi) {
reject(new Error('user api is not load'))
}
// const requestKey = `request__${Math.random().toString().substring(2)}`
const timeout = timeouts.get(requestKey)

View File

@@ -33,7 +33,7 @@ export const importApi = (script: string): LX.UserApi.UserApiInfo => {
script,
allowShowUpdateAlert: true,
}
if (!userApis) userApis = []
userApis ??= []
userApis.push(apiInfo)
getStore(STORE_NAMES.USER_API).set('userApis', userApis)
return apiInfo

View File

@@ -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']

View File

@@ -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 => {

View File

@@ -1,7 +1,7 @@
import { join } from 'path'
import { BrowserWindow } from 'electron'
import { debounce, isLinux } from '@common/utils'
import { getLyricWindowBounds } 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
y = screenHeight - height
}
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)
}
@@ -202,7 +211,10 @@ export const alwaysOnTopTools: AlwaysOnTopTools = {
startLoop() {
this.clearLoop()
this.timeout = setInterval(() => {
if (!isExistWindow()) return this.clearLoop()
if (!isExistWindow()) {
this.clearLoop()
return
}
setAlwaysOnTop(true, 'screen-saver')
}, 1000)
},

View File

@@ -1,77 +1,52 @@
// 设置窗口位置、大小
let winX
let winY
let wasW
let wasH
let 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,
}
}

View File

@@ -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()
}
}

View File

@@ -20,7 +20,7 @@ const winEvent = () => {
return
}
if (global.lx.isTrafficLightClose) global.lx.isTrafficLightClose = false
global.lx.isTrafficLightClose &&= false
event.preventDefault()
browserWindow!.hide()
})
@@ -242,13 +242,13 @@ export const setThumbarButtons = ({ empty, collect, play, next, prev }: LX.TaskB
export const setThumbnailClip = (region: Electron.Rectangle) => {
if (!browserWindow) return
return browserWindow.setThumbnailClip(region)
browserWindow.setThumbnailClip(region)
}
export const clearCache = async() => {
if (!browserWindow) throw new Error('main window is undefined')
return await browserWindow.webContents.session.clearCache()
await browserWindow.webContents.session.clearCache()
}
export const getCacheSize = async() => {

View File

@@ -55,12 +55,16 @@ export default () => {
showWindow()
})
mainOn<boolean>(WIN_MAIN_RENDERER_EVENT_NAME.close, ({ params: isForce }) => {
if (isForce) return app.exit(0)
if (isForce) {
app.exit(0)
return
}
global.lx.isTrafficLightClose = true
closeWindow()
})
// 全屏
mainHandle<boolean, boolean>(WIN_MAIN_RENDERER_EVENT_NAME.fullscreen, async({ params: isFullscreen }) => {
global.lx.event_app.main_window_fullscreen(isFullscreen)
return setFullScreen(isFullscreen)
})
@@ -79,7 +83,7 @@ export default () => {
mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.clear_cache, async() => {
return await clearCache()
await clearCache()
})
mainHandle<number>(WIN_MAIN_RENDERER_EVENT_NAME.get_cache_size, async() => {

View File

@@ -13,7 +13,7 @@ export default () => {
await global.lx.worker.dbService.downloadInfoUpdate(list)
})
mainHandle<string[]>(WIN_MAIN_RENDERER_EVENT_NAME.download_list_remove, async({ params: ids }) => {
return await global.lx.worker.dbService.downloadInfoRemove(ids)
await global.lx.worker.dbService.downloadInfoRemove(ids)
})
mainHandle(WIN_MAIN_RENDERER_EVENT_NAME.download_list_clear, async() => {
await global.lx.worker.dbService.downloadInfoClear()

View File

@@ -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)

View File

@@ -6,7 +6,10 @@ import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
const handleInflate = async(data: Buffer) => {
return await new Promise((resolve: (result: Buffer) => void, reject) => {
inflate(data, (err, result) => {
if (err) return reject(err)
if (err) {
reject(err)
return
}
resolve(result)
})
})

View 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)
})
}

Some files were not shown because too many files have changed in this diff Show More