Compare commits

...

117 Commits

Author SHA1 Message Date
lyswhut
b726ee42f7 Merge branch 'dev' 2021-10-02 13:43:46 +08:00
lyswhut
6b71b4709e 发布v1.14.0 2021-10-02 13:43:19 +08:00
lyswhut
0701b78720 优化英文歌曲名匹配 2021-10-01 22:47:55 +08:00
lyswhut
82d1ac1e83 更新依赖 2021-10-01 22:18:50 +08:00
lyswhut
8acf98631f 更新版本号 2021-10-01 22:16:08 +08:00
lyswhut
0a31d19561 新增重复歌曲列表 2021-10-01 22:15:45 +08:00
lyswhut
91279922f1 新增歌词文本选择复制功能 2021-10-01 12:30:07 +08:00
lyswhut
96804ae2e2 更新版本号 2021-09-30 16:45:20 +08:00
lyswhut
3ae8a89961 更新依赖 2021-09-30 16:44:32 +08:00
lyswhut
f8f13fd32c 优化歌词处理 2021-09-30 14:56:35 +08:00
lyswhut
ae61d2c629 调整位置 2021-09-30 14:56:24 +08:00
lyswhut
b1918e8b15 新增删除列表前的确认弹窗 2021-09-30 14:45:14 +08:00
lyswhut
ce7bca5b36 新增单个列表导入/导出功能 2021-09-30 14:27:32 +08:00
lyswhut
51be62bc29 更新依赖 2021-09-30 08:52:14 +08:00
Unintended
c1ff3971de Typo (#643) 2021-09-25 11:16:56 +08:00
Wei Yijun
57db286405 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题 (#636) 2021-09-22 10:33:48 +08:00
lyswhut
592b670e96 更新版本号 2021-09-18 17:31:21 +08:00
lyswhut
97fdcaac51 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示 2021-09-18 17:30:46 +08:00
lyswhut
271d9ac13e 更新依赖 2021-09-15 17:08:38 +08:00
lyswhut
b6cea110a7 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题 2021-09-11 15:39:02 +08:00
lyswhut
4b5dae4779 修复点击播放详情页的进度条跳进度时会出现偏移的问题 2021-09-10 14:31:07 +08:00
lyswhut
a250317a28 修复mg排行榜无法加载的问题 2021-09-09 16:45:10 +08:00
lyswhut
a4d3c06119 Merge branch 'dev' 2021-09-05 12:16:56 +08:00
lyswhut
246412cb2c 发布v1.13.0 2021-09-05 12:14:17 +08:00
lyswhut
2fb013fb41 修正排版 2021-09-05 12:13:44 +08:00
lyswhut
6f9d684b2d 复从电脑浏览器复制的企鹅歌单链接无法打开的问题 2021-09-05 12:13:32 +08:00
lyswhut
af6a2ec581 更新依赖 2021-09-04 21:24:42 +08:00
lyswhut
488b28921f 添加“Windows 7 下桌面歌词字体列表为空”问题解答 2021-08-29 21:31:44 +08:00
lyswhut
4c23fc1011 移除eiows包 2021-08-29 18:32:03 +08:00
lyswhut
db1c903f1b 更新版本号 2021-08-29 18:27:38 +08:00
lyswhut
3fdf7411fc 新增桌面歌词字体设置 2021-08-29 18:27:01 +08:00
lyswhut
1da275ff3a 更新依赖 2021-08-28 13:25:34 +08:00
lyswhut
bc5167f38d 使用中文括号 2021-08-26 16:21:26 +08:00
lyswhut
609b6f3123 更新版本号 2021-08-26 16:00:43 +08:00
lyswhut
fe35081e03 新增“下载的歌词文件编码格式”设置 2021-08-26 16:00:03 +08:00
lyswhut
72df1a35a0 更新推荐的DNS值 2021-08-26 08:54:58 +08:00
lyswhut
90ffcf5071 更新依赖 2021-08-25 16:29:11 +08:00
lyswhut
35f60cc536 添加说明 2021-08-24 08:46:42 +08:00
lyswhut
31bb40c50b 更新版本号 2021-08-23 11:25:53 +08:00
lyswhut
bc7b35f816 修复无法从歌单界面打开网易歌单详情的问题 2021-08-23 11:23:42 +08:00
lyswhut
0c76612a4c 更新版本号 2021-08-22 13:10:28 +08:00
lyswhut
ab86674aa3 软件内快捷键的最小化触发时,如果已启用托盘,则隐藏程序,否则最小化程序 2021-08-22 10:26:59 +08:00
lyswhut
8cd5a01dd5 更新依赖 2021-08-20 21:59:16 +08:00
lyswhut
4ef5bafcca 更新依赖 2021-08-20 21:56:15 +08:00
lyswhut
2c783e0657 修复错误 2021-08-20 11:15:51 +08:00
lyswhut
57a8b7842e 修复某些情况下列表同步不一致的问题 2021-08-19 15:23:29 +08:00
lyswhut
4b5a7b6a74 添加测试群 2021-08-18 17:04:38 +08:00
lyswhut
c09c31767d 更新依赖 2021-08-18 16:10:13 +08:00
lyswhut
ecff04618b 支持网易源“我喜欢”歌单以注入token的方式打开 2021-08-18 14:03:00 +08:00
lyswhut
c377469dad 歌曲搜索框新增清理按钮 2021-08-16 15:46:43 +08:00
lyswhut
ecb41382cb 更新常见问题 2021-08-16 09:10:23 +08:00
lyswhut
3150b12b99 修复重名问题 2021-08-15 19:45:11 +08:00
lyswhut
364260b070 修复某些情况下同步功能会导致切歌混乱的问题 2021-08-12 10:40:45 +08:00
lyswhut
5ccf6f077d Merge branch 'dev' 2021-08-11 17:07:02 +08:00
lyswhut
5b113c5c22 发布v1.12.2 2021-08-11 17:06:18 +08:00
lyswhut
ff53900ea3 修复同步服务器启动失败时,关闭同步服务不会清空失败信息的问题 2021-08-11 16:39:33 +08:00
lyswhut
b0b0403d9a 移除多余内容 2021-08-11 15:23:20 +08:00
lyswhut
21c3b3ab57 修复上一曲切换问题 2021-08-11 13:28:51 +08:00
lyswhut
e85709a341 修复稍后播放切歌问题 2021-08-11 11:57:41 +08:00
lyswhut
04be42a4d6 添加规则 2021-08-11 10:22:15 +08:00
lyswhut
8196e8a84d 更新依赖 2021-08-11 10:00:04 +08:00
lyswhut
1f85492690 修复播放下载列表的歌曲时切歌、歌词显示问题 2021-08-11 09:59:47 +08:00
lyswhut
e15178150b 移除日志 2021-08-08 12:16:22 +08:00
lyswhut
d83aee2507 Merge branch 'dev' 2021-08-08 11:54:46 +08:00
lyswhut
354a9904e9 发布v1.12.1 2021-08-08 11:54:21 +08:00
lyswhut
fe3379ffbb 修复随机播放下无法切歌的问题 2021-08-08 11:54:13 +08:00
lyswhut
0164936038 Merge branch 'dev' 2021-08-08 10:29:09 +08:00
lyswhut
9106d6455f 发布v1.12.0 2021-08-08 10:28:43 +08:00
lyswhut
5ddea2b1da 修复图片为空时设置歌曲信息出错的问题 2021-08-07 19:35:13 +08:00
lyswhut
2549c5587e 更新依赖 2021-08-07 13:45:44 +08:00
lyswhut
c9f28d8105 更新依赖 2021-08-05 10:39:59 +08:00
lyswhut
75b2db4749 添加播放器对系统媒体控制与显示的兼容处理 2021-08-05 10:21:20 +08:00
lyswhut
b6f291d721 添加提示语 2021-08-02 14:32:45 +08:00
lyswhut
4f7e227392 移除日志输出 2021-08-01 20:12:38 +08:00
lyswhut
7d95eb34c7 修复同步服务启动失败刷新连接码按钮仍然可以点击的问题 2021-08-01 20:11:38 +08:00
lyswhut
7e3ee38920 修复删除列表中正在播放的歌曲时不会自动切歌的问题 2021-08-01 20:07:40 +08:00
lyswhut
d44c2c26a8 修复beta版MAC包构建失败的问题 2021-08-01 13:46:16 +08:00
lyswhut
d77a41c20f 更新版本 2021-08-01 13:35:58 +08:00
lyswhut
c50c747691 完善代码 2021-08-01 13:28:12 +08:00
lyswhut
bcf3dde60d 完成同步功能 2021-08-01 12:57:13 +08:00
lyswhut
33ec3de1b4 更新依赖 2021-08-01 12:46:17 +08:00
lyswhut
26be5b5c4f 移除多余依赖 2021-07-23 22:01:34 +08:00
lyswhut
56af57c682 修复某些情况下所显示的歌词、封面图片与当前正在播放的歌曲不一致的问题 2021-07-23 16:14:56 +08:00
lyswhut
3a5cb3f7c2 修复导入kg歌单最多只能加载100、500首歌曲的问题 2021-07-21 15:33:41 +08:00
lyswhut
724c6be85b 禁用win arm版本的自动下载更新 2021-07-18 14:56:18 +08:00
lyswhut
1c4a56f95b Merge branch 'dev' 2021-07-18 13:59:58 +08:00
lyswhut
9c21e96538 发布v1.11.0 2021-07-18 13:56:26 +08:00
lyswhut
67074bd682 更新依赖 2021-07-17 16:40:52 +08:00
lyswhut
b535987392 更新依赖 2021-07-14 14:27:59 +08:00
lyswhut
d5c0c2e3d3 更新依赖 2021-07-10 12:26:14 +08:00
lyswhut
7580c79c00 替换过时API 2021-07-07 15:02:46 +08:00
lyswhut
c0a13622b0 更新依赖 2021-07-06 16:51:18 +08:00
lyswhut
58d7326f77 优化mg源打开歌单的链接兼容 2021-07-06 16:51:13 +08:00
lyswhut
45d7a12d0d 更新依赖 2021-07-05 10:08:44 +08:00
lyswhut
40b4f15340 更新说明 2021-07-02 14:58:14 +08:00
lyswhut
9c28ff67c1 更新 2021-07-02 10:28:32 +08:00
lyswhut
e3f146dabf 移除wy源歌单列表按“最新”排序的按钮 2021-06-30 11:55:32 +08:00
lyswhut
2b2a6a7f10 更新依赖 2021-06-30 11:44:51 +08:00
lyswhut
192574dfe8 更新依赖 2021-06-27 14:47:19 +08:00
lyswhut
8e95e06b2d 添加网易歌单无法打开的说明 2021-06-24 14:50:04 +08:00
lyswhut
1963b3ee71 修复mg源搜索失效的问题 2021-06-19 13:26:58 +08:00
lyswhut
507f66b3b6 修复dns查询问题 2021-06-18 15:13:15 +08:00
lyswhut
94bf9eae8b 更新依赖 2021-06-17 15:38:03 +08:00
lyswhut
0fa32f976c 移除日志输出 2021-06-16 16:24:39 +08:00
lyswhut
198238d4b5 优化网络请求,尝试去解决无法连接服务器的问题 2021-06-16 10:23:48 +08:00
lyswhut
66631ac105 修复进度条高度问题 2021-06-14 13:52:46 +08:00
lyswhut
19c5996351 更新依赖 2021-06-11 16:25:36 +08:00
lyswhut
cd3140a9c7 新增“添加歌曲到列表时的位置”设置 2021-06-11 15:45:09 +08:00
lyswhut
56201d6a01 更新依赖 2021-06-10 18:10:52 +08:00
lyswhut
654cb41e3e 添加 win arm64 架构的安装包构建 2021-06-07 09:04:34 +08:00
lyswhut
fa744d9204 添加i18n-ally扩展设置 2021-06-06 20:31:09 +08:00
lyswhut
3e1c56ff82 修复打包失败的问题 2021-06-05 12:18:43 +08:00
lyswhut
5d5164c4e9 更新依赖 2021-06-04 16:08:15 +08:00
lyswhut
0791e760a4 修复打开kw歌单失败时会无限重试的问题 2021-06-03 15:12:53 +08:00
lyswhut
b53fe2dd5e 优化wy源歌单导入匹配 2021-06-03 14:50:37 +08:00
lyswhut
1648b0c2dd 更新依赖 2021-05-31 17:12:53 +08:00
lyswhut
1a5adfabf4 更新依赖 2021-05-27 08:44:37 +08:00
123 changed files with 8628 additions and 5455 deletions

View File

@@ -16,7 +16,9 @@
"no-multiple-empty-lines": [1, {"max": 2}],
"comma-dangle": [2, "always-multiline"],
"standard/no-callback-literal": "off",
"prefer-const": "off"
"prefer-const": "off",
"no-labels": "off",
"node/no-callback-literal": "off"
},
"settings": {
"html/html-extensions": [".html", ".vue"]

View File

@@ -52,6 +52,14 @@ jobs:
name: lx-music-desktop-x86-Setup
path: build/* x86 Setup.exe
- name: Build Package Setup arm64
run: npm run pack:win:setup:arm64
- name: Upload Artifact Setup arm64
uses: actions/upload-artifact@v2
with:
name: lx-music-desktop-arm64-Setup
path: build/* arm64 Setup.exe
- name: Build Package Setup x86_64
run: npm run pack:win:setup:x86_64
- name: Upload Artifact Setup x86_64
@@ -116,8 +124,8 @@ jobs:
- name: Build Package dmg
run: |
npm run publish:mac:dmg
npm run publish:mac:dmg:arm64
npm run pack:mac:dmg
npm run pack:mac:dmg:arm64
env:
ELECTRON_CACHE: $HOME/.cache/electron
ELECTRON_BUILDERCACHE: $HOME/.cache/electron-builder

View File

@@ -40,6 +40,7 @@ jobs:
run: |
npm run publish:win:setup:x64:always
npm run publish:win:setup:x86
npm run publish:win:setup:arm64
npm run publish:win:setup:x86_64
npm run publish:win:7z:x64
npm run publish:win:7z:x86

8
.ncurc.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
upgrade: true,
reject: [
'vue-loader',
'webpack-dev-server',
// 'eslint-config-standard'
]
}

12
.vscode/settings.json vendored
View File

@@ -5,5 +5,15 @@
"@lyric/*": "${workspaceFolder}/src/renderer-lyric/*",
"@static/*": "${workspaceFolder}/src/static/*",
"@common/*": "${workspaceFolder}/src/common/*",
}
},
"i18n-ally.localesPaths": [
"src/renderer/lang"
],
"i18n-ally.displayLanguage": "zh-cn",
"i18n-ally.sourceLanguage": "zh-cn",
"i18n-ally.namespace": true,
"i18n-ally.translate.engines": ["google-cn", "google"],
"i18n-ally.pathMatcher": "{locale}/{namespaces}.json",
"i18n-ally.keystyle": "flat",
"i18n-ally.sortKeys": true
}

View File

@@ -6,6 +6,104 @@ 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/).
## [1.14.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.13.0...v1.14.0) - 2021-10-02
### 新增
- 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示
- 新增单个列表导入/导出功能,可以方便分享歌曲列表,可在右击“我的列表”里的列表名后弹出的菜单中使用
- 新增删除列表前的确认弹窗,防止误删列表
- 新增歌词文本选择复制功能,可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式,选择完成后可鼠标右击或者使用系统快捷键复制
- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在右击“我的列表”里的列表名后弹出的菜单中使用
### 修复
- 修复mg排行榜无法加载的问题
- 修复点击播放详情页的进度条跳进度时会出现偏移的问题
- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题
- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题
- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题
## [1.13.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.2...v1.13.0) - 2021-09-05
如果你喜欢并经常使用洛雪音乐并想要第一时间尝鲜洛雪的新功能可以加入测试企鹅群768786588
注意:测试版的功可能会不稳定,打算潜水的勿加。
### 新增
- 歌曲搜索框新增清理按钮,点击此按钮可以清理搜索框并返回初始搜索界面
- 新增“下载的歌词文件编码格式”设置,默认下载的歌词编码仍是`UTF-8`,对于某些在设备(如车机)上出现歌词中文乱码的用户可以尝试选择以`GBK`编码格式保存歌词文件
- 新增设置-桌面歌词-歌词字体设置此设置可用于设置桌面歌词的字体已知的问题Windows 7 下可能会出现字体列表为空的情况,这是当前系统的 Powershell 版本小于5.1导致的,请自行**尝试**看常见解决)
### 优化
- 支持网易源“我喜欢”歌单以注入token的方式打开。由于网易源的“我喜欢”歌单需要登录才能打开若你看不懂后半句就去阅读 常见问题-无法打开外部歌单),现若想要打开此类歌单,需要在歌单链接后面拼上 `###` 再加上有效的token拼接格式`[id|url]###token`例子最后面的xxxxxx替换成你的token`https://music.163.com/#/playlist?id=123456&userid=123456###xxxxxx`
- 软件内快捷键的最小化触发时,如果已启用托盘,则隐藏程序,否则最小化程序
### 修复
- 修复某些情况下同步功能会导致切歌混乱的问题
- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题
## [1.12.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.1...v1.12.2) - 2021-08-11
### 修复
- 修复播放下载列表的歌曲时切歌的问题
- 修复播放下载列表的歌曲时歌词无法显示的问题
- 修复下载列表稍后播放功能无效的问题
- 修复同步服务器启动失败时,关闭同步服务不会清空失败信息的问题
## [1.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.0...v1.12.1) - 2021-08-08
### 修复
- 修复随机播放下无法切歌的问题
## [1.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.11.0...v1.12.0) - 2021-08-08
### 新增
- 新增局域网同步功能实验性首次使用前建议先备份一次列表此功能需要配合PC端使用移动端与PC端处在同一个局域网路由器的网络下时可以多端实时同步歌曲列表使用问题请看"常见问题"。
### 优化
- 添加播放器对系统媒体控制与显示的兼容处理现在在windows下的锁屏界面可以正确显示当前播放的音乐信息及切换歌曲了
### 修复
- 修复导入kg歌单最多只能加载100、500首歌曲的问题。注现在可以加载1000+首歌曲的歌单但出于未知原因会导致部分歌曲无法加载可能是无版权导致的目前酷狗码仍然最多只能加载500首歌
- 修复某些情况下所显示的歌词、封面图片与当前正在播放的歌曲不一致的问题
## [1.11.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.2...v1.11.0) - 2021-07-18
### 新增
- 添加 win arm64 架构的安装包构建
- 新增“添加歌曲到列表时的位置”设置,可选项为列表的“顶部”与“底部”
### 优化
- 优化网络请求,尝试去解决无法连接服务器的问题
- 优化mg源打开歌单的链接兼容
### 修复
- 修复mg源搜索失效的问题
### 移除
- 因wy源的歌单列表已没有“最新”排序的选项所以现跟随移除wy源歌单列表按“最新”排序的按钮
### 变更
- 添加歌曲到列表时从原来的底部改为顶部,若你想要将你的列表歌曲顺序反转以适应这一变更,可先按住`shift`键的情况下点击列表的最后一首歌然后再点击列表的第一首歌完成倒序选中最后随便右击列表的任意一首歌在弹出的菜单中选择调整顺序在弹出框输入1后确定即可反转列表。
若你想要恢复原来的行为则可以去更改“添加歌曲到列表时的位置”设置项。
### 其他
- 更新electron到v13.1.7
## [1.10.2](https://github.com/lyswhut/lx-music-desktop/compare/v1.10.1...v1.10.2) - 2021-05-25
### 修复

87
FAQ.md
View File

@@ -13,13 +13,13 @@
尝试更换网络,如切换到移动网络,若移动网络还是不行则尝试开关下手机的飞行模式后再试,<br>
若使用家庭网络的话可尝试将光猫断电5分钟左右再通电联网后播放。
### 提示`getaddrinfo EAI_AGAIN ...`
### 提示 `getaddrinfo EAI_AGAIN ...` 或 `无法连接到服务器`
尝试在在浏览器打开这个地址`http://ts.tempmusic.tk`浏览器显示404是正常的如果不是404那就证明所在网络无法访问接口服务器。
若网页无法打开或打开来不是404则可能是DNS的问题可以尝试以下办法
1. 将DNS改成自动获取试试改完可能需要清理下系统DNS缓存才生效
2. 手动把DNS改一下不要用360的DNS可以把DNS改成`114.114.114.114``8.8.8.8`改完可能需要清理下系统DNS缓存才生效
2. 手动把DNS改一下不要用360的DNS可以把DNS改成`223.6.6.6``8.8.8.8`改完可能需要清理下系统DNS缓存才生效
改完DNS后可能需要重启软件才生效
@@ -38,7 +38,7 @@
若网页无法打开或打来不是404则应该是DNS的问题可以尝试以下办法
1. 将DNS改成自动获取试试
2. 手动把DNS改一下不要用360的DNS可以把DNS改成`114.114.114.114``8.8.8.8`
2. 手动把DNS改一下不要用360的DNS可以把DNS改成`223.6.6.6``8.8.8.8`
## 列表多选
@@ -58,10 +58,69 @@
## 无法打开外部歌单
不支持源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
不支持源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
对于分享出来的歌单若打开失败可尝试先在浏览器中打开后再从浏览器地址栏复制URL地址到软件打开<br>
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>
注:网易源的“我喜欢”歌单无法在未登录的情况下打开,所以你需要手动创建一个歌单后将“我喜欢”里的歌曲移动到该歌单打开
### 打开网易源“我喜欢”歌单
由于网易源的“我喜欢”歌单需要登录才能打开从v1.13.0起提供了可以以注入token的方式打开网易源“我喜欢”歌单的功能现若想要打开此类歌单需要在歌单链接或id后面拼上 `###` 再加上有效的token拼接格式`[id|url]###token`例子最后面的xxxxxx替换成你的token
- `https://music.163.com/#/playlist?id=11332&userid=123456###xxxxxx`
- `11332###xxxxxx`
即:将 `歌单链接或者歌单ID``###``token` 这三者拼到一起。
#### `token`的获取方法
**注:`token`是你账号的临时身份令牌,不要随便泄露给他人**<br>
在浏览器打开登录网易云音乐并**登录**后,按`F12`,此时将会打开开发者窗口,然后按你使用的浏览器操作:
##### Chrome、360、QQ等浏览器
这些浏览器打开此窗口时界面可能是中文也可能是英文,英文的话按括号里的来
1. 点击窗口顶部`应用程序(application)`
2. 展开左侧 `Cookies`
3. 点击 `https://music.163.com`
4. 在右侧窗口找到 `名称(Name)``MUSIC_U` 的这行,这行的第二列(`值(Value)`)内的那串内容就是`token`,双击它进入编辑状态,然后按`ctrl + c`键就可以将它复制
##### 火狐浏览器
1. 点击窗口顶部`存储`
2. 展开左侧 `Cookie`
3. 点击 `https://music.163.com`
4. 在右侧窗口找到 `名称``MUSIC_U` 的这行,这行的最后一列(`值`)内的那串内容就是`token`,双击它进入编辑状态,然后按`ctrl + c`键就可以将它复制
## 更新已收藏的在线歌单
该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效,可右击已收藏的列表名从弹出的菜单中选择“更新”使用该功能,
需要注意的是:这将会覆盖本地的目标列表,歌曲将被替换成最新的在线列表。
## 同步功能的使用(实验性,首次使用前建议先备份一次列表)
**注意:由于同步传输时的数据是明文传输,请在受信任的网络下使用此功能!**<br>
此功能需要配合移动端使用PC端与移动端处在同一个局域网路由器的网络下时可以多端实时同步歌曲列表使用方法
1. 在PC端的设置-数据同步开启同步功能(这时如果出现安全软件、防火墙等提示网络连接弹窗时需要点击允许)
2. 在移动端的设置-同步-同步服务器地址输入PC端显示的同步服务器地址如果显示可以多个则输入与**移动端上显示的本机地址**最相似的那个端口号与PC端的同步端口一致**输入完毕后需要按一下键盘上的回车键使输入的内容生效**
3. 输入完这两项后点击“启动同步”
4. 若连接成功对于首次同步时若两边的设备的列表不为空则PC端会弹出选择列表同步方式的弹窗同步方式的说明弹窗下面有介绍
#### 关于同步弹窗的说明
对于首次同步时若两边的设备的列表不为空则PC端会弹出选择列表同步方式的弹窗此弹窗内的同步方式仅针对**首次同步**<br>
第一次同步成功后,以后再同步时将会自动根据两边设备的列表内容合并同步,不信你可以在同步完成后断开两边的连接,然后在两边增删一些歌曲或列表后再同步试试看~😉
#### 连接同步服务失败的可能原因
- 此功能需要PC端与移动端都连接在同一个路由器下的网络才能使用
- 检查防火墙是否拦截了PC端的服务端口
- 路由器若开启了AP隔离则此功能无法使用
## 界面异常(界面显示不完整)
### Windows 7 下界面异常
@@ -97,6 +156,14 @@
- 添加启动参数`-dha`运行(添加的方法请自行百度“给快捷方式加参数”)
- 尝试将绿色版的软件放在**桌面**或**我的文档**运行
## Windows 7 下桌面歌词字体列表为空
Windows 7 系统系统需要安装 Powershell 5.1及以上版本才可正常获取系统字体列表。
想要查看当前 Powershell 版本可以在 Powershell 窗口输入命令:`Get-Host`
最新 Powershell 安装包可以去官方 [Github releases](https://github.com/PowerShell/PowerShell/releases) 页下载,安装过程中若出现错误,请自行按照提示或者百度/Google解决。
## 安装版安装失败,提示安装程序并未成功地运行完成
对于部分电脑出现安装失败的问题,可以做出以下尝试:
@@ -152,9 +219,9 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看上面的
**`.dmg`** 结尾的文件为 MAC 版本;
**`.AppImage`**、**`.deb`** 结尾的为 Linux 版本。
**`.AppImage`**、**`.deb`**、**`.rpm`**、**`.pacman`** 结尾的为 Linux 版本。
带有`x64`的为64位的系统版本`x86`的为32位的系统版本若两个都带有的则为集合版安装时会自动根据系统位数选择对应的版本安装。
带有`x64`的为64位的系统版本`x86`的为32位的系统版本若两个都带有的则为集合版安装时会自动根据系统位数选择对应的版本安装;带有`arm`的为arm架构系统的版本
## 软件更新
@@ -162,13 +229,13 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看上面的
若下载未完成时软件被关闭,下次启动软件会再次自动下载。<br>
若还是**更新失败**可能是无法访问GitHub导致的这时需要手动更新即下载最新安装包直接覆盖安装即可。<br>
注意:**绿色版**的软件自动更新功能**不可用**,建议使用安装版!!<br>
注意:**Mac版**、**Linux deb**版不支持自动更新!
注意:**Mac版**、**Linux**版不支持自动更新!
## 更新已收藏的在线歌单
### Windows 安装版在升级后,卸载了旧版本,但没有安装新版本
该功能仅对直接从歌单详情页点“收藏”按钮收藏的歌单有效,可右击已收藏的列表名从弹出的菜单中选择“同步”使用该功能,
出现这个问题的原因一般是你当初在安装本软件的时候是以管理员身份安装的,运行软件的时候没有以管理员身份运行,所以卸载后无法再装上。
需要注意的是:这将会覆盖本地的目标列表,歌曲将被替换成最新的在线列表
安装本软件时建议选择 `为当前用户安装`,并安装在当前用户目录或者安装在不需要管理员权限的目录(即其他分区下),不要选`为所有用户安装`
## 缺少`xxx.dll`

View File

@@ -36,7 +36,7 @@
所用技术栈:
- Electron 12
- Electron 13
- Vue 2
已支持的平台:

View File

@@ -2,7 +2,8 @@ const isDev = process.env.NODE_ENV === 'development'
module.exports = {
modules: {
localIdentName: isDev ? '[folder]-[name]--[local]--[hash:base64:5]' : '[hash:base64:5]',
localIdentName: isDev ? '[path][name]__[local]--[hash:base64:5]' : '[hash:base64:5]',
exportLocalsConvention: 'camelCase',
},
sourceMap: isDev,
}

View File

@@ -29,11 +29,10 @@ module.exports = {
exclude: /node_modules/,
enforce: 'pre',
},
// {
// test: /\.js$/,
// loader: 'babel-loader',
// exclude: /node_modules/,
// },
{
test: /\.node$/,
use: 'node-loader',
},
],
},
performance: {

View File

@@ -15,6 +15,7 @@ module.exports = merge(baseConfig, {
},
externals: [
...Object.keys(dependencies || {}),
// 'font-list',
],
node: {
__dirname: false,

View File

@@ -18,7 +18,7 @@ module.exports = {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../../dist/electron'),
publicPath: './',
publicPath: 'auto',
},
resolve: {
alias: {
@@ -44,6 +44,10 @@ module.exports = {
exclude: /node_modules/,
enforce: 'pre',
},
{
test: /\.node$/,
use: 'node-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
@@ -72,15 +76,6 @@ module.exports = {
},
}),
},
{
test: /\.styl(:?us)?$/,
oneOf: mergeCSSLoader({
loader: 'stylus-loader',
options: {
sourceMap: true,
},
}),
},
{
test: /\.pug$/,
oneOf: [
@@ -97,26 +92,38 @@ module.exports = {
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'imgs/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'media/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'fonts/[name]-[contenthash:8][ext]',
},
},
],

View File

@@ -18,7 +18,7 @@ module.exports = {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../../dist/electron'),
publicPath: './',
publicPath: 'auto',
},
resolve: {
alias: {
@@ -44,6 +44,10 @@ module.exports = {
exclude: /node_modules/,
enforce: 'pre',
},
{
test: /\.node$/,
use: 'node-loader',
},
{
test: /\.vue$/,
loader: 'vue-loader',
@@ -72,15 +76,6 @@ module.exports = {
},
}),
},
{
test: /\.styl(:?us)?$/,
oneOf: mergeCSSLoader({
loader: 'stylus-loader',
options: {
sourceMap: true,
},
}),
},
{
test: /\.pug$/,
oneOf: [
@@ -97,26 +92,38 @@ module.exports = {
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'imgs/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'media/[name]-[contenthash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]',
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10000,
},
},
generator: {
filename: 'fonts/[name]-[contenthash:8][ext]',
},
},
],

View File

@@ -16,7 +16,8 @@ const rendererLyricConfig = require('./renderer-lyric/webpack.config.dev')
let electronProcess = null
let manualRestart = false
let hotMiddleware
let hotMiddlewareRenderer
let hotMiddlewareRendererLyric
function startRenderer() {
@@ -24,7 +25,7 @@ function startRenderer() {
// rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)
// rendererConfig.mode = 'development'
const compiler = webpack(rendererConfig)
hotMiddleware = webpackHotMiddleware(compiler, {
hotMiddlewareRenderer = webpackHotMiddleware(compiler, {
log: false,
heartbeat: 2500,
})
@@ -32,15 +33,15 @@ function startRenderer() {
compiler.hooks.compilation.tap('compilation', compilation => {
// console.log(Object.keys(compilation.hooks))
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({ action: 'reload' })
hotMiddlewareRenderer.publish({ action: 'reload' })
cb()
})
})
compiler.hooks.done.tap('done', stats => {
// logStats('Renderer', 'Compile done')
// logStats('Renderer', stats)
})
// compiler.hooks.done.tap('done', stats => {
// // logStats('Renderer', 'Compile done')
// // logStats('Renderer', stats)
// })
const server = new WebpackDevServer(
compiler,
@@ -54,7 +55,7 @@ function startRenderer() {
errors: true,
},
before(app, ctx) {
app.use(hotMiddleware)
app.use(hotMiddlewareRenderer)
ctx.middleware.waitUntilValid(() => {
resolve()
})
@@ -71,7 +72,7 @@ function startRendererLyric() {
// rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)
// rendererConfig.mode = 'development'
const compiler = webpack(rendererLyricConfig)
hotMiddleware = webpackHotMiddleware(compiler, {
hotMiddlewareRendererLyric = webpackHotMiddleware(compiler, {
log: false,
heartbeat: 2500,
})
@@ -79,15 +80,15 @@ function startRendererLyric() {
compiler.hooks.compilation.tap('compilation', compilation => {
// console.log(Object.keys(compilation.hooks))
HtmlWebpackPlugin.getHooks(compilation).beforeEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({ action: 'reload' })
hotMiddlewareRendererLyric.publish({ action: 'reload' })
cb()
})
})
compiler.hooks.done.tap('done', stats => {
// logStats('Renderer', 'Compile done')
// logStats('Renderer', stats)
})
// compiler.hooks.done.tap('done', stats => {
// // logStats('Renderer', 'Compile done')
// // logStats('Renderer', stats)
// })
const server = new WebpackDevServer(
compiler,
@@ -101,7 +102,7 @@ function startRendererLyric() {
errors: true,
},
before(app, ctx) {
app.use(hotMiddleware)
app.use(hotMiddlewareRendererLyric)
ctx.middleware.waitUntilValid(() => {
resolve()
})
@@ -120,7 +121,8 @@ function startMain() {
const compiler = webpack(mainConfig)
compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {
hotMiddleware.publish({ action: 'compiling' })
hotMiddlewareRenderer.publish({ action: 'compiling' })
hotMiddlewareRendererLyric.publish({ action: 'compiling' })
done()
})

8917
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,16 @@
{
"name": "lx-music-desktop",
"version": "1.10.2",
"version": "1.14.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
"scripts": {
"pack": "node build-config/pack.js && npm run pack:win:setup:x64",
"pack:win": "node build-config/pack.js && npm run pack:win:setup:x86_64 && npm run pack:win:setup:x64 && npm run pack:win:7z",
"pack:win": "node build-config/pack.js && npm run pack:win:setup:x64 && npm run pack:win:setup:x86 && npm run pack:win:setup:arm64 && npm run pack:win:setup:x86_64 && npm run pack:win:7z",
"pack:win:setup:x86_64": "cross-env TARGET=Setup ARCH=x86_64 electron-builder -w=nsis --x64 --ia32 -p never",
"pack:win:setup:x64": "cross-env TARGET=Setup ARCH=x64 electron-builder -w=nsis --x64 -p never",
"pack:win:setup:x86": "cross-env TARGET=Setup ARCH=x86 electron-builder -w=nsis --ia32 -p never",
"pack:win:setup:arm64": "cross-env TARGET=Setup ARCH=arm64 electron-builder -w=nsis --arm64 -p never",
"pack:win:portable": "npm run pack:win:portable:x86_64 && npm run pack:win:portable:x64 && npm run pack:win:portable:x86",
"pack:win:portable:x86_64": "cross-env TARGET=portable ARCH=x86_64 electron-builder -w=portable --x64 --ia32 -p never",
"pack:win:portable:x64": "cross-env TARGET=portable ARCH=x64 electron-builder -w=portable --x64 -p never",
@@ -35,6 +36,7 @@
"publish:win:setup:x64:always": "cross-env TARGET=Setup ARCH=x64 electron-builder -w=nsis --x64 -p always",
"publish:win:setup:x64": "cross-env TARGET=Setup ARCH=x64 electron-builder -w=nsis --x64 -p always",
"publish:win:setup:x86": "cross-env TARGET=Setup ARCH=x86 electron-builder -w=nsis --ia32 -p onTagOrDraft",
"publish:win:setup:arm64": "cross-env TARGET=Setup ARCH=arm64 electron-builder -w=nsis --arm64 -p onTagOrDraft",
"publish:win:setup:x86_64": "cross-env TARGET=Setup ARCH=x86_64 electron-builder -w=nsis --x64 --ia32 -p onTagOrDraft",
"publish:win:portable": "npm run publish:win:portable:x86_64 && npm run publish:win:portable:x64 && npm run publish:win:portable:x86",
"publish:win:portable:x86_64": "cross-env TARGET=portable ARCH=x86_64 electron-builder -w=portable --x64 --ia32 -p onTagOrDraft",
@@ -68,7 +70,7 @@
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://localhost:1081 npm i"
},
"browserslist": [
"Electron 12.0.9"
"Electron 13.3.0"
],
"engines": {
"node": ">= 14"
@@ -81,6 +83,9 @@
"files": [
"dist/electron/**/*"
],
"asar": {
"smartUnpack": false
},
"extraResources": [
"./licenses"
],
@@ -161,50 +166,51 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.14.3",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/core": "^7.15.5",
"@babel/plugin-proposal-class-properties": "^7.14.5",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.14.0",
"@babel/plugin-transform-runtime": "^7.14.3",
"@babel/plugin-transform-modules-umd": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.14.2",
"@babel/preset-env": "^7.15.6",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-minify-webpack-plugin": "^0.3.1",
"babel-preset-minify": "^0.5.1",
"browserslist": "^4.16.6",
"cfonts": "^2.9.2",
"chalk": "^4.1.1",
"browserslist": "^4.17.2",
"cfonts": "^2.10.0",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^9.0.0",
"core-js": "^3.12.1",
"copy-webpack-plugin": "^9.0.1",
"core-js": "^3.18.1",
"cross-env": "^7.0.3",
"css-loader": "^5.2.6",
"css-minimizer-webpack-plugin": "^3.0.0",
"css-loader": "^6.3.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"del": "^6.0.0",
"electron": "^12.0.9",
"electron-builder": "^22.11.4",
"electron": "^13.4.0",
"electron-builder": "^22.11.7",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"eslint": "^7.27.0",
"eslint-config-standard": "^14.1.1",
"electron-to-chromium": "^1.3.856",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-standard": "^4.1.0",
"file-loader": "^6.2.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^5.3.1",
"html-webpack-plugin": "^5.3.2",
"less": "^4.1.1",
"less-loader": "^9.0.0",
"less-loader": "^10.0.1",
"less-plugin-clean-css": "^1.5.1",
"markdown-it": "^12.0.6",
"mini-css-extract-plugin": "^1.6.0",
"postcss": "^8.3.0",
"postcss-loader": "^5.3.0",
"markdown-it": "^12.2.0",
"mini-css-extract-plugin": "^2.3.0",
"node-loader": "^2.0.0",
"postcss": "^8.3.8",
"postcss-loader": "^6.1.1",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
"pug-loader": "^2.4.0",
@@ -212,34 +218,37 @@
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"spinnies": "^0.5.1",
"stylus": "^0.54.8",
"stylus-loader": "^6.0.0",
"terser-webpack-plugin": "^5.1.2",
"terser-webpack-plugin": "^5.2.4",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.7",
"vue-template-compiler": "^2.6.12",
"webpack": "^5.37.1",
"webpack-cli": "^4.7.0",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.56.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-merge": "^5.7.3"
"webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"crypto-js": "^4.0.0",
"dnscache": "^1.0.2",
"electron-log": "^4.3.5",
"electron-store": "^8.0.0",
"bufferutil": "^4.0.4",
"crypto-js": "^4.1.1",
"electron-log": "^4.4.1",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"font-list": "git+https://github.com/lyswhut/node-font-list.git#c6caf4060e471afe143a4aca30d554644522966d",
"http-terminator": "^3.0.3",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.0",
"js-htmlencode": "^0.3.0",
"lrc-file-parser": "^1.0.7",
"needle": "^2.6.0",
"koa": "^2.13.3",
"long": "^4.0.0",
"lrc-file-parser": "^1.2.1",
"needle": "^3.0.0",
"node-id3": "^0.2.3",
"request": "^2.88.2",
"vue": "^2.6.12",
"vue-i18n": "^8.24.4",
"vue-router": "^3.5.1",
"socket.io": "^4.2.0",
"utf-8-validate": "^5.0.6",
"vue": "^2.6.14",
"vue-i18n": "^8.26.5",
"vue-router": "^3.5.2",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"
}

View File

@@ -15,7 +15,7 @@ module.exports = {
'*-height', '*-width',
'flex', '::-webkit-scrollbar',
'top', 'left', 'bottom', 'right',
'border-radius',
'border-radius', 'gap',
],
selectorBlackList: ['html', 'ignore-to-rem'],
replace: true,

View File

@@ -1,3 +1,15 @@
### 新增
- 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示
- 新增单个列表导入/导出功能,可以方便分享歌曲列表,可在右击“我的列表”里的列表名后弹出的菜单中使用
- 新增删除列表前的确认弹窗,防止误删列表
- 新增歌词文本选择复制功能,可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式,选择完成后可鼠标右击或者使用系统快捷键复制
- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在右击“我的列表”里的列表名后弹出的菜单中使用
### 修复
- 修复企鹅音乐搜索歌曲没有结果的问题
- 修复mg排行榜无法加载的问题
- 修复点击播放详情页的进度条跳进度时会出现偏移的问题
- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题
- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题
- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@ const path = require('path')
const os = require('os')
const defaultSetting = {
version: '1.0.41',
version: '1.0.45',
player: {
togglePlayMethod: 'listLoop',
highQuality: false,
@@ -26,6 +26,7 @@ const defaultSetting = {
theme: 0,
isLockScreen: true,
style: {
font: '',
fontSize: 120,
opacity: 95,
isZoomActiveLrc: true,
@@ -36,6 +37,7 @@ const defaultSetting = {
isShowSource: true,
prevSelectListId: 'default',
isSaveScrollLocation: true,
addMusicLocationType: 'top',
},
download: {
enable: false,
@@ -43,6 +45,7 @@ const defaultSetting = {
fileName: '歌名 - 歌手',
maxDownloadNum: 3,
isDownloadLrc: false,
lrcFormat: 'utf8',
isEmbedPic: true,
isEmbedLyric: false,
isUseOtherSource: false,
@@ -84,6 +87,10 @@ const defaultSetting = {
isToTray: false,
themeId: 0,
},
sync: {
enable: false,
port: '23332',
},
windowSizeId: 2,
themeId: 0,
langId: null,

View File

@@ -22,6 +22,7 @@ const names = {
set_app_setting: 'set_app_setting',
set_window_size: 'set_window_size',
show_save_dialog: 'show_save_dialog',
get_system_fonts: 'get_system_fonts',
handle_request: 'handle_request',
cancel_request: 'cancel_request',
@@ -32,6 +33,8 @@ const names = {
restart_window: 'restart_window',
lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
@@ -66,6 +69,14 @@ const names = {
get_music_url: 'get_music_url',
save_music_url: 'save_music_url',
clear_music_url: 'clear_music_url',
sync_enable: 'sync_enable',
sync_status: 'sync_status',
sync_get_status: 'sync_get_status',
sync_generate_code: 'sync_generate_code',
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
},
winLyric: {
close: 'close',

View File

@@ -10,6 +10,10 @@ class Common extends EventEmitter {
configStatus(name) {
this.emit(COMMON_EVENT_NAME.configStatus, name)
}
saveMyList(data) {
this.emit(COMMON_EVENT_NAME.saveMyList, data)
}
}
module.exports = Common

View File

@@ -1,6 +1,7 @@
exports.common = {
initConfig: 'initConfig',
configStatus: 'config',
saveMyList: 'saveMyList',
}
exports.mainWindow = {

View File

@@ -7,6 +7,7 @@ const WinLyric = require('./WinLyric')
const HotKey = require('./HotKey')
const { Event: UserApi } = require('../modules/userApi')
const { Event: Sync } = require('../modules/sync')
if (!global.lx_event.common) global.lx_event.common = new Common()
if (!global.lx_event.mainWindow) global.lx_event.mainWindow = new MainWindow()
@@ -15,3 +16,4 @@ if (!global.lx_event.winLyric) global.lx_event.winLyric = new WinLyric()
if (!global.lx_event.hotKey) global.lx_event.hotKey = new HotKey()
if (!global.lx_event.userApi) global.lx_event.userApi = new UserApi()
if (!global.lx_event.sync) global.lx_event.sync = new Sync()

View File

@@ -55,12 +55,12 @@ app.on('web-contents-created', (event, contents) => {
if (!navigationUrlWhiteList.some(url => url.test(navigationUrl))) return event.preventDefault()
console.log('navigation to url:', navigationUrl)
})
contents.on('new-window', async(event, navigationUrl) => {
event.preventDefault()
if (/^devtools/.test(navigationUrl)) return
console.log(navigationUrl)
if (!/^https?:\/\//.test(navigationUrl)) return
await shell.openExternal(navigationUrl)
contents.setWindowOpenHandler(({ url }) => {
if (!/^devtools/.test(url) && /^https?:\/\//.test(url)) {
shell.openExternal(url)
}
console.log(url)
return { action: 'deny' }
})
contents.on('will-attach-webview', (event, webPreferences, params) => {
// Strip away preload scripts if unused or verify their location is legitimate

View File

@@ -0,0 +1,23 @@
const { EventEmitter } = require('events')
const SYNC_EVENT_NAME = require('./name')
class Sync extends EventEmitter {
status(status) {
this.emit(SYNC_EVENT_NAME.status, status)
}
sync_list(data) {
this.emit(SYNC_EVENT_NAME.sync_list, data)
}
sync_handle_list(data) {
this.emit(SYNC_EVENT_NAME.sync_handle_list, data)
}
action_list(data) {
this.emit(SYNC_EVENT_NAME.sync_action_list, data)
}
}
module.exports = Sync

View File

@@ -0,0 +1,6 @@
module.exports = {
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
sync_handle_list: 'sync_handle_list',
status: 'status',
}

View File

@@ -0,0 +1,15 @@
const Event = require('./event/event')
const eventNames = require('./event/name')
const modules = require('./modules')
const { startServer, stopServer, getStatus, generateCode } = require('./server/server')
module.exports = {
startServer,
stopServer,
getStatus,
generateCode,
Event,
eventNames,
modules,
}

View File

@@ -0,0 +1 @@
exports.list = require('./list')

View File

@@ -0,0 +1,41 @@
const { encryptMsg, decryptMsg } = require('../server/utils')
let io
const handleListAction = ({ action, data }) => {
// console.log(action, data)
global.lx_event.sync.action_list({ action, data })
}
// const addMusic = (orderId, callback) => {
// // ...
// }
const broadcast = async(action, data, excludeIds = []) => {
if (!io) return
const sockets = await io.fetchSockets()
for (const socket of sockets) {
if (excludeIds.includes(socket.data.keyInfo.clientId)) continue
socket.emit(action, encryptMsg(socket.data.keyInfo, data))
}
}
exports.sendListAction = (action, data) => {
// io.sockets
return broadcast('list:action', JSON.stringify({ action, data }))
}
exports.registerListHandler = (_io, socket) => {
io = _io
socket.on('list:action', msg => {
// console.log(msg)
msg = decryptMsg(socket.data.keyInfo, msg)
if (!msg) return
handleListAction(JSON.parse(msg))
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)
}
exports.unregisterListHandler = () => {
io = null
}

View File

@@ -0,0 +1,69 @@
const { aesEncrypt, aesDecrypt, createClientKeyInfo, getClientKeyInfo, setClientKeyInfo } = require('./utils')
const authMsg = 'lx-music auth::'
const helloMsg = 'Hello~::^-^::'
exports.authCode = async(req, res, authCode) => {
let code = 401
let msg = 'Forbidden'
// console.log(req.headers)
if (req.headers.m) {
label:
if (req.headers.i) {
const keyInfo = getClientKeyInfo(req.headers.i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(req.headers.m, keyInfo.key, keyInfo.iv)
} catch (err) {
break label
}
console.log(text)
if (text.startsWith(authMsg)) {
code = 200
const deviceName = text.replace(authMsg, '') || 'Unknown'
if (deviceName != keyInfo.deviceName) {
keyInfo.deviceName = deviceName
setClientKeyInfo(keyInfo)
}
msg = aesEncrypt(helloMsg, keyInfo.key, keyInfo.iv)
}
} 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(authCode, key, iv)
let text
try {
text = aesDecrypt(req.headers.m, key, iv)
} catch (err) {
break label
}
console.log(text)
if (text.startsWith(authMsg)) {
code = 200
const deviceName = text.replace(authMsg, '') || 'Unknown'
msg = aesEncrypt(JSON.stringify(createClientKeyInfo(deviceName)), key, iv)
}
}
}
res.writeHead(code)
res.end(msg)
}
exports.authConnect = async req => {
const { i, t } = req._query
label:
if (i && t) {
const keyInfo = getClientKeyInfo(i)
if (!keyInfo) break label
let text
try {
text = aesDecrypt(t, keyInfo.key, keyInfo.iv)
} catch (err) {
break label
}
if (text == 'lx-music connect') return
}
throw new Error('failed')
}

View File

@@ -0,0 +1,7 @@
const { startServer, stopServer, getStatus } = require('./server')
module.exports = {
startServer,
stopServer,
getStatus,
}

View File

@@ -0,0 +1,172 @@
const http = require('http')
const sio = require('socket.io')
const { createHttpTerminator } = require('http-terminator')
const modules = require('../modules')
const { authCode, authConnect } = require('./auth')
const { getAddress, getServerId, generateCode, getClientKeyInfo } = require('./utils')
const syncList = require('./syncList')
let status = {
status: false,
message: '',
address: [],
code: '',
devices: [],
}
const handleConnection = (io, socket) => {
console.log('connection')
// console.log(socket.handshake.query)
for (const module of Object.values(modules)) {
module.registerListHandler(io, socket)
}
}
const authConnection = (req, callback) => {
// console.log(req.headers)
// // console.log(req.auth)
// console.log(req._query.authCode)
authConnect(req).then(() => {
callback(null, true)
}).catch(err => {
callback(err, false)
})
}
let httpTerminator = null
let io = null
const handleStartServer = (port = 9527) => new Promise((resolve, reject) => {
const httpServer = http.createServer((req, res) => {
// console.log(req.url)
let code
let msg
switch (req.url) {
case '/hello':
code = 200
msg = 'Hello~::^-^::'
break
case '/id':
code = 200
msg = 'OjppZDo6' + getServerId()
break
case '/ah':
authCode(req, res, status.code)
break
default:
code = 401
msg = 'Forbidden'
break
}
if (!code) return
res.writeHead(code)
res.end(msg)
})
httpTerminator = createHttpTerminator({
server: httpServer,
})
io = sio(httpServer, {
path: '/sync',
serveClient: false,
connectTimeout: 10000,
pingTimeout: 30000,
maxHttpBufferSize: 3e6,
allowRequest: authConnection,
transports: ['websocket'],
})
io.on('connection', async socket => {
socket.on('disconnect', reason => {
console.log('disconnect', reason)
status.devices.splice(status.devices.findIndex(k => k.clientId == keyInfo.clientId), 1)
global.lx_event.sync.status(status)
})
const keyInfo = getClientKeyInfo(socket.handshake.query.i)
// socket.lx_keyInfo = keyInfo
socket.data.keyInfo = keyInfo
try {
await syncList(io, socket)
} catch (err) {
console.log(err)
return
}
status.devices.push(keyInfo)
handleConnection(io, socket, keyInfo)
global.lx_event.sync.status(status)
})
httpServer.on('error', error => {
console.log(error)
reject(error)
})
httpServer.on('listening', () => {
const addr = httpServer.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
console.info(`Listening on ${bind}`)
resolve()
})
httpServer.listen(port)
})
const handleStopServer = async() => {
if (!httpTerminator) return
await io.close()
await httpTerminator.terminate().catch(() => {})
io = null
httpTerminator = null
}
exports.stopServer = async() => {
if (!status.status) {
status.status = false
status.message = ''
status.address = []
status.code = ''
global.lx_event.sync.status(status)
return
}
console.log('stoping sync server...')
return handleStopServer().then(() => {
console.log('sync server stoped')
status.status = false
status.message = ''
status.address = []
status.code = ''
}).catch(err => {
console.log(err)
status.message = err.message
}).finally(() => {
global.lx_event.sync.status(status)
})
}
exports.startServer = async port => {
if (status.status) await handleStopServer()
console.log('starting sync server...')
return handleStartServer(port).then(() => {
console.log('sync server started')
status.status = true
status.message = ''
status.address = getAddress()
status.code = generateCode()
}).catch(err => {
console.log(err)
status.status = false
status.message = err.message
status.address = []
status.code = ''
}).finally(() => {
global.lx_event.sync.status(status)
})
}
exports.getStatus = () => status
exports.generateCode = async() => {
status.code = generateCode()
global.lx_event.sync.status(status)
return status.code
}

View File

@@ -0,0 +1,394 @@
const path = require('path')
const fs = require('fs')
const fsPromises = fs.promises
const { app } = require('electron')
const { encryptMsg, decryptMsg } = require('./utils')
const SYNC_EVENT_NAMES = require('../event/name')
const { common: COMMON_EVENT_NAME } = require('@main/events/_name')
const { throttle } = require('@common/utils')
let io
let syncingId = null
const wait = (time = 1000) => new Promise((resolve, reject) => setTimeout(resolve, time))
const getRemoteListData = socket => new Promise((resolve, reject) => {
console.log('getRemoteListData')
const handleError = reason => {
reject(new Error(reason))
}
const handleSuccess = enData => {
socket.removeListener('disconnect', handleError)
socket.removeListener('list:sync', handleSuccess)
console.log('getRemoteListData', 'handleSuccess')
const data = JSON.parse(decryptMsg(socket.data.keyInfo, enData))
if (!data) return reject(new Error('Get remote list data failed'))
if (data.action != 'getData') return
resolve(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' })))
})
const getLocalListData = () => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'getData') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
action: 'getData',
})
})
const getSyncMode = keyInfo => new Promise((resolve, reject) => {
const handleSuccess = ({ action, data }) => {
if (action !== 'selectMode') return
global.lx_event.sync.off(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
resolve(data)
}
global.lx_event.sync.on(SYNC_EVENT_NAMES.sync_handle_list, handleSuccess)
global.lx_event.sync.sync_list({
action: 'selectMode',
data: keyInfo,
})
})
const finishedSync = socket => {
return socket.emit('list:sync', encryptMsg(socket.data.keyInfo, JSON.stringify({
action: 'finished',
})))
}
const setLocalList = listData => {
global.lx_event.sync.sync_list({
action: 'setData',
data: listData,
})
}
const setRemotelList = async(socket, listData) => {
if (!io) return
const sockets = 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 })))
}
}
let writeFilePromises = {}
const updateSnapshot = (path, data) => {
console.log('updateSnapshot', path)
let writeFilePromise = writeFilePromises[path] || Promise.resolve()
return writeFilePromise.then(() => {
writeFilePromise = writeFilePromises[path] = fsPromises.writeFile(path, data)
return writeFilePromise
})
}
const createListDataObj = listData => {
const listDataObj = {}
for (const list of listData.userList) listDataObj[list.id] = list
return listDataObj
}
const handleMergeList = (sourceList, targetList, addMusicLocationType) => {
let newList
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
break
}
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid]) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
return {
...sourceList,
list: ids.map(id => map[id]),
}
}
const mergeList = (sourceListData, targetListData) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const newListData = {}
newListData.defaultList = handleMergeList(sourceListData.defaultList, targetListData.defaultList, addMusicLocationType)
newListData.loveList = handleMergeList(sourceListData.loveList, targetListData.loveList, addMusicLocationType)
const listDataObj = createListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
if (targetList) {
targetList.list = handleMergeList(targetList, list, addMusicLocationType).list
} else {
newListData.userList.push(list)
}
}
return newListData
}
const overwriteList = (sourceListData, targetListData) => {
const newListData = {}
newListData.defaultList = sourceListData.defaultList
newListData.loveList = sourceListData.loveList
const listDataObj = createListDataObj(sourceListData)
newListData.userList = [...sourceListData.userList]
for (const list of targetListData.userList) {
const targetList = listDataObj[list.id]
if (targetList) continue
newListData.userList.push(list)
}
return newListData
}
const handleMergeListData = async socket => {
let isSelectingMode = false
const handleDisconnect = () => {
if (!isSelectingMode) return
global.lx_event.sync.sync_list({
action: 'closeSelectMode',
})
}
socket.on('disconnect', handleDisconnect)
isSelectingMode = true
const mode = await getSyncMode(socket.data.keyInfo)
isSelectingMode = false
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListData', 'remoteListData, localListData')
let listData
switch (mode) {
case 'merge_local_remote':
listData = mergeList(localListData, remoteListData)
break
case 'merge_remote_local':
listData = mergeList(remoteListData, localListData)
break
case 'overwrite_local_remote':
listData = overwriteList(localListData, remoteListData)
break
case 'overwrite_remote_local':
listData = overwriteList(remoteListData, localListData)
break
case 'overwrite_local_remote_full':
listData = localListData
break
case 'overwrite_remote_local_full':
listData = remoteListData
break
case 'none': return
case 'cancel':
socket.disconnect(true)
throw new Error('cancel')
}
return listData
}
const handleSyncList = async socket => {
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleSyncList', 'remoteListData, localListData')
const listData = {}
if (localListData.defaultList.list.length || localListData.loveList.list.length || localListData.userList.length) {
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.length || remoteListData.userList.length) {
const mergedList = await handleMergeListData(socket)
console.log('handleMergeListData', 'mergedList')
console.log(mergedList)
if (!mergedList) return
listData.defaultList = mergedList.defaultList
listData.loveList = mergedList.loveList
listData.userList = mergedList.userList
setLocalList(mergedList)
setRemotelList(socket, mergedList)
} else {
setRemotelList(socket, localListData)
listData.defaultList = localListData.defaultList
listData.loveList = localListData.loveList
listData.userList = localListData.userList
}
} else {
if (remoteListData.defaultList.list.length || remoteListData.loveList.list.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
}
}
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: listData.defaultList,
loveList: listData.loveList,
userList: listData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return listData
})
}
const mergeListDataFromSnapshot = (sourceList, targetList, snapshotList, addMusicLocationType) => {
const removedListIds = new Set()
const sourceListItemIds = new Set()
const targetListItemIds = new Set()
for (const m of sourceList.list) sourceListItemIds.add(m.songmid)
for (const m of targetList.list) targetListItemIds.add(m.songmid)
for (const m of snapshotList.list) {
if (!sourceListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
for (const m of snapshotList.list) {
if (!targetListItemIds.has(m.songmid)) removedListIds.add(m.songmid)
}
let newList
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...targetList.list, ...sourceList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...sourceList.list, ...targetList.list]
for (const item of newList) {
if (map[item.songmid] || removedListIds.has(item.songmid)) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
return {
...sourceList,
list: ids.map(id => map[id]),
}
}
const handleMergeListDataFromSnapshot = async(socket, snapshot) => {
const addMusicLocationType = global.appSetting.list.addMusicLocationType
const [remoteListData, localListData] = await Promise.all([getRemoteListData(socket), getLocalListData()])
console.log('handleMergeListDataFromSnapshot', 'remoteListData, localListData')
const newListData = {}
newListData.defaultList = mergeListDataFromSnapshot(localListData.defaultList, remoteListData.defaultList, snapshot.defaultList, addMusicLocationType)
newListData.loveList = mergeListDataFromSnapshot(localListData.loveList, remoteListData.loveList, snapshot.loveList, addMusicLocationType)
const localUserListData = createListDataObj(localListData)
const remoteUserListData = createListDataObj(remoteListData)
const snapshotUserListData = createListDataObj(snapshot)
const removedListIds = new Set()
const localUserListIds = new Set()
const remoteUserListIds = new Set()
for (const l of localListData.userList) localUserListIds.add(l.id)
for (const l of remoteListData.userList) remoteUserListIds.add(l.id)
for (const l of snapshot.userList) {
if (!localUserListIds.has(l.id)) removedListIds.add(l.id)
}
for (const l of snapshot.userList) {
if (!remoteUserListIds.has(l.id)) removedListIds.add(l.id)
}
let newUserList = []
for (const list of localListData.userList) {
if (removedListIds.has(list.id)) continue
const remoteList = remoteUserListData[list.id]
let newList
if (remoteList) {
newList = mergeListDataFromSnapshot(list, remoteList, snapshotUserListData[list.id], addMusicLocationType)
} else {
newList = { ...list }
}
newUserList.push(newList)
}
for (const list of remoteListData.userList) {
if (removedListIds.has(list.id) || localUserListData[list.id]) continue
newUserList.push({ ...list })
}
newListData.userList = newUserList
setLocalList(newListData)
setRemotelList(socket, newListData)
return updateSnapshot(socket.data.snapshotFilePath, JSON.stringify({
defaultList: newListData.defaultList,
loveList: newListData.loveList,
userList: newListData.userList,
})).then(() => {
socket.data.isCreatedSnapshot = true
return newListData
})
}
const registerUpdateSnapshotTask = (socket, snapshot) => {
if (!socket.data.isCreatedSnapshot) return
const handleUpdateSnapshot = throttle(({ defaultList, loveList, userList }) => {
if (defaultList != null) snapshot.defaultList = defaultList
if (loveList != null) snapshot.loveList = loveList
if (userList != null) snapshot.userList = userList
updateSnapshot(socket.data.snapshotFilePath, JSON.stringify(snapshot))
}, 10000)
global.lx_event.common.on(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
socket.on('disconnect', () => {
global.lx_event.common.off(COMMON_EVENT_NAME.saveMyList, handleUpdateSnapshot)
})
}
const syncList = async socket => {
socket.data.snapshotFilePath = path.join(app.getPath('userData'), `snapshot-${Buffer.from(socket.data.keyInfo.clientId).toString('hex').substring(0, 10)}.json`)
let fileData
let isSyncRequired = false
try {
fileData = await fsPromises.readFile(socket.data.snapshotFilePath)
fileData = JSON.parse(fileData)
} catch (err) {
if (err.code !== 'ENOENT') throw err
isSyncRequired = true
}
console.log('isSyncRequired', isSyncRequired)
if (isSyncRequired) return handleSyncList(socket)
return handleMergeListDataFromSnapshot(socket, fileData)
}
const checkSyncQueue = async() => {
if (!syncingId) return
await wait()
return checkSyncQueue()
}
module.exports = async(_io, socket) => {
io = _io
await checkSyncQueue()
syncingId = socket.data.keyInfo.clientId
return syncList(socket).then(newListData => {
registerUpdateSnapshotTask(socket, { ...newListData })
return finishedSync(socket)
}).finally(() => {
syncingId = null
})
}

View File

@@ -0,0 +1,93 @@
const { networkInterfaces } = require('os')
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
const getStore = require('@common/store')
const STORE_NAME = 'sync'
exports.getAddress = () => {
const nets = networkInterfaces()
const results = []
// console.log(nets)
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
results.push(net.address)
}
}
}
return results
}
let serverId
exports.getServerId = () => {
if (serverId) return serverId
const store = getStore(STORE_NAME)
serverId = store.get('serverId')
if (!serverId) {
serverId = randomBytes(4 * 4).toString('base64')
store.set('serverId', serverId)
}
return serverId
}
let keyInfos
exports.createClientKeyInfo = deviceName => {
const keyInfo = {
clientId: randomBytes(4 * 4).toString('base64'),
key: randomBytes(16).toString('base64'),
iv: randomBytes(16).toString('base64'),
deviceName,
}
const store = getStore(STORE_NAME)
if (!keyInfos) keyInfos = store.get('keys') || {}
if (Object.keys(keyInfos).length > 101) throw new Error('max keys')
keyInfos[keyInfo.clientId] = keyInfo
store.set('keys', keyInfos)
return keyInfo
}
exports.setClientKeyInfo = keyInfo => {
keyInfos[keyInfo.clientId] = keyInfo
const store = getStore(STORE_NAME)
store.set('keys', keyInfos)
}
exports.getClientKeyInfo = clientId => {
if (!keyInfos) {
const store = getStore(STORE_NAME)
keyInfos = store.get('keys') || {}
}
return keyInfos[clientId] || null
}
exports.generateCode = () => {
return Math.random().toString().substring(2, 8)
}
exports.aesEncrypt = (buffer, key, iv) => {
const cipher = createCipheriv('aes-128-cbc', Buffer.from(key, 'base64'), Buffer.from(iv, 'base64'))
return Buffer.concat([cipher.update(buffer), cipher.final()]).toString('base64')
}
exports.aesDecrypt = (text, key, iv) => {
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()
}
exports.encryptMsg = (keyInfo, msg) => {
return msg
// if (!keyInfo) return ''
// return exports.aesEncrypt(msg, keyInfo.key, keyInfo.iv)
}
exports.decryptMsg = (keyInfo, enMsg) => {
return enMsg
// if (!keyInfo) return ''
// let msg = ''
// try {
// msg = exports.aesDecrypt(enMsg, keyInfo.key, keyInfo.iv)
// } catch (err) {
// console.log(err)
// }
// return msg
}

View File

@@ -70,55 +70,63 @@ const destroyTray = () => {
const createMenu = tray => {
if (!global.modules.tray) return
let menu = []
global.modules.mainWindow && menu.push(global.modules.mainWindow.isVisible() ? {
label: '隐藏主界面',
click() {
global.modules.mainWindow.hide()
},
} : {
label: '显示主界面',
click() {
if (!global.modules.mainWindow) return
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
global.modules.mainWindow && menu.push(global.modules.mainWindow.isVisible()
? {
label: '隐藏主界面',
click() {
global.modules.mainWindow.hide()
},
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
},
})
menu.push(global.appSetting.desktopLyric.enable ? {
label: '关闭桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: false } }, TRAY_EVENT_NAME.name)
},
} : {
label: '开启桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isLock ? {
label: '解锁桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: false } }, TRAY_EVENT_NAME.name)
},
} : {
label: '锁定桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isAlwaysOnTop ? {
label: '取消置顶',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: false } }, TRAY_EVENT_NAME.name)
},
} : {
label: '置顶歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: true } }, TRAY_EVENT_NAME.name)
},
})
: {
label: '显示主界面',
click() {
if (!global.modules.mainWindow) return
if (!global.modules.mainWindow.isVisible()) {
global.modules.mainWindow.show()
}
global.modules.mainWindow.restore()
global.modules.mainWindow.focus()
},
})
menu.push(global.appSetting.desktopLyric.enable
? {
label: '关闭桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: false } }, TRAY_EVENT_NAME.name)
},
}
: {
label: '开启桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { enable: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isLock
? {
label: '解锁桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: false } }, TRAY_EVENT_NAME.name)
},
}
: {
label: '锁定桌面歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isLock: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push(global.appSetting.desktopLyric.isAlwaysOnTop
? {
label: '取消置顶',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: false } }, TRAY_EVENT_NAME.name)
},
}
: {
label: '置顶歌词',
click() {
global.lx_core.setAppConfig({ desktopLyric: { isAlwaysOnTop: true } }, TRAY_EVENT_NAME.name)
},
})
menu.push({
label: '退出',
click() {

View File

@@ -47,8 +47,16 @@ exports.createWindow = async userApi => {
show: false,
webPreferences: {
contextIsolation: true,
worldSafeExecuteJavaScript: true,
// worldSafeExecuteJavaScript: true,
nodeIntegration: false,
spellcheck: false,
autoplayPolicy: 'document-user-activation-required',
enableWebSQL: false,
disableDialogs: true,
webgl: false,
images: false,
preload: path.join(dir, 'renderer/preload.js'),
},
})
@@ -59,6 +67,7 @@ exports.createWindow = async userApi => {
})
}
global.modules.userApiWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => {
// eslint-disable-next-line node/no-callback-literal
callback(false)
})

View File

@@ -1,7 +1,7 @@
const path = require('path')
const { BrowserWindow } = require('electron')
const { winLyric: WIN_LYRIC_EVENT_NAME } = require('../../events/_name')
const { debounce } = require('../../../common/utils')
const { debounce, isLinux } = require('../../../common/utils')
const { getLyricWindowBounds } = require('./utils')
require('./event')
@@ -66,6 +66,10 @@ const winEvent = lyricWindow => {
if (global.appSetting.desktopLyric.isLock) {
global.modules.lyricWindow.setIgnoreMouseEvents(true, { forward: false })
}
// linux下每次重开时貌似要重新设置置顶
if (isLinux && global.appSetting.desktopLyric.isAlwaysOnTop) {
global.modules.lyricWindow.setAlwaysOnTop(global.appSetting.desktopLyric.isAlwaysOnTop, 'screen-saver')
}
})
}

View File

@@ -19,7 +19,11 @@ require('./playList')
require('./data')
require('./lyric')
require('./musicUrl')
require('./systemFonts')
require('./kw_decodeLyric')
require('./userApi')
require('./sync')
require('./s2t')

View File

@@ -24,6 +24,7 @@ mainOn(ipcMainWindowNames.save_playlist, (event, { type, data }) => {
switch (type) {
case 'myList':
handleSaveList(data)
global.lx_event.common.saveMyList(data)
break
case 'downloadList':
getStore('downloadList').set('list', data)

View File

@@ -0,0 +1,9 @@
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
const { tranditionalize } = require('../utils/simplify-chinese-main')
mainHandle(ipcMainWindowNames.lang_s2t, async(event, textBase64) => {
if (!global.modules.mainWindow) throw new Error('mainWindow is undefined')
const text = tranditionalize(Buffer.from(textBase64, 'base64').toString())
return Buffer.from(text).toString('base64')
})

View File

@@ -0,0 +1,33 @@
const { mainSend, NAMES: { mainWindow: ipcMainWindowNames }, mainOn, mainHandle } = require('@common/ipc')
const { eventNames, modules, startServer, stopServer, getStatus, generateCode } = require('../modules/sync')
mainOn(ipcMainWindowNames.sync_action_list, (event, { action, data }) => {
modules.list.sendListAction(action, data)
})
mainHandle(ipcMainWindowNames.sync_enable, (event, { enable, port }) => {
return enable ? startServer(port) : stopServer()
})
mainHandle(ipcMainWindowNames.sync_get_status, () => {
return getStatus()
})
mainHandle(ipcMainWindowNames.sync_generate_code, () => {
return generateCode()
})
mainOn(ipcMainWindowNames.sync_list, (event, { action, data }) => {
global.lx_event.sync.sync_handle_list({ action, data })
})
global.lx_event.sync.on(eventNames.sync_action_list, ({ action, data }) => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_action_list, { action, data })
})
global.lx_event.sync.on(eventNames.status, status => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_status, status)
})
global.lx_event.sync.on(eventNames.sync_list, ({ action, data }) => {
mainSend(global.modules.mainWindow, ipcMainWindowNames.sync_list, { action, data })
})

View File

@@ -0,0 +1,5 @@
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('@common/ipc')
const { getFonts } = require('@main/utils/fontManage')
mainHandle(ipcMainWindowNames.get_system_fonts, getFonts)

View File

@@ -1,4 +1,4 @@
const { log } = require('../../common/utils')
const { log, isWin } = require('../../common/utils')
const { autoUpdater } = require('electron-updater')
const { mainOn, mainSend, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
@@ -120,6 +120,11 @@ module.exports = () => {
}, 1000)
})
autoUpdater.checkForUpdates()
// 由于集合安装包中不包含win arm版这将会导致arm版更新失败
if (isWin && process.arch.includes('arm')) {
handleSendEvent({ type: ipcMainWindowNames.update_error, info: 'failed' })
} else {
autoUpdater.checkForUpdates()
}
}

View File

@@ -0,0 +1,9 @@
// const { getAvailableFontFamilies } = require('electron-font-manager')
// exports.getAvailableFontFamilies = getAvailableFontFamilies
const fontList = require('font-list')
exports.getFonts = fontList.getFonts

View File

@@ -0,0 +1,18 @@
todo.md
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
tsconfig.tsbuildinfo
report.*.json
.eslintcache
.DS_Store
.idea
.vscode
.yarn
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2021 Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,10 @@
# simplify-chinese
Convert chinese characters between simplified form and tranditional form / 汉字简繁体转换工具。
```js
const { simplify, tranditionalize } = require('simplify-chinese')
console.log(simplify('窩窩頭')) // 窝窝头
console.log(tranditionalize('窝窝头')) // 窩窩頭
```

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export function simplify(source: string): string
export function tranditionalize(source: string): string

View File

@@ -0,0 +1,30 @@
const { simplified, traditional } = require('./chinese')
const stMap = new Map()
const tsMap = new Map()
simplified.split('').forEach((char, index) => {
stMap.set(char, traditional[index])
tsMap.set(traditional[index], char)
})
function simplify(source) {
let result = []
for (const char of source) {
result.push(tsMap.get(char) || char)
}
return result.join('')
}
function tranditionalize(source) {
let result = []
for (const char of source) {
result.push(stMap.get(char) || char)
}
return result.join('')
}
module.exports = {
simplify,
tranditionalize,
}

View File

@@ -0,0 +1,10 @@
{
"name": "simplify-chinese",
"description": "Convert chinese characters between simplified form and tranditional form 汉字简繁体转换工具",
"version": "1.1.0",
"main": "index.js",
"typings": "index.d.ts",
"repository": "https://github.com/koishijs/simplify-chinese.git",
"author": "Shigma <1700011071@pku.edu.cn>",
"license": "MIT"
}

View File

@@ -39,6 +39,7 @@ export default {
y: -1,
theme: 0,
style: {
font: '',
fontSize: 125,
opacity: 80,
isZoomActiveLrc: true,

View File

@@ -21,6 +21,7 @@ export default {
default() {
return {
style: {
font: '',
fontSize: 125,
opacity: 80,
isZoomActiveLrc: true,
@@ -77,6 +78,7 @@ export default {
computed: {
lrcStyles() {
return {
fontFamily: this.lrcConfig.style.font,
fontSize: this.lrcConfig.style.fontSize / 100 + 'rem',
opacity: this.lrcConfig.style.opacity / 100,
}

View File

@@ -8,6 +8,7 @@
core-icons
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
#container(v-else :class="theme")
core-aside#left
#right
@@ -17,24 +18,19 @@
core-icons
material-version-modal(v-show="version.showModal")
material-pact-modal(v-show="!setting.isAgreePact || globalObj.isShowPact")
material-sync-mode-modal(v-show="globalObj.sync.isShowSyncMode")
</template>
<script>
import dnscache from 'dnscache'
import { mapMutations, mapGetters, mapActions } from 'vuex'
import { rendererOn, rendererSend, rendererInvoke, NAMES } from '../common/ipc'
import { isLinux } from '../common/utils'
import music from './utils/music'
import { throttle, openUrl, compareVer, getPlayList, parseUrlParams, saveSetting } from './utils'
import { base as eventBaseName } from './event/names'
import { base as eventBaseName, sync as eventSyncName } from './event/names'
import apiSourceInfo from './utils/music/api-source-info'
window.ELECTRON_DISABLE_SECURITY_WARNINGS = process.env.ELECTRON_DISABLE_SECURITY_WARNINGS
dnscache({
enable: true,
ttl: 21600,
cachesize: 1000,
})
const listThrottle = (fn, delay = 100) => {
let timer = null
@@ -67,6 +63,11 @@ export default {
message: 'initing',
apis: {},
},
sync: {
enable: false,
isShowSyncMode: false,
deviceName: '',
},
},
updateTimeout: null,
envParams: {
@@ -109,11 +110,20 @@ export default {
document.body.classList.add(this.isDT ? 'disableTransparent' : 'transparent')
window.eventHub.$emit(eventBaseName.bindKey)
this.init()
window.eventHub.$on(eventSyncName.handle_action_list, this.handleSyncAction)
window.eventHub.$on(eventSyncName.handle_sync_list, this.handleSyncList)
if (this.setting.sync.enable && this.setting.sync.port) {
rendererInvoke(NAMES.mainWindow.sync_enable, {
enable: this.setting.sync.enable,
port: this.setting.sync.port,
})
}
},
watch: {
setting: {
handler(n, o) {
saveSetting(n)
global.appSetting = n
},
deep: true,
},
@@ -199,7 +209,25 @@ export default {
methods: {
...mapActions(['getVersionInfo']),
...mapMutations(['setNewVersion', 'setVersionModalVisible', 'setDownloadProgress', 'setSetting', 'setDesktopLyricConfig']),
...mapMutations('list', ['initList']),
...mapMutations('list', {
list_initList: 'initList',
list_setList: 'setList',
list_listAdd: 'listAdd',
list_listMove: 'listMove',
list_listAddMultiple: 'listAddMultiple',
list_listMoveMultiple: 'listMoveMultiple',
list_listRemove: 'listRemove',
list_listRemoveMultiple: 'listRemoveMultiple',
list_listClear: 'listClear',
list_updateMusicInfo: 'updateMusicInfo',
list_createUserList: 'createUserList',
list_removeUserList: 'removeUserList',
list_setUserListName: 'setUserListName',
list_moveupUserList: 'moveupUserList',
list_movedownUserList: 'movedownUserList',
list_setMusicPosition: 'setMusicPosition',
list_setSyncListData: 'setSyncListData',
}),
...mapMutations('download', ['updateDownloadList']),
...mapMutations('search', {
setSearchHistoryList: 'setHistory',
@@ -260,7 +288,7 @@ export default {
})
rendererOn(NAMES.mainWindow.set_config, (event, config) => {
console.log(config)
// console.log(config)
// this.setDesktopLyricConfig(config)
// console.log('set_config', JSON.stringify(this.setting) === JSON.stringify(config))
this.setSetting(Object.assign({}, this.setting, config))
@@ -278,6 +306,7 @@ export default {
this.listenEvent()
asyncTask.push(this.initData())
asyncTask.push(this.initUserApi())
this.globalObj.sync.enable = this.setting.sync.enable
this.globalObj.apiSource = this.setting.apiSource
if (/^user_api/.test(this.setting.apiSource)) {
rendererInvoke(NAMES.mainWindow.set_user_api, this.setting.apiSource)
@@ -334,7 +363,7 @@ export default {
if (!defaultList.list) defaultList.list = []
if (!loveList.list) loveList.list = []
this.initList({ defaultList, loveList, userList })
this.list_initList({ defaultList, loveList, userList })
this.initDownloadList(downloadList) // 初始化下载列表
this.initPlayInfo()
})
@@ -596,6 +625,84 @@ export default {
event.target.value = ''
event.target.blur()
},
handleSyncAction({ action, data }) {
if (typeof data == 'object') data.isSync = true
// console.log(action, data)
switch (action) {
case 'set_list':
this.list_setList(data)
break
case 'list_add':
this.list_listAdd(data)
break
case 'list_move':
this.list_listMove(data)
break
case 'list_add_multiple':
this.list_listAddMultiple(data)
break
case 'list_move_multiple':
this.list_listMoveMultiple(data)
break
case 'list_remove':
this.list_listRemove(data)
break
case 'list_remove_multiple':
this.list_listRemoveMultiple(data)
break
case 'list_clear':
this.list_listClear(data)
break
case 'update_music_info':
this.list_updateMusicInfo(data)
break
case 'create_user_list':
this.list_createUserList(data)
break
case 'remove_user_list':
this.list_removeUserList(data)
break
case 'set_user_list_name':
this.list_setUserListName(data)
break
case 'moveup_user_list':
this.list_moveupUserList(data)
break
case 'movedown_user_list':
this.list_movedownUserList(data)
break
case 'set_music_position':
this.list_setMusicPosition(data)
break
default:
break
}
},
handleSyncList({ action, data }) {
switch (action) {
case 'getData':
global.eventHub.$emit(eventSyncName.send_sync_list, {
action: 'getData',
data: {
defaultList: this.defaultList,
loveList: this.loveList,
userList: this.userList,
},
})
break
case 'setData':
this.list_setSyncListData(data)
break
case 'selectMode':
this.globalObj.sync.deviceName = data.deviceName
this.globalObj.sync.isShowSyncMode = true
break
case 'closeSelectMode':
this.globalObj.sync.isShowSyncMode = false
break
}
},
},
beforeDestroy() {
this.clearUpdateTimeout()
@@ -683,7 +790,7 @@ body {
}
#view {
flex: auto;
height: 0;
min-height: 0;
}
each(@themes, {

View File

@@ -123,6 +123,9 @@ small {
.small {
font-size: .9em;
}
.tip {
color: @color-theme_2-font-label;
}
strong {
font-weight: bold;
}
@@ -192,6 +195,9 @@ each(@themes, {
button, input, textarea, a {
color: ~'@{color-@{value}-theme_2-font}';
}
.tip {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.hover, a {
&:hover {

View File

@@ -217,5 +217,10 @@ svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/19
g#icon-comment(fill='currentColor')
// 0 0 24 24
path(d='M16 11H8V9H16V11M22 4V16C22 17.11 21.11 18 20 18H13.9L10.2 21.71C10 21.9 9.75 22 9.5 22H9C8.45 22 8 21.55 8 21V18H4C2.9 18 2 17.11 2 16V4C2 2.89 2.9 2 4 2H20C21.11 2 22 2.9 22 4M20 4H4V16H10V19.08L13.08 16H20V4')
g#icon-text(fill='currentColor')
// 0 0 24 24
path(fill='currentColor', d='M21,6V8H3V6H21M3,18H12V16H3V18M3,13H21V11H3V13Z')
</template>

View File

@@ -87,7 +87,7 @@ div(:class="$style.player")
<script>
import Lyric from '@renderer/utils/lyric-font-player'
import { rendererSend, rendererOn, NAMES } from '../../../common/ipc'
import { rendererSend, rendererOn, NAMES, rendererInvoke } from '../../../common/ipc'
import { formatPlayTime2, getRandom, checkPath, setTitle, clipboardWriteText, debounce, throttle, assertApiSupport } from '../../utils'
import { mapGetters, mapActions, mapMutations } from 'vuex'
import { requestMsg } from '../../utils/message'
@@ -236,6 +236,7 @@ export default {
break
}
})
this.registerMediaSessionHandler()
navigator.mediaDevices.addEventListener('devicechange', this.handleMediaListChange)
document.addEventListener('mousemove', this.handleVolumeMsMove)
document.addEventListener('mouseup', this.handleVolumeMsUp)
@@ -255,6 +256,8 @@ export default {
if (window.restorePlayInfo) {
this.handleRestorePlay(window.restorePlayInfo)
window.restorePlayInfo = null
navigator.mediaSession.playbackState = 'paused'
this.updateMediaSessionInfo()
return
}
// console.log('changePlay')
@@ -265,7 +268,7 @@ export default {
'setting.player.togglePlayMethod'(n) {
audio.loop = n === 'singleLoop'
if (this.playedList.length) this.clearPlayedList()
if (n == 'random') this.setPlayedList(this.playMusicInfo)
if (n == 'random' && this.playMusicInfo && !this.playMusicInfo.isTempPlay) this.setPlayedList(this.playMusicInfo)
},
'setting.player.isMute'(n) {
audio.muted = n
@@ -411,7 +414,7 @@ export default {
this.restorePlayTime = 0
}
if (!this.targetSong.interval && this.listId != 'download') {
this.updateMusicInfo({ id: this.listId, index: this.playIndex, data: { interval: formatPlayTime2(this.maxPlayTime) }, musicInfo: this.targetSong })
this.updateMusicInfo({ listId: this.listId, id: this.targetSong.songmid, musicInfo: this.targetSong, data: { interval: formatPlayTime2(this.maxPlayTime) } })
}
})
audio.addEventListener('loadstart', () => {
@@ -480,10 +483,11 @@ export default {
},
async play() {
this.clearDelayNextTimeout()
this.updateMediaSessionInfo()
const targetSong = this.targetSong
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(this.playMusicInfo)
if (this.setting.player.togglePlayMethod == 'random' && !this.playMusicInfo.isTempPlay) this.setPlayedList(this.playMusicInfo)
this.retryNum = 0
this.restorePlayTime = 0
@@ -551,20 +555,28 @@ export default {
this.handleUpdateWinLyricInfo('play', audio.currentTime * 1000)
this.setAppTitle()
this.sendProgressEvent(this.progress, 'normal')
navigator.mediaSession.playbackState = 'playing'
},
stopPlay() {
this.isPlay = false
window.lrc.pause()
this.handleUpdateWinLyricInfo('pause')
this.sendProgressEvent(this.progress, 'paused')
this.clearAppTitle()
this.$nextTick(() => {
if (this.playMusicInfo) {
this.sendProgressEvent(this.progress, 'paused')
navigator.mediaSession.playbackState = 'paused'
} else {
this.sendProgressEvent(this.progress, 'none')
navigator.mediaSession.playbackState = 'none'
}
})
},
handleSetProgress(event) {
this.setProgress(event.offsetX / this.pregessWidth)
this.setProgress(event.offsetX / this.pregessWidth * this.maxPlayTime)
},
setProgress(pregress) {
setProgress(time) {
if (!audio.src) return
const time = pregress * this.maxPlayTime
if (this.restorePlayTime) this.restorePlayTime = time
if (this.mediaBuffer.playTime) {
this.clearBufferTimeout()
@@ -634,16 +646,36 @@ export default {
if (!this.musicInfo.img) {
this.getPic(targetSong).then(() => {
if (targetSong.songmid !== this.musicInfo.songmid) return
this.musicInfo.img = targetSong.img
this.updateMediaSessionInfo()
})
}
},
setLrc(targetSong) {
this.getLrc(targetSong).then(({ lyric, tlyric, lxlyric }) => {
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
}).catch(() => {
if (targetSong.songmid !== this.musicInfo.songmid) return
return (
global.i18n.locale == 'zh-tw'
? Promise.all([
lyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
tlyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(tlyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
lxlyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lxlyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
])
: Promise.resolve([lyric, tlyric, lxlyric])
).then(([lyric, tlyric, lxlyric]) => {
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
})
}).catch((err) => {
console.log(err)
this.status = this.statusText = this.$t('core.player.lyric_error')
}).finally(() => {
this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc, lxlrc: this.musicInfo.lxlrc })
@@ -834,7 +866,7 @@ export default {
this.playNext()
break
case 'progress':
this.setProgress(data)
this.setProgress(data * this.maxPlayTime)
break
case 'volume':
break
@@ -902,6 +934,59 @@ export default {
if (this.setting.player.togglePlayMethod == 'random') this.setPlayedList(this.playMusicInfo)
},
updateMediaSessionInfo() {
const mediaMetadata = {
title: this.targetSong.name,
artist: this.targetSong.singer,
album: this.targetSong.albumName,
}
if (this.targetSong.img) mediaMetadata.artwork = [{ src: this.targetSong.img }]
navigator.mediaSession.metadata = new window.MediaMetadata(mediaMetadata)
},
registerMediaSessionHandler() {
// navigator.mediaSession.setActionHandler('play', () => {
// if (this.isPlay || !this.playMusicInfo) return
// console.log('play')
// this.startPlay()
// })
// navigator.mediaSession.setActionHandler('pause', () => {
// if (!this.isPlay || !this.playMusicInfo) return
// console.log('pause')
// this.stopPlay()
// })
navigator.mediaSession.setActionHandler('stop', () => {
if (!this.isPlay || !this.playMusicInfo) return
console.log('stop')
this.stopPlay()
})
navigator.mediaSession.setActionHandler('seekbackward', details => {
if (!this.isPlay || !this.playMusicInfo) return
console.log('seekbackward')
this.setProgress(Math.max(audio.currentTime - details.seekOffset, 0))
})
navigator.mediaSession.setActionHandler('seekforward', details => {
if (!this.isPlay || !this.playMusicInfo) return
console.log('seekforward')
this.setProgress(Math.min(audio.currentTime + details.seekOffset, audio.duration))
})
navigator.mediaSession.setActionHandler('seekto', details => {
console.log('seekto', details.seekTime)
let time = Math.min(details.seekTime, audio.duration)
time = Math.max(time, 0)
this.setProgress(time)
})
navigator.mediaSession.setActionHandler('previoustrack', () => {
console.log('previoustrack')
this.playPrev()
})
navigator.mediaSession.setActionHandler('nexttrack', () => {
console.log('nexttrack')
this.playNext()
})
// navigator.mediaSession.setActionHandler('skipad', () => {
// console.log('')
// })
},
},
}
</script>

View File

@@ -27,17 +27,28 @@
p(v-if="musicInfo.album") {{$t('core.player.album')}}{{musicInfo.album}}
div(:class="$style.right")
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="[$style.lyric, { [$style.draging]: lyricEvent.isMsDown }]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="$style.lyricSpace")
div(:class="[$style.lyricText]" ref="dom_lyric_text")
//- p(v-for="(info, index) in lyricLines" :key="index" :class="lyric.line == index ? $style.lrcActive : null") {{info.text}}
div(:class="$style.lyricSpace")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
div(:class="[$style.lyricSelectContent, 'select', 'scroll']" v-if="isShowLrcSelectContent" @contextmenu="handleCopySelectText")
//- div(:class="$style.lyricSpace")
div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lyricSelectline, { [$style.lrcActive]: lyric.line == index }]")
span {{info.text}}
br(v-if="info.translation")
span(:class="$style.lyricSelectlineTransition") {{info.translation}}
//- div(:class="$style.lyricSpace")
material-music-comment(:class="$style.comment" :titleFormat="this.setting.download.fileName" :musicInfo="musicInfo" v-model="isShowComment")
div(:class="$style.footer")
div(:class="$style.footerLeft")
div(:class="$style.footerLeftControlBtns")
div(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="isShowLrcSelectContent = !isShowLrcSelectContent" :tips="$t('core.player.lyric_select')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-text')
div(:class="[$style.footerLeftControlBtn, isShowComment ? $style.active : null]" @click="isShowComment = !isShowComment" :tips="$t('core.player.comment_show')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-comment')
@@ -59,7 +70,7 @@
div(:class="$style.progressContent")
div(:class="$style.progress")
//- div(:class="[$style.progressBar, $style.progressBar1]" :style="{ transform: `scaleX(${progress || 0})` }")
div(:class="[$style.progressBar, $style.progressBar2, isActiveTransition ? $style.barTransition : '']" @transitionend="handleTransitionEnd" :style="{ transform: `scaleX(${playInfo.progress || 0})`, willChange: isPlay || isActiveTransition ? 'transform' : 'auto' }")
div(:class="[$style.progressBar, $style.progressBar2, isActiveTransition ? $style.barTransition : '']" @transitionend="handleTransitionEnd" :style="{ transform: `scaleX(${playInfo.progress || 0})` }")
div(:class="$style.progressMask" @click='setProgress' ref="dom_progress")
div(:class="$style.timeLabel")
span(style="margin-left: 15px") {{playInfo.status}}
@@ -87,7 +98,7 @@
<script>
import { mapGetters, mapMutations } from 'vuex'
import { base as eventBaseName } from '../../event/names'
import { scrollTo } from '../../utils'
import { clipboardWriteText, scrollTo } from '../../utils'
let cancelScrollFn = null
@@ -230,23 +241,22 @@ export default {
lyricLines: [],
isSetedLines: false,
isShowComment: false,
isShowLrcSelectContent: false,
}
},
mounted() {
this.$nextTick(() => {
this.setProgressWidth()
})
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
this.listenEvent()
// console.log('object', this.$refs.dom_lyric_text)
this.setLyric(this.lyricLines)
},
beforeDestroy() {
this.unlistenEvent()
this.clearLyricScrollTimeout()
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
window.removeEventListener('resize', this.handleResize)
},
computed: {
...mapGetters(['setting']),
@@ -256,6 +266,16 @@ export default {
...mapMutations('player', [
'visiblePlayerDetail',
]),
listenEvent() {
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
},
unlistenEvent() {
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
window.removeEventListener('resize', this.handleResize)
},
setLyric(lines) {
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
@@ -359,6 +379,12 @@ export default {
close() {
window.eventHub.$emit(eventBaseName.close)
},
handleCopySelectText() {
let str = window.getSelection().toString()
str = str.trim()
if (!str.length) return
clipboardWriteText(str)
},
},
}
</script>
@@ -635,14 +661,41 @@ export default {
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
// .lrc-active {
// color: @color-theme;
// font-size: 1.2em;
// }
}
.lyricSelectContent {
position: absolute;
left: 0;
top: 0;
// text-align: center;
height: 100%;
width: 100%;
font-size: 16px;
background-color: @color-theme_2-background_1;
z-index: 10;
color: @color-player-detail-lyric;
.lyricSelectline {
padding: 8px 0;
overflow-wrap: break-word;
transition: @transition-theme !important;
transition-property: color, font-size;
line-height: 1.3;
}
.lyricSelectlineTransition {
font-size: 14px;
}
.lrc-active {
color: @color-theme;
}
}
.lyric-space {
height: 70%;
}
.lrc-active {
color: @color-theme;
font-size: 1.2em;
}
.comment {
flex: 0 0 0;
@@ -853,6 +906,13 @@ each(@themes, {
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.lyricSelectContent {
background-color: ~'@{color-@{value}-theme_2-background_1}';
color: ~'@{color-@{value}-player-detail-lyric}';
.lrc-active {
color: ~'@{color-@{value}-theme}';
}
}
.footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}';
}

View File

@@ -0,0 +1,257 @@
<template lang="pug">
Modal(:show="visible" @close="$emit('update:visible', false)" bg-close)
div(:class="$style.header")
h2 {{listInfo.name}}
main.scroll(:class="$style.main")
ul(ref="dom_list" v-if="duplicateList.length" :class="$style.list")
li(v-for="(item, index) in duplicateList" :key="item.songmid" :class="$style.listItem")
div(:class="$style.num") {{item.index + 1}}
div(:class="$style.text")
h3(:class="$style.text") {{item.musicInfo.name}} - {{item.musicInfo.singer}}
h3(v-if="item.musicInfo.albumName" :class="[$style.text, $style.albumName]") {{item.musicInfo.albumName}}
div(:class="$style.label") {{item.musicInfo.source}}
div(:class="$style.label") {{item.musicInfo.interval}}
div(:class="$style.btns")
button(type="button" @click="handlePlay(index)" :class="$style.btn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='50%' viewBox='0 0 287.386 287.386' space='preserve' v-once)
use(xlink:href='#icon-testPlay')
button(type="button" @click="handleRemove(index)" :class="$style.btn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='50%' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
div(:class="$style.noItem" v-else)
p(v-text="$t('view.list.no_item')")
</template>
<script>
import { mapMutations } from 'vuex'
import Modal from './Modal'
import Btn from './Btn'
export default {
props: {
visible: {
type: Boolean,
default: false,
},
listInfo: {
type: Object,
required: true,
},
},
model: {
prop: 'visible',
event: 'visible',
},
components: {
Modal,
Btn,
},
watch: {
visible(n) {
if (n) this.handleFilterList()
},
},
data() {
return {
duplicateList: [],
}
},
mounted() {
if (this.listInfo.list) this.handleFilterList()
},
methods: {
...mapMutations('list', [
'listRemove',
]),
...mapMutations('player', {
setPlayList: 'setList',
}),
handleFilterList() {
const listMap = new Map()
const duplicateList = []
this.listInfo.list.forEach((musicInfo, index) => {
const musicInfoName = musicInfo.name.toLowerCase().trim()
if (listMap.has(musicInfoName)) {
const targetMusicInfo = listMap.get(musicInfoName)
duplicateList.push({
index: this.listInfo.list.indexOf(targetMusicInfo),
musicInfo: targetMusicInfo,
}, {
index,
musicInfo,
})
} else {
listMap.set(musicInfoName, musicInfo)
}
})
this.duplicateList = duplicateList
},
handleRemove(index) {
const { musicInfo: targetMusicInfo } = this.duplicateList.splice(index, 1)[0]
// let duplicates = []
// for (let index = 0; index < this.duplicateList.length; index++) {
// const { musicInfo } = this.duplicateList[index]
// if (musicInfo.name == targetMusicInfo.name) {
// duplicates.push(index)
// if (duplicates.length > 1) break
// }
// }
// console.log(duplicates)
// if (duplicates.length < 2) this.duplicateList.splice(duplicates[0], 1)
this.listRemove({ listId: this.listInfo.id, id: targetMusicInfo.songmid })
this.handleFilterList()
},
handlePlay(index) {
const { index: musicInfoIndex } = this.duplicateList[index]
this.setPlayList({ list: this.listInfo, index: musicInfoIndex })
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.header {
flex: none;
padding: 15px;
text-align: center;
}
.main {
min-height: 200px;
width: 460px;
}
.list {
// background-color: @color-search-form-background;
font-size: 13px;
transition-property: height;
position: relative;
.listItem {
position: relative;
padding: 8px 5px;
transition: background-color .2s ease;
line-height: 1.3;
// overflow: hidden;
display: flex;
flex-flow: row nowrap;
align-items: center;
&:hover {
background-color: @color-theme_2-hover;
}
// border-radius: 4px;
// &:last-child {
// border-bottom-left-radius: 4px;
// border-bottom-right-radius: 4px;
// }
}
}
.num {
flex: none;
font-size: 12px;
width: 30px;
text-align: center;
color: @color-theme_2-font-label;
}
.text {
flex: auto;
padding-left: 5px;
.mixin-ellipsis-1;
}
.albumName {
font-size: 12px;
opacity: 0.6;
.mixin-ellipsis-1;
}
.label {
flex: none;
font-size: 12px;
opacity: 0.5;
padding: 0 5px;
display: flex;
align-items: center;
// transform: rotate(45deg);
// background-color:
}
.btns {
flex: none;
font-size: 12px;
padding: 0 5px;
display: flex;
align-items: center;
}
.btn {
background-color: transparent;
border: none;
border-radius: @form-radius;
margin-right: 5px;
cursor: pointer;
padding: 4px 7px;
color: @color-btn;
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
&:last-child {
margin-right: 0;
}
svg {
height: 16px;
}
&:hover {
background-color: @color-theme_2-hover;
}
&:active {
background-color: @color-theme_2-active;
}
}
.no-item {
position: relative;
height: 200px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
p {
font-size: 16px;
color: @color-theme_2-font-label;
}
}
each(@themes, {
:global(#container.@{value}) {
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
}
.num {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.btn {
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
.no-item {
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
})
</style>

View File

@@ -3,7 +3,9 @@ transition(enter-active-class="animated fadeIn"
leave-active-class="animated fadeOut")
div(:class="$style.modal" v-show="show" @click="bgClose && close()")
transition(:enter-active-class="inClass"
:leave-active-class="outClass")
:leave-active-class="outClass"
@after-leave="$emit('after-leave', $event)"
)
div(:class="$style.content" v-show="show" @click.stop)
header(:class="$style.header")
button(type="button" @click="close" v-if="closeBtn")
@@ -140,7 +142,8 @@ export default {
box-shadow: 0 0 3px rgba(0, 0, 0, .3);
overflow: hidden;
max-height: 80%;
max-width: 70%;
max-width: 76%;
min-width: 220px;
position: relative;
display: flex;
flex-flow: column nowrap;

View File

@@ -9,6 +9,10 @@ div(:class="$style.container")
@keyup.40.prevent="handleKeyDown"
@keyup.38.prevent="handleKeyUp"
@contextmenu="handleContextMenu")
transition(enter-active-class="animated zoomIn" leave-active-class="animated zoomOut")
button(type="button" @click="handleClearList" v-show="text")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-window-close')
button(type="button" @click="handleSearch")
slot
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 30.239 30.239' space='preserve')
@@ -144,6 +148,11 @@ export default {
this.text = `${this.text.substring(0, dom_input.selectionStart)}${str}${this.text.substring(dom_input.selectionEnd, this.text.length)}`
this.$emit('input', this.text)
},
handleClearList() {
this.text = ''
this.$emit('input', this.text)
this.sendEvent('submit')
},
},
}
</script>
@@ -210,14 +219,17 @@ export default {
// background-color: @color-search-form-background;
background-color: transparent;
outline: none;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
cursor: pointer;
height: 100%;
padding: 6px 7px;
color: @color-btn;
transition: background-color .2s ease;
&:last-child {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
&:hover {
background-color: @color-theme-hover;
}

View File

@@ -374,6 +374,7 @@ export default {
.albumName {
font-size: 12px;
opacity: 0.6;
.mixin-ellipsis-1;
}
.source {
flex: none;

View File

@@ -45,8 +45,8 @@ export default {
},
computed: {
label() {
if (!this.value) return ''
if (!this.itemName) return this.value
if (this.value == null) return ''
if (this.itemName == null) return this.value
const item = this.list.find(l => l[this.itemKey] == this.value)
if (!item) return ''
return item[this.itemName]

View File

@@ -0,0 +1,164 @@
<template lang="pug">
material-modal(:show="globalObj.sync.isShowSyncMode" @close="handleClose(false)" :bgClose="false" :close-btn="false")
main(:class="$style.main")
h2 {{$t('material.sync_mode_modal.title', { name: globalObj.sync.deviceName })}}
div.scroll(:class="$style.content")
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('material.sync_mode_modal.merge_label')}}
dd(:class="$style.btns")
material-btn(:class="$style.btn" @click="handleSelectMode('merge_local_remote')") {{$t('material.sync_mode_modal.merge_btn_local_remote')}}
material-btn(:class="$style.btn" @click="handleSelectMode('merge_remote_local')") {{$t('material.sync_mode_modal.merge_btn_remote_local')}}
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('material.sync_mode_modal.overwrite_label')}}
dd(:class="$style.btns")
material-btn(:class="$style.btn" @click="handleSelectMode('overwrite_local_remote')") {{$t('material.sync_mode_modal.overwrite_btn_local_remote')}}
material-btn(:class="$style.btn" @click="handleSelectMode('overwrite_remote_local')") {{$t('material.sync_mode_modal.overwrite_btn_remote_local')}}
dd(style="font-size: 14px; margin-top: 5px;")
material-checkbox(id="sync_mode_modal_isOverwrite" v-model="isOverwrite" :label="$t('material.sync_mode_modal.overwrite')")
dl(:class="$style.btnGroup")
dt(:class="$style.label") {{$t('material.sync_mode_modal.other_label')}}
dd(:class="$style.btns")
material-btn(:class="$style.btn" @click="handleSelectMode('none')") {{$t('material.sync_mode_modal.overwrite_btn_none')}}
material-btn(:class="$style.btn" @click="handleSelectMode('cancel')") {{$t('material.sync_mode_modal.overwrite_btn_cancel')}}
dl(:class="$style.btnGroup")
dd
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('material.sync_mode_modal.merge_tip')}}
p(:class="$style.tip") {{$t('material.sync_mode_modal.merge_tip_desc')}}
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('material.sync_mode_modal.overwrite_tip')}}
p(:class="$style.tip") {{$t('material.sync_mode_modal.overwrite_tip_desc')}}
section(:class="$style.tipGroup")
h3(:class="$style.title") {{$t('material.sync_mode_modal.other_tip')}}
p(:class="$style.tip") {{$t('material.sync_mode_modal.other_tip_desc')}}
</template>
<script>
import { sync as eventSyncName } from '@renderer/event/names'
export default {
data() {
return {
isOverwrite: false,
globalObj: {
sync: {
isShowSyncMode: false,
deviceName: '',
},
},
}
},
computed: {
},
mounted() {
this.$nextTick(() => {
this.globalObj = window.globalObj
})
},
methods: {
handleSelectMode(mode) {
if (mode.startsWith('overwrite') && this.isOverwrite) mode += '_full'
window.eventHub.$emit(eventSyncName.send_sync_list, {
action: 'selectMode',
data: mode,
})
this.handleClose()
},
handleClose() {
this.globalObj.sync.isShowSyncMode = false
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.main {
padding: 15px;
max-width: 700px;
min-width: 200px;
min-height: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
h2 {
font-size: 16px;
color: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.content {
flex: auto;
padding: 15px 0 5px;
padding-right: 5px;
.btnGroup + .btnGroup {
margin-top: 10px;
}
.label {
color: @color-theme_2-font-label;
font-size: 14px;
line-height: 2;
}
.desc {
line-height: 1.5;
font-size: 14px;
text-align: justify;
}
.tipGroup {
display: flex;
flex-direction: row;
font-size: 12px;
+ .tipGroup {
margin-top: 5px;
}
.title {
white-space: nowrap;
font-weight: bold;
margin-right: 5px;
}
.tip {
line-height: 1.3;
}
}
}
.btns {
display: flex;
align-items: center;
}
.btn {
display: block;
white-space: nowrap;
+.btn {
margin-left: 15px;
}
&:last-child {
margin-bottom: 0;
}
}
each(@themes, {
:global(#container.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.name {
color: ~'@{color-@{value}-theme}';
}
}
})
</style>

View File

@@ -1,7 +1,7 @@
import Vue from 'vue'
import keyBind from '../utils/keyBind'
import { rendererOn, rendererSend, NAMES, rendererInvoke } from '../../common/ipc'
import { base as baseName } from './names'
import { base as baseName, sync as syncName } from './names'
import { common as hotKeyNamesCommon } from '../../common/hotKey'
const eventHub = window.eventHub = new Vue()
@@ -25,6 +25,14 @@ eventHub.$on(baseName.bindKey, () => {
// console.log(event, key)
if (!window.isEditingHotKey && appHotKeyConfig.local.enable && appHotKeyConfig.local.keys[key]) {
if (type == 'up') return
// 软件内快捷键的最小化触发时
// 如果已启用托盘,则隐藏程序,否则最小化程序 https://github.com/lyswhut/lx-music-desktop/issues/603
if (appHotKeyConfig.local.keys[key].action == hotKeyNamesCommon.min.action && global.appSetting.tray.isToTray) {
eventHub.$emit(hotKeyNamesCommon.hide_toggle.action)
return
}
eventHub.$emit(appHotKeyConfig.local.keys[key].action)
return
}
@@ -77,3 +85,18 @@ rendererOn(NAMES.mainWindow.set_hot_key_config, (event, config) => {
}
window.eventHub.$emit(baseName.set_hot_key_config, config)
})
rendererOn(NAMES.mainWindow.sync_action_list, (event, { action, data }) => {
window.eventHub.$emit(syncName.handle_action_list, { action, data })
})
eventHub.$on(syncName.send_action_list, ({ action, data }) => {
if (!window.globalObj.sync.enable) return
rendererSend(NAMES.mainWindow.sync_action_list, { action, data })
})
rendererOn(NAMES.mainWindow.sync_list, (event, { action, data }) => {
window.eventHub.$emit(syncName.handle_sync_list, { action, data })
})
eventHub.$on(syncName.send_sync_list, ({ action, data }) => {
if (!window.globalObj.sync.enable) return
rendererSend(NAMES.mainWindow.sync_list, { action, data })
})

View File

@@ -10,6 +10,12 @@ const names = {
set_config: 'set_config',
set_hot_key_config: 'set_hot_key_config',
},
sync: {
send_action_list: 'send_action_list',
handle_action_list: 'handle_action_list',
send_sync_list: 'send_sync_list',
handle_sync_list: 'handle_sync_list',
},
}
for (const item of Object.keys(names)) {
@@ -20,3 +26,4 @@ for (const item of Object.keys(names)) {
}
export const base = names.base
export const sync = names.sync

View File

@@ -1,5 +1,7 @@
{
"date_format_second": "{num} seconds ago",
"cancel_button_text": "Cancel",
"confirm_button_text": "OK",
"date_format_hour": "{num} hours ago",
"date_format_minute": "{num} minutes ago",
"date_format_hour": "{num} hours ago"
"date_format_second": "{num} seconds ago"
}

View File

@@ -1,43 +1,43 @@
{
"copy_title": " (Click to copy)",
"volume": "Volume: ",
"pause": "Pause",
"play": "Play",
"prev": "Prev",
"next": "Next",
"playing": "Now playing...",
"stop": "Paused",
"end": "Stopped",
"refresh_url": "Music URL expired, refreshing...",
"error": "Error loading music. Switch to next song after 5 seconds",
"loading": "Music loading...",
"buffering": "Buffering...",
"geting_url": "Getting music link...",
"lyric_error": "Failed to get lyrics",
"hide_detail": "Hide detail page (Right-click in the view to quickly hide the details page)",
"name": "Name: ",
"singer": "Artist: ",
"album": "Album: ",
"add_music_to": "Add the current song to...",
"desktop_lyric_on": "Open Desktop Lyrics",
"desktop_lyric_off": "Close Desktop Lyrics",
"desktop_lyric_lock": "Right click to lock lyrics",
"desktop_lyric_unlock": "Right click to unlock lyrics",
"play_toggle_mode_list_loop": "List Loop",
"play_toggle_mode_random": "List Random",
"play_toggle_mode_list": "Play in order",
"play_toggle_mode_single_loop": "Single Loop",
"play_toggle_mode_off": "Disable",
"pic_tip": "Right click to locate the currently playing song in \"My List\"",
"comment_show": "Song comments",
"comment_hot_loading": "Hot comments are loading",
"comment_new_loading": "Latest comments are loading",
"album": "Album: ",
"buffering": "Buffering...",
"comment_hot_load_error": "Hot comments failed to load, click to try to reload",
"comment_new_load_error": "The latest comment failed to load, click to try to reload",
"comment_refresh": "Refresh comments",
"comment_no_content": "No comments yet",
"comment_hot_loading": "Hot comments are loading",
"comment_hot_title": "Hot Comment",
"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",
"comment_title": "{name} comment"
"comment_no_content": "No comments yet",
"comment_refresh": "Refresh comments",
"comment_show": "Song comments",
"comment_title": "{name} comment",
"copy_title": " (Click to copy)",
"desktop_lyric_lock": "Right click to lock lyrics",
"desktop_lyric_off": "Close Desktop Lyrics",
"desktop_lyric_on": "Open Desktop Lyrics",
"desktop_lyric_unlock": "Right click to unlock lyrics",
"end": "Stopped",
"error": "Error loading music. Switch to next song after 5 seconds",
"geting_url": "Getting music link...",
"hide_detail": "Hide detail page (Right-click in the view to quickly hide the details page)",
"loading": "Music loading...",
"lyric_error": "Failed to get lyrics",
"lyric_select": "Lyric text selection",
"name": "Name: ",
"next": "Next",
"pause": "Pause",
"pic_tip": "Right click to locate the currently playing song in \"My List\"",
"play": "Play",
"play_toggle_mode_list": "Play in order",
"play_toggle_mode_list_loop": "List Loop",
"play_toggle_mode_off": "Disable",
"play_toggle_mode_random": "List Random",
"play_toggle_mode_single_loop": "Single Loop",
"playing": "Now playing...",
"prev": "Prev",
"refresh_url": "Music URL expired, refreshing...",
"singer": "Artist: ",
"stop": "Paused",
"volume": "Volume: "
}

View File

@@ -0,0 +1,19 @@
{
"merge_btn_local_remote": "Local list merge remote list",
"merge_btn_remote_local": "Remote list merge local list",
"merge_label": "Merge",
"merge_tip": "Merge:",
"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.",
"other_label": "Other",
"other_tip": "Other: ",
"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.",
"overwrite": "Full coverage",
"overwrite_btn_cancel": "Cancel sync",
"overwrite_btn_local_remote": "Local list Overwrite remote list",
"overwrite_btn_none": "Only use real-time synchronization",
"overwrite_btn_remote_local": "Remote list Overwrite local list",
"overwrite_label": "Cover",
"overwrite_tip": "Cover: ",
"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.",
"title": "Choose how to synchronize the list with {name}"
}

View File

@@ -1,28 +1,38 @@
{
"lists_new_list_btn": "Create list",
"lists_new_list_input": "New list...",
"lists_rename": "Rename",
"lists_moveup": "Move Up",
"lists_movedown": "Move Down",
"lists_sync": "Sync",
"lists_remove": "Remove",
"action": "Manage",
"album": "Album",
"default_list": "Recently Played",
"list_add_to": "Add to ...",
"list_copy_name": "Copy name",
"list_download": "Download",
"list_move_to": "Move to ...",
"list_play": "Play",
"list_play_later": "Play later",
"list_copy_name": "Copy name",
"list_add_to": "Add to ...",
"list_move_to": "Move to ...",
"list_sort": "Adjust position",
"list_download": "Download",
"list_search": "Search",
"list_remove": "Remove",
"list_search": "Search",
"list_sort": "Adjust position",
"list_source_detail": "Song Page",
"default_list": "Recently Played",
"lists_duplicate": "Duplicate song",
"lists_export": "Export",
"lists_export_part_desc": "Choose where to save the list file",
"lists_import": "Import",
"lists_import_part_button_cancel": "No",
"lists_import_part_button_confirm": "Overwrite",
"lists_import_part_confirm": "The imported list ({importName}) has the same ID as the local list ({localName}). Do you overwrite the local list?",
"lists_import_part_desc": "Select list file",
"lists_movedown": "Move Down",
"lists_moveup": "Move Up",
"lists_new_list_btn": "Create list",
"lists_new_list_input": "New list...",
"lists_remove": "Remove",
"lists_remove_tip": "Do you really want to remove {name}?",
"lists_remove_tip_button": "Yes, that's right",
"lists_rename": "Rename",
"lists_sync": "Update",
"loding_list": "Loading...",
"love_list": "Favorites",
"name": "Name",
"no_item": "Nothing's here...",
"singer": "Artist",
"album": "Album",
"action": "Manage",
"time": "Length",
"loding_list": "Loading...",
"no_item": "Nothing's here..."
"time": "Length"
}

View File

@@ -1,154 +1,156 @@
{
"about": "About lx-music-desktop",
"backup": "Backup and restore",
"backup_all": "All data (list data and setting data)",
"backup_all_export": "Export",
"backup_all_export_desc": "Select the backup to...",
"backup_all_import": "Import",
"backup_all_import_desc": "Select a backup file",
"backup_part": "Partial data (list data includes audition list, favorite list, user-defined list, setting data does not include shortcut key settings)",
"backup_part_export_list": "Export lists",
"backup_part_export_list_desc": "Save the list to...",
"backup_part_export_setting": "Export settings",
"backup_part_export_setting_desc": "Save the list to...",
"backup_part_import_list": "Import lists",
"backup_part_import_list_desc": "Select a list backup",
"backup_part_import_setting": "Import settings",
"backup_part_import_setting_desc": "Select the Settings file",
"basic": "General",
"basic_theme": "Theme",
"basic_show_animation": "Show switching animation",
"basic_animation": "Random pop-up animation",
"basic_source_title": "Choose a music source",
"basic_source_test": "Test API (Available for most software features)",
"basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
"basic_source": "Music source",
"basic_sourcename_title": "Select the name of music source",
"basic_sourcename_real": "Original",
"basic_sourcename_alias": "Aliases",
"basic_sourcename": "Source name",
"basic_source_status_success": "Initialization successful",
"basic_source_status_initing": "Initializing",
"basic_source_status_failed": "Initialization failed",
"basic_source_user_api_btn": "Custom Source Management",
"basic_window_size_title": "Set the window size",
"basic_window_size": "Window size",
"basic_window_size_smaller": "Smaller",
"basic_window_size_small": "Small",
"basic_window_size_medium": "Medium",
"basic_window_size_big": "Large",
"basic_window_size_larger": "Larger",
"basic_window_size_oversized": "Oversized",
"basic_window_size_huge": "Huge",
"basic_to_tray": "Do not exit the software when closing the software and minimize it to the system tray",
"basic_lang_title": "The language displayed in the software",
"basic_lang": "Language",
"basic_control_btn_position": "Control Button Position",
"basic_control_btn_position_left": "Left",
"basic_control_btn_position_right": "Right",
"play": "Play",
"play_save_play_time": "Remember playback progress",
"play_lyric_transition": "Show lyrics translation",
"play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)",
"play_quality": "Play 320K quality songs first (if supported)",
"play_task_bar": "Show playing progress on the taskbar",
"play_mediaDevice_title": "Select a media device for audio output",
"play_mediaDevice": "Audio output",
"play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"basic_lang": "Language",
"basic_lang_title": "The language displayed in the software",
"basic_show_animation": "Show switching animation",
"basic_source": "Music source",
"basic_source_status_failed": "Initialization failed",
"basic_source_status_initing": "Initializing",
"basic_source_status_success": "Initialization successful",
"basic_source_temp": "Temporary API (some features not available; workaround if Test API unavailable)",
"basic_source_test": "Test API (Available for most software features)",
"basic_source_title": "Choose a music source",
"basic_source_user_api_btn": "Custom Source Management",
"basic_sourcename": "Source name",
"basic_sourcename_alias": "Aliases",
"basic_sourcename_real": "Original",
"basic_sourcename_title": "Select the name of music source",
"basic_theme": "Theme",
"basic_to_tray": "Do not exit the software when closing the software and minimize it to the system tray",
"basic_window_size": "Window size",
"basic_window_size_big": "Large",
"basic_window_size_huge": "Huge",
"basic_window_size_larger": "Larger",
"basic_window_size_medium": "Medium",
"basic_window_size_oversized": "Oversized",
"basic_window_size_small": "Small",
"basic_window_size_smaller": "Smaller",
"basic_window_size_title": "Set the window size",
"click_copy": "Click to copy",
"click_open": "Click to open",
"desktop_lyric": "Desktop Lyric Settings",
"desktop_lyric_enable": "Display lyrics",
"desktop_lyric_lock": "Lock lyrics",
"desktop_lyric_always_on_top": "Make the lyrics always above other windows",
"desktop_lyric_enable": "Display lyrics",
"desktop_lyric_font": "Lyric font",
"desktop_lyric_font_default": "Default",
"desktop_lyric_lock": "Lock lyrics",
"desktop_lyric_lock_screen": "It is not allowed to drag the lyrics window out of the main screen",
"search": "Search",
"search_hot": "Top Searches",
"search_history": "Search history",
"search_focus_search_box": "Automatically focus the search box on startup",
"list": "List",
"list_source": "Show song source (only valid for my music category)",
"list_scroll": "Remember the position of the scroll bar of the playlist (only valid for my music classification)",
"download": "Download",
"download_enable": "Whether to enable download function",
"download_path_title": "Define the path to downloading",
"download_path": "Download path",
"download_path_label": "Current: ",
"download_path_open_label": "Click to open this path",
"download_path_change_btn": "Change",
"download_name_title": "Select the music naming method for downloading",
"download_name": "Music file naming",
"download_data_embed": "Whether to embed the following content in the audio file",
"download_embed_pic": "Embedding cover",
"download_embed_lyric": "Embedding lyric",
"download_lyric_title": "Select whether to download the lyrics file",
"download_embed_pic": "Embedding cover",
"download_enable": "Whether to enable download function",
"download_lyric": "Lyrics download",
"download_lyric_format": "Downloaded lyrics file encoding format",
"download_lyric_format_gbk": "GBK (Try to select this format when Chinese garbled characters appear on some devices)",
"download_lyric_format_utf8": "UTF-8",
"download_lyric_title": "Select whether to download the lyrics file",
"download_name": "Music file naming",
"download_name1": "Title - Artist",
"download_name2": "Artist - Title",
"download_name3": "Title only",
"download_use_other_source": "Automatically change the source to download (when the song cannot be downloaded from the original source, try to switch to another source to download. Note: this function does not 100% guarantee that the version of the song after the source is changed is the same as the original version)",
"download_name_title": "Select the music naming method for downloading",
"download_path": "Download path",
"download_path_change_btn": "Change",
"download_path_label": "Current: ",
"download_path_open_label": "Click to open this path",
"download_path_title": "Define the path to downloading",
"download_select_save_path": "Select the save path",
"download_use_other_source": "Automatically change the source to download (when the song cannot be downloaded from the original source, try to switch to another source to download. Note: this function does not 100% guarantee that the version of the song after the source is changed is the same as the original version)",
"hot_key": "Shortcut Key Settings",
"hot_key_local_title": "Shortcut Keys in Software",
"hot_key_global_title": "Global Shortcut Key",
"hot_key_tip_input": "Please enter a new key",
"hot_key_unset_input": "Not Set",
"hot_key_common_toggle_close": "Quit Program",
"hot_key_common_toggle_min": "Minimize/Restore Program",
"hot_key_common_min": "Minimize the program",
"hot_key_common_toggle_hide": "Show/Hide Program",
"hot_key_common_focus_search_input": "Focus Search Box",
"hot_key_player_toggle_play": "Play/Pause Control",
"hot_key_player_prev": "Previous Song",
"hot_key_common_min": "Minimize the program",
"hot_key_common_toggle_close": "Quit Program",
"hot_key_common_toggle_hide": "Show/Hide Program",
"hot_key_common_toggle_min": "Minimize/Restore Program",
"hot_key_desktop_lyric_toggle_always_top": "Top Desktop Lyrics Switch",
"hot_key_desktop_lyric_toggle_lock": "Desktop Lyric Lock Switch",
"hot_key_desktop_lyric_toggle_visible": "Turn on/off desktop lyrics",
"hot_key_global_title": "Global Shortcut Key",
"hot_key_local_title": "Shortcut Keys in Software",
"hot_key_player_next": "Next Song",
"hot_key_player_volume_up": "Increase Volume",
"hot_key_player_prev": "Previous Song",
"hot_key_player_toggle_play": "Play/Pause Control",
"hot_key_player_volume_down": "Reduce Volume",
"hot_key_player_volume_mute": "Mute Switch",
"hot_key_desktop_lyric_toggle_visible": "Turn on/off desktop lyrics",
"hot_key_desktop_lyric_toggle_lock": "Desktop Lyric Lock Switch",
"hot_key_desktop_lyric_toggle_always_top": "Top Desktop Lyrics Switch",
"hot_key_player_volume_up": "Increase Volume",
"hot_key_tip_input": "Please enter a new key",
"hot_key_unset_input": "Not Set",
"is_enable": "Enabled",
"is_show": "Showed",
"list": "List",
"list_add_music_location_type": "Position when adding a song to the list",
"list_add_music_location_type_bottom": "Bottom",
"list_add_music_location_type_top": "Top",
"list_scroll": "Remember the position of the scroll bar of the playlist (only valid for my music classification)",
"list_source": "Show song source (only valid for my music category)",
"network": "Network",
"network_proxy_title": "HTTP Proxy (False setting would block Internet connections)",
"network_proxy_host": "Host",
"network_proxy_port": "Port",
"network_proxy_username": "Username",
"network_proxy_password": "Password",
"network_proxy_port": "Port",
"network_proxy_title": "HTTP Proxy (False setting would block Internet connections)",
"network_proxy_username": "Username",
"odc": "Auto clear",
"odc_clear_search_input": "Clear the search box when you are not searching",
"odc_clear_search_list": "Clear the search list when you are not searching",
"backup": "Backup and restore",
"backup_part": "Partial data (list data includes audition list, favorite list, user-defined list, setting data does not include shortcut key settings)",
"backup_part_import_list": "Import lists",
"backup_part_export_list": "Export lists",
"backup_part_import_setting": "Import settings",
"backup_part_export_setting": "Export settings",
"backup_all": "All data (list data and setting data)",
"backup_all_import": "Import",
"backup_all_export": "Export",
"backup_all_import_desc": "Select a backup file",
"backup_all_export_desc": "Select the backup to...",
"backup_part_import_setting_desc": "Select the Settings file",
"backup_part_export_setting_desc": "Save the list to...",
"backup_part_import_list_desc": "Select a list backup",
"backup_part_export_list_desc": "Save the list to...",
"other": "Extras",
"other_play_list_cache": "List cache management (links to songs that have been cached in my list, alternative sources for playback, after cleaning up, you need to re-acquire them when you play and download songs, and do not clean up if there are no issues related to song playback)",
"other_play_list_cache_clear_btn": "Clear list cache information",
"other_resource_cache": "Resource cache management (pictures, audios and other caches, pictures and other resources will need to be downloaded again after cleaning up, it is not recommended to clean up, the software will dynamically manage the cache size according to the disk space)",
"other_resource_cache_clear_btn": "Clear resource cache",
"other_resource_cache_label": "The software has used cache size: ",
"other_tray_theme": "Tray Icon Style",
"other_tray_theme_native": "Solid Color",
"other_tray_theme_origin": "Primary Color",
"other_resource_cache": "Resource cache management (pictures, audios and other caches, pictures and other resources will need to be downloaded again after cleaning up, it is not recommended to clean up, the software will dynamically manage the cache size according to the disk space)",
"other_resource_cache_label": "The software has used cache size: ",
"other_resource_cache_clear_btn": "Clear resource cache",
"other_play_list_cache": "List cache management (links to songs that have been cached in my list, alternative sources for playback, after cleaning up, you need to re-acquire them when you play and download songs, and do not clean up if there are no issues related to song playback)",
"other_play_list_cache_clear_btn": "Clear list cache information",
"play": "Play",
"play_lyric_lxlrc": "Use Karaoke-style lyrics playback (if supported)",
"play_lyric_transition": "Show lyrics translation",
"play_mediaDevice": "Audio output",
"play_mediaDevice_remove_stop_play": "Pause the song when the current sound output device is changed",
"play_mediaDevice_title": "Select a media device for audio output",
"play_quality": "Play 320K quality songs first (if supported)",
"play_save_play_time": "Remember playback progress",
"play_task_bar": "Show playing progress on the taskbar",
"search": "Search",
"search_focus_search_box": "Automatically focus the search box on startup",
"search_history": "Search history",
"search_hot": "Top Searches",
"sync": "Data synchronization [This is a test function, it is recommended to back up the playlist before using it for the first time]",
"sync_address": "Synchronization service address: {address}",
"sync_auth_code": "Connection code: {code}",
"sync_device": "Connected devices: {devices}",
"sync_enable": "Enable the synchronization function (because the data is transmitted in clear text, please use it under a trusted network)",
"sync_port": "Sync port settings",
"sync_port_tip": "Please enter the synchronization service port number",
"sync_refresh_code": "Refresh the connection code",
"update": "Update",
"update_latest_label": "Latest version: ",
"update_unknown": "Unknown",
"update_checking": "Checking for updates...",
"update_current_label": "Current version: ",
"update_downloading": "Update is found and being downloaded...⏳",
"update_progress": "Download progress: ",
"update_latest": "The software is up-to-date, enjoy yourself!🥂",
"update_open_version_modal_btn": "Open the update window🚀",
"update_checking": "Checking for updates...",
"update_init": "Processing update...",
"about": "About lx-music-desktop",
"is_enable": "Enabled",
"is_show": "Showed",
"click_open": "Click to open",
"click_copy": "Click to copy"
"update_latest": "The software is up-to-date, enjoy yourself!🥂",
"update_latest_label": "Latest version: ",
"update_open_version_modal_btn": "Open the update window🚀",
"update_progress": "Download progress: ",
"update_unknown": "Unknown"
}

View File

@@ -1,5 +1,7 @@
{
"date_format_second": "{num}秒前",
"cancel_button_text": "我不",
"confirm_button_text": "好的",
"date_format_hour": "{num}小时前",
"date_format_minute": "{num}分钟前",
"date_format_hour": "{num}小时前"
"date_format_second": "{num}前"
}

View File

@@ -1,43 +1,43 @@
{
"copy_title": "(点击复制)",
"volume": "当前音量:",
"pause": "暂停",
"play": "播放",
"prev": "上一首",
"next": "下一首",
"playing": "播放中...",
"stop": "暂停播放",
"end": "播放完毕",
"refresh_url": "URL过期正在刷新URL...",
"error": "音频加载出错5 秒后切换下一首",
"loading": "音乐加载中...",
"buffering": "缓冲中...",
"geting_url": "歌曲链接获取中...",
"lyric_error": "歌词获取失败",
"hide_detail": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"name": "歌曲名:",
"singer": "艺术家:",
"album": "专辑名:",
"add_music_to": "添加当前歌曲到...",
"desktop_lyric_on": "开启桌面歌词",
"desktop_lyric_off": "关闭桌面歌词",
"desktop_lyric_lock": "右击锁定歌词",
"desktop_lyric_unlock": "右击解锁歌词",
"play_toggle_mode_list_loop": "列表循环",
"play_toggle_mode_random": "列表随机",
"play_toggle_mode_list": "顺序播放",
"play_toggle_mode_single_loop": "单曲循环",
"play_toggle_mode_off": "禁用",
"pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"comment_show": "歌曲评论",
"comment_hot_loading": "热门评论加载中",
"comment_new_loading": "最新评论加载中",
"album": "专辑名:",
"buffering": "缓冲中...",
"comment_hot_load_error": "热门评论加载失败,点击尝试重新加载",
"comment_new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment_refresh": "刷新评论",
"comment_no_content": "暂无评论",
"comment_hot_loading": "热门评论加载",
"comment_hot_title": "热门评论",
"comment_new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment_new_loading": "最新评论加载中",
"comment_new_title": "最新评论",
"comment_title": "{name} 的评论"
"comment_no_content": "暂无评论",
"comment_refresh": "刷新评论",
"comment_show": "歌曲评论",
"comment_title": "{name} 的评论",
"copy_title": "(点击复制)",
"desktop_lyric_lock": "右击锁定歌词",
"desktop_lyric_off": "关闭桌面歌词",
"desktop_lyric_on": "开启桌面歌词",
"desktop_lyric_unlock": "右击解锁歌词",
"end": "播放完毕",
"error": "音频加载出错5 秒后切换下一首",
"geting_url": "歌曲链接获取中...",
"hide_detail": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"loading": "音乐加载中...",
"lyric_error": "歌词获取失败",
"lyric_select": "歌词文本选择",
"name": "歌曲名:",
"next": "下一首",
"pause": "暂停",
"pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"play": "播放",
"play_toggle_mode_list": "顺序播放",
"play_toggle_mode_list_loop": "列表循环",
"play_toggle_mode_off": "禁用",
"play_toggle_mode_random": "列表随机",
"play_toggle_mode_single_loop": "单曲循环",
"playing": "播放中...",
"prev": "上一首",
"refresh_url": "URL过期正在刷新URL...",
"singer": "艺术家:",
"stop": "暂停播放",
"volume": "当前音量:"
}

View File

@@ -0,0 +1,19 @@
{
"merge_btn_local_remote": "本机列表 合并 远程列表",
"merge_btn_remote_local": "远程列表 合并 本机列表",
"merge_label": "合并",
"merge_tip": "合并:",
"merge_tip_desc": "将两边的列表合并到一起,相同的歌曲将被去掉(去掉的是被合并者的歌曲),不同的歌曲将被添加。",
"other_label": "其他",
"other_tip": "其他:",
"other_tip_desc": "“仅使用实时同步功能”将不修改双方的列表,仅实时同步操作;“取消同步”将直接断开双方的连接。",
"overwrite": "完全覆盖",
"overwrite_btn_cancel": "取消同步",
"overwrite_btn_local_remote": "本机列表 覆盖 远程列表",
"overwrite_btn_none": "仅使用实时同步功能",
"overwrite_btn_remote_local": "远程列表 覆盖 本机列表",
"overwrite_label": "覆盖",
"overwrite_tip": "覆盖:",
"overwrite_tip_desc": "被覆盖者与覆盖者列表ID相同的列表将被删除后替换成覆盖者的列表列表ID不同的列表将被合并到一起若勾选完全覆盖则被覆盖者的所有列表将被移除然后替换成覆盖者的列表。",
"title": "选择与 {name} 的列表同步方式"
}

View File

@@ -1,28 +1,38 @@
{
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_rename": "重命名",
"lists_moveup": "上移",
"lists_movedown": "下移",
"lists_sync": "同步",
"lists_remove": "删除",
"action": "操作",
"album": "专辑",
"default_list": "试听列表",
"list_add_to": "添加到...",
"list_copy_name": "复制歌曲名",
"list_download": "下载",
"list_move_to": "移动到...",
"list_play": "播放",
"list_play_later": "稍后播放",
"list_copy_name": "复制歌曲名",
"list_source_detail": "歌曲详情页",
"list_add_to": "添加到...",
"list_move_to": "移动到...",
"list_sort": "调整位置",
"list_download": "下载",
"list_search": "搜索",
"list_remove": "删除",
"default_list": "试听列表",
"list_search": "搜索",
"list_sort": "调整位置",
"list_source_detail": "歌曲详情页",
"lists_duplicate": "重复歌曲",
"lists_export": "导出",
"lists_export_part_desc": "选择列表文件保存位置",
"lists_import": "导入",
"lists_import_part_button_cancel": "不要啊",
"lists_import_part_button_confirm": "覆盖掉",
"lists_import_part_confirm": "导入的列表({importName})与本地列表({localName}的ID相同是否覆盖本地列表",
"lists_import_part_desc": "选择列表文件",
"lists_movedown": "下移",
"lists_moveup": "上移",
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_remove": "删除",
"lists_remove_tip": "你真的想要移除 {name} 吗?",
"lists_remove_tip_button": "是的 没错",
"lists_rename": "重命名",
"lists_sync": "更新",
"loding_list": "加载中...",
"love_list": "收藏",
"name": "歌曲名",
"no_item": "列表竟然是空的...",
"singer": "歌手",
"album": "专辑",
"action": "操作",
"time": "时长",
"loding_list": "加载中...",
"no_item": "列表竟然是空的..."
"time": "时长"
}

View File

@@ -1,154 +1,156 @@
{
"about": "关于洛雪音乐",
"backup": "备份与恢复",
"backup_all": "所有数据(列表数据与设置数据)",
"backup_all_export": "导出",
"backup_all_export_desc": "选择备份保存位置",
"backup_all_import": "导入",
"backup_all_import_desc": "选择备份文件",
"backup_part": "部分数据(列表数据包括试听列表、收藏列表、用户自定义列表,设置数据不包括快捷键设置)",
"backup_part_export_list": "导出列表",
"backup_part_export_list_desc": "选择歌单保存位置",
"backup_part_export_setting": "导出设置",
"backup_part_export_setting_desc": "选择设置保存位置",
"backup_part_import_list": "导入列表",
"backup_part_import_list_desc": "选择列表文件",
"backup_part_import_setting": "导入设置",
"backup_part_import_setting_desc": "选择配置文件",
"basic": "基本设置",
"basic_theme": "主题颜色",
"basic_animation": "弹出层随机动画",
"basic_show_animation": "显示切换动画",
"basic_source_title": "选择音乐来源",
"basic_source_test": "测试接口(几乎软件的所有功能都可用)",
"basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
"basic_source": "音乐来源",
"basic_sourcename_title": "选择音源名字类型",
"basic_sourcename_real": "原名",
"basic_sourcename_alias": "别名",
"basic_sourcename": "音源名字",
"basic_source_status_success": "初始化成功",
"basic_source_status_initing": "初始化中",
"basic_source_status_failed": "初始化失败",
"basic_source_user_api_btn": "自定义源管理",
"basic_window_size_title": "设置软件窗口尺寸",
"basic_window_size": "窗口尺寸",
"basic_window_size_smaller": "较小",
"basic_window_size_small": "小",
"basic_window_size_medium": "中",
"basic_window_size_big": "大",
"basic_window_size_larger": "较大",
"basic_window_size_oversized": "超大",
"basic_window_size_huge": "巨大",
"basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘",
"basic_lang_title": "软件显示的语言",
"basic_lang": "语言",
"basic_control_btn_position": "控制按钮位置",
"basic_control_btn_position_left": "左边",
"basic_control_btn_position_right": "右边",
"play": "播放设置",
"play_save_play_time": "记住播放进度",
"play_lyric_transition": "显示歌词翻译",
"play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"play_quality": "优先播放320K品质的歌曲如果支持",
"play_task_bar": "在任务栏上显示当前歌曲播放进度",
"play_mediaDevice_title": "选择声音输出的媒体设备",
"play_mediaDevice": "音频输出",
"play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"basic_lang": "语言",
"basic_lang_title": "软件显示的语言",
"basic_show_animation": "显示切换动画",
"basic_source": "音乐来源",
"basic_source_status_failed": "初始化失败",
"basic_source_status_initing": "初始化中",
"basic_source_status_success": "初始化成功",
"basic_source_temp": "临时接口(软件的某些功能不可用,建议测试接口不可用再使用本接口)",
"basic_source_test": "测试接口(几乎软件的所有功能都可用)",
"basic_source_title": "选择音乐来源",
"basic_source_user_api_btn": "自定义源管理",
"basic_sourcename": "音源名字",
"basic_sourcename_alias": "别名",
"basic_sourcename_real": "原名",
"basic_sourcename_title": "选择音源名字类型",
"basic_theme": "主题颜色",
"basic_to_tray": "关闭软件时不退出软件将其最小化到系统托盘",
"basic_window_size": "窗口尺寸",
"basic_window_size_big": "大",
"basic_window_size_huge": "巨大",
"basic_window_size_larger": "较大",
"basic_window_size_medium": "中",
"basic_window_size_oversized": "超大",
"basic_window_size_small": "小",
"basic_window_size_smaller": "较小",
"basic_window_size_title": "设置软件窗口尺寸",
"click_copy": "点击复制",
"click_open": "点击打开",
"desktop_lyric": "桌面歌词设置",
"desktop_lyric_enable": "显示歌词",
"desktop_lyric_lock": "锁定歌词",
"desktop_lyric_always_on_top": "使歌词总是在其他窗口之上",
"desktop_lyric_enable": "显示歌词",
"desktop_lyric_font": "歌词字体",
"desktop_lyric_font_default": "默认",
"desktop_lyric_lock": "锁定歌词",
"desktop_lyric_lock_screen": "不允许歌词窗口拖出主屏幕之外",
"search": "搜索设置",
"search_hot": "显示热门搜索",
"search_history": "显示历史搜索记录",
"search_focus_search_box": "启动时自动聚焦搜索框",
"list": "列表设置",
"list_source": "显示歌曲源(仅对我的音乐分类有效)",
"list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)",
"download": "下载设置",
"download_enable": "是否启用下载功能",
"download_path_title": "下载歌曲保存的路径",
"download_path": "下载路径",
"download_path_label": "当前下载路径:",
"download_path_open_label": "点击打开当前路径",
"download_path_change_btn": "更改",
"download_name_title": "下载歌曲时的命名方式",
"download_name": "文件命名方式",
"download_data_embed": "是否将以下内容嵌入到音频文件中",
"download_embed_pic": "封面嵌入",
"download_embed_lyric": "歌词嵌入",
"download_lyric_title": "是否同时下载歌词文件",
"download_embed_pic": "封面嵌入",
"download_enable": "是否启用下载功能",
"download_lyric": "歌词下载",
"download_lyric_format": "下载的歌词文件编码格式",
"download_lyric_format_gbk": "GBK在某些设备上出现中文乱码时可尝试选择此格式",
"download_lyric_format_utf8": "UTF-8",
"download_lyric_title": "是否同时下载歌词文件",
"download_name": "文件命名方式",
"download_name1": "歌名 - 歌手",
"download_name2": "歌手 - 歌名",
"download_name3": "歌名",
"download_use_other_source": "自动换源下载当无法从歌曲的原始源下载时尝试切换到其他源下载此功能不100%保证换源后的歌曲版本与原版一致)",
"download_name_title": "下载歌曲时的命名方式",
"download_path": "下载路径",
"download_path_change_btn": "更改",
"download_path_label": "当前下载路径:",
"download_path_open_label": "点击打开当前路径",
"download_path_title": "下载歌曲保存的路径",
"download_select_save_path": "选择歌曲保存路径",
"download_use_other_source": "自动换源下载当无法从歌曲的原始源下载时尝试切换到其他源下载此功能不100%保证换源后的歌曲版本与原版一致)",
"hot_key": "快捷键设置",
"hot_key_local_title": "软件内快捷键",
"hot_key_global_title": "全局快捷键",
"hot_key_tip_input": "请输入新的按键",
"hot_key_unset_input": "未设置",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_min": "最小化/还原程序",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_hide": "显示/隐藏程序",
"hot_key_common_focus_search_input": "聚焦搜索框",
"hot_key_player_toggle_play": "播放/暂停控制",
"hot_key_player_prev": "上一首歌曲",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_hide": "显示/隐藏程序",
"hot_key_common_toggle_min": "最小化/还原程序",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌词置顶切换",
"hot_key_desktop_lyric_toggle_lock": "桌面歌词锁定切换",
"hot_key_desktop_lyric_toggle_visible": "开/关桌面歌词",
"hot_key_global_title": "全局快捷键",
"hot_key_local_title": "软件内快捷键",
"hot_key_player_next": "下一首歌曲",
"hot_key_player_volume_up": "增加音量",
"hot_key_player_prev": "上一首歌曲",
"hot_key_player_toggle_play": "播放/暂停控制",
"hot_key_player_volume_down": "减少音量",
"hot_key_player_volume_mute": "静音切换",
"hot_key_desktop_lyric_toggle_visible": "开/关桌面歌词",
"hot_key_desktop_lyric_toggle_lock": "桌面歌词锁定切换",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌词置顶切换",
"hot_key_player_volume_up": "增加音量",
"hot_key_tip_input": "请输入新的按键",
"hot_key_unset_input": "未设置",
"is_enable": "是否启用",
"is_show": "是否显示",
"list": "列表设置",
"list_add_music_location_type": "添加歌曲到列表时的位置",
"list_add_music_location_type_bottom": "底部",
"list_add_music_location_type_top": "顶部",
"list_scroll": "记住播放列表滚动条位置(仅对我的音乐分类有效)",
"list_source": "显示歌曲源(仅对我的音乐分类有效)",
"network": "网络设置",
"network_proxy_title": "HTTP代理设置乱设置软件将无法联网",
"network_proxy_host": "主机",
"network_proxy_port": "端口",
"network_proxy_username": "用户名",
"network_proxy_password": "密码",
"network_proxy_port": "端口",
"network_proxy_title": "HTTP代理设置乱设置软件将无法联网",
"network_proxy_username": "用户名",
"odc": "强迫症设置",
"odc_clear_search_input": "离开搜索界面时清空搜索框",
"odc_clear_search_list": "离开搜索界面时清空搜索列表",
"backup": "备份与恢复",
"backup_part": "部分数据(列表数据包括试听列表、收藏列表、用户自定义列表,设置数据不包括快捷键设置)",
"backup_part_import_list": "导入列表",
"backup_part_export_list": "导出列表",
"backup_part_import_setting": "导入设置",
"backup_part_export_setting": "导出设置",
"backup_all": "所有数据(列表数据与设置数据)",
"backup_all_import": "导入",
"backup_all_export": "导出",
"backup_all_import_desc": "选择备份文件",
"backup_all_export_desc": "选择备份保存位置",
"backup_part_import_setting_desc": "选择配置文件",
"backup_part_export_setting_desc": "选择设置保存位置",
"backup_part_import_list_desc": "选择列表文件",
"backup_part_export_list_desc": "选择歌单保存位置",
"other": "其他",
"other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)",
"other_play_list_cache_clear_btn": "清理列表缓存信息",
"other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_resource_cache_clear_btn": "清理资源缓存",
"other_resource_cache_label": "软件已使用缓存大小:",
"other_tray_theme": "托盘图标样式",
"other_tray_theme_native": "纯色",
"other_tray_theme_origin": "原色",
"other_resource_cache": "资源缓存管理(图片、音频等缓存,清理后图片等资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小)",
"other_resource_cache_label": "软件已使用缓存大小:",
"other_resource_cache_clear_btn": "清理资源缓存",
"other_play_list_cache": "列表缓存管理(我的列表中已缓存的歌曲链接、播放代替源,清理后播放、下载歌曲时需要重新获取,没有歌曲播放相关的问题不要清理)",
"other_play_list_cache_clear_btn": "清理列表缓存信息",
"play": "播放设置",
"play_lyric_lxlrc": "使用卡拉OK式歌词播放如果支持",
"play_lyric_transition": "显示歌词翻译",
"play_mediaDevice": "音频输出",
"play_mediaDevice_remove_stop_play": "当前的声音输出设备被改变时暂停播放歌曲",
"play_mediaDevice_title": "选择声音输出的媒体设备",
"play_quality": "优先播放320K品质的歌曲如果支持",
"play_save_play_time": "记住播放进度",
"play_task_bar": "在任务栏上显示当前歌曲播放进度",
"search": "搜索设置",
"search_focus_search_box": "启动时自动聚焦搜索框",
"search_history": "显示历史搜索记录",
"search_hot": "显示热门搜索",
"sync": "数据同步 [此为测试功能,首次使用前建议先备份一次歌单]",
"sync_address": "同步服务地址:{address}",
"sync_auth_code": "连接码:{code}",
"sync_device": "已连接的设备:{devices}",
"sync_enable": "启用同步功能(由于数据是明文传输,请在受信任的网络下使用)",
"sync_port": "同步端口设置",
"sync_port_tip": "请输入同步服务端口号",
"sync_refresh_code": "刷新连接码",
"update": "软件更新",
"update_latest_label": "最新版本:",
"update_unknown": "未知",
"update_checking": "检查更新中...",
"update_current_label": "当前版本:",
"update_downloading": "发现新版本并在努力下载中,请稍后...⏳",
"update_progress": "下载进度:",
"update_latest": "软件已是最新,尽情地体验吧~🥂",
"update_open_version_modal_btn": "打开更新窗口 🚀",
"update_checking": "检查更新中...",
"update_init": "处理更新中...",
"about": "关于洛雪音乐",
"is_enable": "是否启用",
"is_show": "是否显示",
"click_open": "点击打开",
"click_copy": "点击复制"
"update_latest": "软件已是最新,尽情地体验吧~🥂",
"update_latest_label": "最新版本:",
"update_open_version_modal_btn": "打开更新窗口 🚀",
"update_progress": "下载进度:",
"update_unknown": "未知"
}

View File

@@ -1,5 +1,7 @@
{
"date_format_second": "{num}秒前",
"cancel_button_text": "取消",
"confirm_button_text": "好的",
"date_format_hour": "{num}小時前",
"date_format_minute": "{num}分鐘前",
"date_format_hour": "{num}小時前"
"date_format_second": "{num}前"
}

View File

@@ -1,43 +1,43 @@
{
"copy_title": "(點擊複製)",
"volume": "當前音量:",
"pause": "暫停",
"play": "播放",
"prev": "上一首",
"next": "下一首",
"playing": "播放中...",
"stop": "暫停播放",
"end": "播放完畢",
"refresh_url": "URL過期正在刷新URL...",
"error": "音頻加載出錯5 秒後切換下一首",
"loading": "音樂加載中...",
"buffering": "緩衝中...",
"geting_url": "歌曲鏈接獲取中...",
"lyric_error": "歌詞獲取失敗",
"hide_detail": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"name": "歌曲名:",
"singer": "藝術家:",
"album": "專輯名:",
"add_music_to": "添加當前歌曲到...",
"desktop_lyric_on": "開啟桌面歌詞",
"desktop_lyric_off": "關閉桌面歌詞",
"desktop_lyric_lock": "右擊鎖定歌詞",
"desktop_lyric_unlock": "右擊解鎖歌詞",
"play_toggle_mode_list_loop": "列表循環",
"play_toggle_mode_random": "列表隨機",
"play_toggle_mode_list": "順序播放",
"play_toggle_mode_single_loop": "單曲循環",
"play_toggle_mode_off": "禁用",
"pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"comment_show": "歌曲評論",
"comment_hot_loading": "熱門評論加載中",
"comment_new_loading": "最新評論加載中",
"album": "專輯名:",
"buffering": "緩衝中...",
"comment_hot_load_error": "熱門評論加載失敗,點擊嘗試重新加載",
"comment_new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment_refresh": "刷新評論",
"comment_no_content": "暫無評論",
"comment_hot_loading": "熱門評論加載",
"comment_hot_title": "熱門評論",
"comment_new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment_new_loading": "最新評論加載中",
"comment_new_title": "最新評論",
"comment_title": "{name} 的評論"
"comment_no_content": "暫無評論",
"comment_refresh": "刷新評論",
"comment_show": "歌曲評論",
"comment_title": "{name} 的評論",
"copy_title": "(點擊複製)",
"desktop_lyric_lock": "右擊鎖定歌詞",
"desktop_lyric_off": "關閉桌面歌詞",
"desktop_lyric_on": "開啟桌面歌詞",
"desktop_lyric_unlock": "右擊解鎖歌詞",
"end": "播放完畢",
"error": "音頻加載出錯5 秒後切換下一首",
"geting_url": "歌曲鏈接獲取中...",
"hide_detail": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"loading": "音樂加載中...",
"lyric_error": "歌詞獲取失敗",
"lyric_select": "歌詞文本選擇",
"name": "歌曲名:",
"next": "下一首",
"pause": "暫停",
"pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"play": "播放",
"play_toggle_mode_list": "順序播放",
"play_toggle_mode_list_loop": "列表循環",
"play_toggle_mode_off": "禁用",
"play_toggle_mode_random": "列表隨機",
"play_toggle_mode_single_loop": "單曲循環",
"playing": "播放中...",
"prev": "上一首",
"refresh_url": "URL過期正在刷新URL...",
"singer": "藝術家:",
"stop": "暫停播放",
"volume": "當前音量:"
}

View File

@@ -0,0 +1,19 @@
{
"merge_btn_local_remote": "本機列表 合併 遠程列表",
"merge_btn_remote_local": "遠程列表 合併 本機列表",
"merge_label": "合併",
"merge_tip": "合併:",
"merge_tip_desc": "將兩邊的列表合併到一起,相同的歌曲將被去掉(去掉的是被合併者的歌曲),不同的歌曲將被添加。",
"other_label": "其他",
"other_tip": "其他:",
"other_tip_desc": "“僅使用實時同步功能”將不修改雙方的列表,僅實時同步操作;“取消同步”將直接斷開雙方的連接。",
"overwrite": "完全覆蓋",
"overwrite_btn_cancel": "取消同步",
"overwrite_btn_local_remote": "本機列表 覆蓋 遠程列表",
"overwrite_btn_none": "僅使用實時同步功能",
"overwrite_btn_remote_local": "遠程列表 覆蓋 本機列表",
"overwrite_label": "覆蓋",
"overwrite_tip": "覆蓋:",
"overwrite_tip_desc": "被覆蓋者與覆蓋者列表ID相同的列表將被刪除後替換成覆蓋者的列表列表ID不同的列表將被合併到一起若勾選完全覆蓋則被覆蓋者的所有列表將被移除然後替換成覆蓋者的列表。",
"title": "選擇與 {name} 的列表同步方式"
}

View File

@@ -1,28 +1,38 @@
{
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_rename": "重命名",
"lists_moveup": "上移",
"lists_movedown": "下移",
"lists_sync": "同步",
"lists_remove": "刪除",
"action": "操作",
"album": "專輯",
"default_list": "試聽列表",
"list_add_to": "添加到...",
"list_copy_name": "複製歌曲名",
"list_download": "下載",
"list_move_to": "移動到...",
"list_play": "播放",
"list_play_later": "稍後播放",
"list_copy_name": "複製歌曲名",
"list_add_to": "添加到...",
"list_move_to": "移動到...",
"list_sort": "調整位置",
"list_download": "下載",
"list_search": "搜索",
"list_remove": "刪除",
"list_search": "搜索",
"list_sort": "調整位置",
"list_source_detail": "歌曲詳情頁",
"default_list": "試聽列表",
"lists_duplicate": "重複歌曲",
"lists_export": "導出",
"lists_export_part_desc": "選擇列表文件保存位置",
"lists_import": "導入",
"lists_import_part_button_cancel": "不要啊",
"lists_import_part_button_confirm": "覆蓋掉",
"lists_import_part_confirm": "導入的列表({importName})與本地列表({localName}的ID相同是否覆蓋本地列表",
"lists_import_part_desc": "選擇列表文件",
"lists_movedown": "下移",
"lists_moveup": "上移",
"lists_new_list_btn": "新建列表",
"lists_new_list_input": "新列表...",
"lists_remove": "刪除",
"lists_remove_tip": "你真的想要移除 {name} 嗎?",
"lists_remove_tip_button": "是的 沒錯",
"lists_rename": "重命名",
"lists_sync": "更新",
"loding_list": "加載中...",
"love_list": "收藏列表",
"name": "歌曲名",
"no_item": "列表竟然是空的...",
"singer": "歌手",
"album": "專輯",
"action": "操作",
"time": "時長",
"loding_list": "加載中...",
"no_item": "列表竟然是空的..."
"time": "時長"
}

View File

@@ -1,154 +1,156 @@
{
"about": "關於洛雪音樂",
"backup": "備份與恢復",
"backup_all": "所有數據(列表數據與設置數據)",
"backup_all_export": "導出",
"backup_all_export_desc": "選擇備份保存位置",
"backup_all_import": "導入",
"backup_all_import_desc": "選擇備份文件",
"backup_part": "部分數據(列表數據包括試聽列表、收藏列表、用戶自定義列表,設置數據不包括快捷鍵設置)",
"backup_part_export_list": "導出列表",
"backup_part_export_list_desc": "選擇歌單保存位置",
"backup_part_export_setting": "導出設置",
"backup_part_export_setting_desc": "選擇設置保存位置",
"backup_part_import_list": "導入列表",
"backup_part_import_list_desc": "選擇列表文件",
"backup_part_import_setting": "導入設置",
"backup_part_import_setting_desc": "選擇配置文件",
"basic": "基本設置",
"basic_theme": "主題顏色",
"basic_animation": "彈出層隨機動畫",
"basic_show_animation": "顯示切換動畫",
"basic_source_title": "選擇音樂來源",
"basic_source_test": "測試接口(幾乎軟件的所有功能都可用)",
"basic_source_temp": "臨時接口(軟件的某些功能不可用,建議測試接口不可用再使用本接口)",
"basic_source": "音樂來源",
"basic_sourcename_title": "選擇音源名字類型",
"basic_sourcename_real": "原名",
"basic_sourcename_alias": "別名",
"basic_sourcename": "音源名字",
"basic_source_status_success": "初始化成功",
"basic_source_status_initing": "初始化中",
"basic_source_status_failed": "初始化失敗",
"basic_source_user_api_btn": "自定義源管理",
"basic_window_size_title": "設置軟件窗口尺寸",
"basic_window_size": "窗口尺寸",
"basic_window_size_smaller": "較小",
"basic_window_size_small": "小",
"basic_window_size_medium": "中",
"basic_window_size_big": "大",
"basic_window_size_larger": "較大",
"basic_window_size_oversized": "超大",
"basic_window_size_huge": "巨大",
"basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤",
"basic_lang_title": "軟件顯示的語言",
"basic_lang": "語言",
"basic_control_btn_position": "控制按鈕位置",
"basic_control_btn_position_left": "左邊",
"basic_control_btn_position_right": "右邊",
"play": "播放設置",
"play_save_play_time": "記住播放進度",
"play_lyric_transition": "顯示歌詞翻譯",
"play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"play_quality": "優先播放320K品質的歌曲如果支持",
"play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"play_mediaDevice": "音頻輸出",
"play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"basic_lang": "語言",
"basic_lang_title": "軟件顯示的語言",
"basic_show_animation": "顯示切換動畫",
"basic_source": "音樂來源",
"basic_source_status_failed": "初始化失敗",
"basic_source_status_initing": "初始化中",
"basic_source_status_success": "初始化成功",
"basic_source_temp": "臨時接口(軟件的某些功能不可用,建議測試接口不可用再使用本接口)",
"basic_source_test": "測試接口(幾乎軟件的所有功能都可用)",
"basic_source_title": "選擇音樂來源",
"basic_source_user_api_btn": "自定義源管理",
"basic_sourcename": "音源名字",
"basic_sourcename_alias": "別名",
"basic_sourcename_real": "原名",
"basic_sourcename_title": "選擇音源名字類型",
"basic_theme": "主題顏色",
"basic_to_tray": "關閉軟件時不退出軟件將其最小化到系統托盤",
"basic_window_size": "窗口尺寸",
"basic_window_size_big": "大",
"basic_window_size_huge": "巨大",
"basic_window_size_larger": "較大",
"basic_window_size_medium": "中",
"basic_window_size_oversized": "超大",
"basic_window_size_small": "小",
"basic_window_size_smaller": "較小",
"basic_window_size_title": "設置軟件窗口尺寸",
"click_copy": "點擊複製",
"click_open": "點擊打開",
"desktop_lyric": "桌面歌詞設置",
"desktop_lyric_enable": "顯示歌詞",
"desktop_lyric_lock": "鎖定歌詞",
"desktop_lyric_always_on_top": "使歌詞總是在其他窗口之上",
"desktop_lyric_enable": "顯示歌詞",
"desktop_lyric_font": "歌詞字體",
"desktop_lyric_font_default": "默認",
"desktop_lyric_lock": "鎖定歌詞",
"desktop_lyric_lock_screen": "不允許歌詞窗口拖出主屏幕之外",
"search": "搜索設置",
"search_hot": "顯示熱門搜索",
"search_history": "顯示歷史搜索記錄",
"search_focus_search_box": "啟動時自動聚焦搜索框",
"list": "列表設置",
"list_source": "顯示歌曲源(僅對我的音樂分類有效)",
"list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)",
"download": "下載設置",
"download_enable": "是否啟用下載功能",
"download_path_title": "下載歌曲保存的路徑",
"download_path": "下載路徑",
"download_path_label": "當前下載路徑:",
"download_path_open_label": "點擊打開當前路徑",
"download_path_change_btn": "更改",
"download_name_title": "下載歌曲時的命名方式",
"download_name": "文件命名方式",
"download_data_embed": "是否將以下內容嵌入到音頻文件中",
"download_embed_pic": "封面嵌入",
"download_embed_lyric": "歌詞嵌入",
"download_lyric_title": "是否同時下載歌詞文件",
"download_embed_pic": "封面嵌入",
"download_enable": "是否啟用下載功能",
"download_lyric": "歌詞下載",
"download_lyric_format": "下載的歌詞文件編碼格式",
"download_lyric_format_gbk": "GBK在某些設備上出現中文亂碼時可嘗試選擇此格式",
"download_lyric_format_utf8": "UTF-8",
"download_lyric_title": "是否同時下載歌詞文件",
"download_name": "文件命名方式",
"download_name1": "歌名 - 歌手",
"download_name2": "歌手 - 歌名",
"download_name3": "歌名",
"download_use_other_source": "自動換源下載當無法從歌曲的原始源下載時嘗試切換到其他源下載此功能不100%保證換源後的歌曲版本與原版一致)",
"download_name_title": "下載歌曲時的命名方式",
"download_path": "下載路徑",
"download_path_change_btn": "更改",
"download_path_label": "當前下載路徑:",
"download_path_open_label": "點擊打開當前路徑",
"download_path_title": "下載歌曲保存的路徑",
"download_select_save_path": "選擇歌曲保存路徑",
"download_use_other_source": "自動換源下載當無法從歌曲的原始源下載時嘗試切換到其他源下載此功能不100%保證換源後的歌曲版本與原版一致)",
"hot_key": "快捷鍵設置",
"hot_key_local_title": "軟件內快捷鍵",
"hot_key_global_title": "全局快捷鍵",
"hot_key_tip_input": "請輸入新的按鍵",
"hot_key_unset_input": "未設置",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_min": "最小化/還原程序",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_hide": "顯示/隱藏程序",
"hot_key_common_focus_search_input": "聚焦搜索框",
"hot_key_player_toggle_play": "播放/暫停控制​​",
"hot_key_player_prev": "上一首歌曲",
"hot_key_common_min": "最小化程序",
"hot_key_common_toggle_close": "退出程序",
"hot_key_common_toggle_hide": "顯示/隱藏程序",
"hot_key_common_toggle_min": "最小化/還原程序",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌詞置頂切換",
"hot_key_desktop_lyric_toggle_lock": "桌面歌詞鎖定切換",
"hot_key_desktop_lyric_toggle_visible": "開/關桌面歌詞",
"hot_key_global_title": "全局快捷鍵",
"hot_key_local_title": "軟件內快捷鍵",
"hot_key_player_next": "下一首歌曲",
"hot_key_player_volume_up": "增加音量",
"hot_key_player_prev": "上一首歌曲",
"hot_key_player_toggle_play": "播放/暫停控制​​",
"hot_key_player_volume_down": "減少音量",
"hot_key_player_volume_mute": "靜音切換",
"hot_key_desktop_lyric_toggle_visible": "開/關桌面歌詞",
"hot_key_desktop_lyric_toggle_lock": "桌面歌詞鎖定切換",
"hot_key_desktop_lyric_toggle_always_top": "桌面歌詞置頂切換",
"hot_key_player_volume_up": "增加音量",
"hot_key_tip_input": "請輸入新的按鍵",
"hot_key_unset_input": "未設置",
"is_enable": "是否啟用",
"is_show": "是否顯示",
"list": "列表設置",
"list_add_music_location_type": "添加歌曲到列表時的位置",
"list_add_music_location_type_bottom": "底部",
"list_add_music_location_type_top": "頂部",
"list_scroll": "記住播放列表滾動條位置(僅對我的音樂分類有效)",
"list_source": "顯示歌曲源(僅對我的音樂分類有效)",
"network": "網絡設置",
"network_proxy_title": "HTTP代理設置亂設置軟件將無法聯網",
"network_proxy_host": "主機",
"network_proxy_port": "端口",
"network_proxy_username": "用戶名",
"network_proxy_password": "密碼",
"network_proxy_port": "端口",
"network_proxy_title": "HTTP代理設置亂設置軟件將無法聯網",
"network_proxy_username": "用戶名",
"odc": "強迫症設置",
"odc_clear_search_input": "離開搜索界面時清空搜索框",
"odc_clear_search_list": "離開搜索界面時清空搜索列表",
"backup": "備份與恢復",
"backup_part": "部分數據(列表數據包括試聽列表、收藏列表、用戶自定義列表,設置數據不包括快捷鍵設置)",
"backup_part_import_list": "導入列表",
"backup_part_export_list": "導出列表",
"backup_part_import_setting": "導入設置",
"backup_part_export_setting": "導出設置",
"backup_all": "所有數據(列表數據與設置數據)",
"backup_all_import": "導入",
"backup_all_export": "導出",
"backup_all_import_desc": "選擇備份文件",
"backup_all_export_desc": "選擇備份保存位置",
"backup_part_import_setting_desc": "選擇配置文件",
"backup_part_export_setting_desc": "選擇設置保存位置",
"backup_part_import_list_desc": "選擇列表文件",
"backup_part_export_list_desc": "選擇歌單保存位置",
"other": "其他",
"other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)",
"other_play_list_cache_clear_btn": "清理列表緩存信息",
"other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_resource_cache_clear_btn": "清理資源緩存",
"other_resource_cache_label": "軟件已使用緩存大小:",
"other_tray_theme": "托盤圖標樣式",
"other_tray_theme_native": "純色",
"other_tray_theme_origin": "原色",
"other_resource_cache": "資源緩存管理(圖片、音頻等緩存,清理後圖片等資源將需要重新下載,不建議清理,軟件會根據磁盤空間動態管理緩存大小)",
"other_resource_cache_label": "軟件已使用緩存大小:",
"other_resource_cache_clear_btn": "清理資源緩存",
"other_play_list_cache": "列表緩存管理(我的列表中已緩存的歌曲鏈接、播放代替源,清理後播放、下載歌曲時需要重新獲取,沒有歌曲播放相關的問題不要清理)",
"other_play_list_cache_clear_btn": "清理列表緩存信息",
"play": "播放設置",
"play_lyric_lxlrc": "使用卡拉OK式歌詞播放如果支持",
"play_lyric_transition": "顯示歌詞翻譯",
"play_mediaDevice": "音頻輸出",
"play_mediaDevice_remove_stop_play": "當前的聲音輸出設備被改變時暫停播放歌曲",
"play_mediaDevice_title": "選擇聲音輸出的媒體設備",
"play_quality": "優先播放320K品質的歌曲如果支持",
"play_save_play_time": "記住播放進度",
"play_task_bar": "在任務欄上顯示當前歌曲播放進度",
"search": "搜索設置",
"search_focus_search_box": "啟動時自動聚焦搜索框",
"search_history": "顯示歷史搜索記錄",
"search_hot": "顯示熱門搜索",
"sync": "數據同步 [此為測試功能,首次使用前建議先備份一次歌單]",
"sync_address": "同步服務地址:{address}",
"sync_auth_code": "連接碼:{code}",
"sync_device": "已連接的設備:{devices}",
"sync_enable": "啟用同步功能(由於數據是明文傳輸,請在受信任的網絡下使用)",
"sync_port": "同步端口設置",
"sync_port_tip": "請輸入同步服務端口號",
"sync_refresh_code": "刷新連接碼",
"update": "軟件更新",
"update_latest_label": "最新版本:",
"update_unknown": "未知",
"update_checking": "檢查更新中...",
"update_current_label": "當前版本:",
"update_downloading": "發現新版本並在努力下載中,請稍後...⏳",
"update_progress": "下載進度:",
"update_latest": "軟件已是最新,盡情地體驗吧~🥂",
"update_open_version_modal_btn": "打開更新窗口 🚀",
"update_checking": "檢查更新中...",
"update_init": "處理更新中...",
"about": "關於洛雪音樂",
"is_enable": "是否啟用",
"is_show": "是否顯示",
"click_open": "點擊打開",
"click_copy": "點擊複製"
"update_latest": "軟件已是最新,盡情地體驗吧~🥂",
"update_latest_label": "最新版本:",
"update_open_version_modal_btn": "打開更新窗口 🚀",
"update_progress": "下載進度:",
"update_unknown": "未知"
}

View File

@@ -26,6 +26,7 @@ Vue.config.productionTip = false
Vue.config.devtools = process.env.NODE_ENV === 'development'
getSetting().then(({ setting, version }) => {
global.appSetting = setting
// Set language automatically
if (!window.i18n.availableLocales.includes(setting.langId)) {
let langId = null

View File

@@ -0,0 +1,74 @@
<template>
<Modal :show="visible" @close="handleCancel" @after-leave="afterLeave" :closeBtn="false">
<main :class="$style.main">{{message}}</main>
<footer :class="$style.footer">
<Btn :class="$style.btn" v-if="showCancel" @click="handleCancel">{{cancelBtnText}}</Btn>
<Btn :class="$style.btn" @click="handleComfirm">{{confirmBtnText}}</Btn>
</footer>
</Modal>
</template>
<script>
import Modal from '@renderer/components/material/Modal'
import Btn from '@renderer/components/material/Btn'
export default {
components: {
Modal,
Btn,
},
data() {
return {
visible: false,
message: '',
showCancel: false,
cancelButtonText: '',
confirmButtonText: '',
}
},
computed: {
cancelBtnText() {
return this.cancelButtonText || this.$t('base.cancel_button_text')
},
confirmBtnText() {
return this.confirmButtonText || this.$t('base.confirm_button_text')
},
},
beforeDestroy() {
const el = this.$el
el.parentNode.removeChild(el)
},
methods: {
afterLeave(el, done) {
this.$destroy()
},
handleCancel() {
},
handleComfirm() {
},
},
}
</script>
<style lang="less" module>
.main {
flex: auto;
min-height: 40px;
padding: 15px;
font-size: 14px;
max-width: 320px;
min-width: 220px;
line-height: 1.5;
}
.footer {
flex: none;
padding: 0 15px 15px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
gap: 15px;
}
</style>

View File

@@ -0,0 +1,54 @@
import Dialog from './Dialog'
import i18n from '../i18n'
import store from '@renderer/store'
import Vue from 'vue'
const defaultOptions = {
message: '',
showCancel: false,
cancelButtonText: '',
confirmButtonText: '',
}
const dialog = {
install(Vue, options) {
const DialogConstructor = Vue.extend(Dialog)
const dialog = function Dialog(options) {
const { message, showCancel, cancelButtonText, confirmButtonText } =
Object.assign({}, defaultOptions, typeof options == 'string' ? { message: options } : options || {})
return new Promise((resolve, reject) => {
let instance = new DialogConstructor({ i18n, store }).$mount(document.createElement('div'))
// 属性设置
instance.visible = true
instance.message = message
instance.showCancel = showCancel
instance.cancelButtonText = cancelButtonText
instance.confirmButtonText = confirmButtonText
// 挂载
document.getElementById('container').appendChild(instance.$el)
instance.handleCancel = () => {
instance.visible = false
resolve(false)
}
instance.handleComfirm = () => {
instance.visible = false
resolve(true)
}
})
}
dialog.confirm = options => dialog(
typeof options == 'string'
? { message: options, showCancel: true }
: { ...options, showCancel: true },
)
Vue.prototype.$dialog = dialog
},
}
Vue.use(dialog)

View File

@@ -3,6 +3,8 @@ import { debounce } from '../../utils'
let instance
let prevTips
let prevX = 0
let prevY = 0
const getTips = el =>
el
@@ -53,6 +55,9 @@ const updateTips = event => {
}
document.body.addEventListener('mousemove', event => {
if (event.x == prevX && event.y == prevY) return
prevX = event.x
prevY = event.y
hideTips()
showTips(event)
})

View File

@@ -1,2 +1,3 @@
// import './axios'
import './Dialog'
import './Tips'

View File

@@ -11,11 +11,14 @@ import {
getMusicUrl as getMusicUrlFormStorage,
setMusicUrl,
assertApiSupport,
filterFileName,
} from '../../utils'
import { NAMES, rendererInvoke } from '@common/ipc'
window.downloadList = []
// state
const state = {
list: [],
list: window.downloadList,
waitingList: [],
downloadStatus: {
RUN: 'run',
@@ -31,7 +34,6 @@ const dls = {}
const tryNum = {}
let isRuningActionTask = false
const filterFileName = /[\\/:*?#"<>|]/g
// getters
const getters = {
@@ -191,13 +193,13 @@ const getMusicUrl = async function(downloadInfo, isUseOtherSource, isRefresh) {
return cachedUrl && !isRefresh
? cachedUrl
: (
isUseOtherSource
? handleGetMusicUrl.call(this, downloadInfo.musicInfo, downloadInfo.type)
: music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type).promise
).then(({ url }) => {
setMusicUrl(downloadInfo.musicInfo, downloadInfo.type, url)
return url
})
isUseOtherSource
? handleGetMusicUrl.call(this, downloadInfo.musicInfo, downloadInfo.type)
: music[downloadInfo.musicInfo.source].getMusicUrl(downloadInfo.musicInfo, downloadInfo.type).promise
).then(({ url }) => {
setMusicUrl(downloadInfo.musicInfo, downloadInfo.type, url)
return url
})
}
const getPic = function(musicInfo, retryedSource = [], originMusic) {
// console.log(musicInfo.source)
@@ -223,7 +225,8 @@ const getPic = function(musicInfo, retryedSource = [], originMusic) {
})
})
}
const getLyric = function(musicInfo, retryedSource = [], originMusic) {
const handleGetLyric = function(musicInfo, retryedSource = [], originMusic) {
if (!originMusic) originMusic = musicInfo
let reqPromise
try {
@@ -247,6 +250,33 @@ const getLyric = function(musicInfo, retryedSource = [], originMusic) {
})
}
const getLyric = function(musicInfo, isUseOtherSource) {
return getLyricFromStorage(musicInfo).then(lrcInfo => {
return (
lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? handleGetLyric.call(this, musicInfo)
: music[musicInfo.source].getLyric(musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
).then(lrcs => {
if (!lrcs) return lrcs
if (global.i18n.locale != 'zh-tw') return lrcs
return rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lrcs.lyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString()).then(lyric => {
lrcs.lyric = lyric
return lrcs
})
})
})
}
// 修复 1.1.x版本 酷狗源歌词格式
const fixKgLyric = lrc => /\[00:\d\d:\d\d.\d+\]/.test(lrc) ? lrc.replace(/(?:\[00:(\d\d:\d\d.\d+\]))/gm, '[$1') : lrc
@@ -263,30 +293,16 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
? downloadInfo.musicInfo.img
? Promise.resolve(downloadInfo.musicInfo.img)
: (
isUseOtherSource
? getPic.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getPic(downloadInfo.musicInfo).promise
).catch(err => {
console.log(err)
return null
})
: Promise.resolve(),
isEmbedLyric
? getLyricFromStorage(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? getLyric.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
? getPic.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getPic(downloadInfo.musicInfo).promise
).catch(err => {
console.log(err)
return null
})
})
: Promise.resolve(),
isEmbedLyric
? getLyric.call(this, downloadInfo.musicInfo, isUseOtherSource)
: Promise.resolve(),
]
Promise.all(tasks).then(([imgUrl, lyrics = {}]) => {
@@ -306,19 +322,11 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
* @param {*} downloadInfo
* @param {*} filePath
*/
const downloadLyric = (downloadInfo, filePath) => {
const promise = getLyric(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise.then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
})
})
promise.then(lrcs => {
if (lrcs.lyric) {
const downloadLyric = function(downloadInfo, isUseOtherSource, filePath, lrcFormat) {
getLyric.call(this, downloadInfo.musicInfo, isUseOtherSource).then(lrcs => {
if (lrcs?.lyric) {
lrcs.lyric = fixKgLyric(lrcs.lyric)
saveLrc(filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'), lrcs.lyric)
saveLrc(filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'), lrcs.lyric, lrcFormat)
}
})
}
@@ -369,9 +377,10 @@ const actions = {
status: state.downloadStatus.WAITING,
statusText: '待下载',
url: null,
fileName: `${rootState.setting.download.fileName
// songmid: musicInfo.songmid,
fileName: filterFileName(`${rootState.setting.download.fileName
.replace('歌名', musicInfo.name)
.replace('歌手', musicInfo.singer)}.${ext}`.replace(filterFileName, ''),
.replace('歌手', musicInfo.singer)}.${ext}`),
progress: {
downloaded: 0,
total: 0,
@@ -434,7 +443,7 @@ const actions = {
dispatch('startTask')
saveMeta.call(_this, downloadInfo, downloadInfo.filePath, rootState.setting.download.isUseOtherSource, rootState.setting.download.isEmbedPic, rootState.setting.download.isEmbedLyric)
if (rootState.setting.download.isDownloadLrc) downloadLyric(downloadInfo, downloadInfo.filePath)
if (rootState.setting.download.isDownloadLrc) downloadLyric.call(_this, downloadInfo, rootState.setting.download.isUseOtherSource, downloadInfo.filePath, rootState.setting.download.lrcFormat)
console.log('on complate')
},
onError(err) {
@@ -667,7 +676,7 @@ const mutations = {
downloadInfo.order = order
},
updateDownloadList(state, list) {
state.list = list
state.list = window.downloadList = list
},
updateUrl(state, { downloadInfo, url }) {
downloadInfo.url = url

View File

@@ -1,10 +1,14 @@
import musicSdk from '../../utils/music'
import { clearLyric, clearMusicUrl } from '../../utils'
import { sync as eventSyncName } from '@renderer/event/names'
let allList = {}
window.allList = allList
const allListInit = (defaultList, loveList, userList) => {
for (const id of Object.keys(allList)) {
delete allList[id]
}
allList[defaultList.id] = defaultList
allList[loveList.id] = loveList
for (const list of userList) allList[list.id] = list
@@ -67,11 +71,40 @@ const mutations = {
if (userList != null) state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
state.isInitedList = true
// if (!isSync) {
// window.eventHub.$emit(eventSyncName.send_action_list, {
// action: 'init_list',
// data: { defaultList, loveList, userList },
// })
// }
},
setList(state, { id, list, name, location, source, sourceListId }) {
setSyncListData(state, { defaultList, loveList, userList }) {
state.defaultList.list.splice(0, state.defaultList.list.length, ...defaultList.list)
state.loveList.list.splice(0, state.loveList.list.length, ...loveList.list)
for (let index = 0; index < userList.length; index++) {
const list = userList[index]
const targetList = allList[list.id]
if (targetList) {
userList.splice(index, 1, targetList)
targetList.name = list.name
targetList.list.splice(0, targetList.list.length, ...list.list)
}
}
console.log()
state.userList = userList
allListInit(state.defaultList, state.loveList, state.userList)
},
setList(state, { id, list, name, location, source, sourceListId, isSync }) {
const targetList = allList[id]
if (targetList) {
if (name && targetList.name === name) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_list',
data: { id, list, name, location, source, sourceListId },
})
}
targetList.list.splice(0, targetList.list.length, ...list)
targetList.location = location
return
@@ -79,6 +112,12 @@ const mutations = {
id += '_' + Math.random()
}
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_list',
data: { id, list, name, location, source, sourceListId },
})
}
let newList = {
name,
id,
@@ -90,46 +129,127 @@ const mutations = {
state.userList.push(newList)
allListUpdate(newList)
},
listAdd(state, { id, musicInfo }) {
listAdd(state, { id, musicInfo, addMusicLocationType, isSync }) {
if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_add',
data: { id, musicInfo, addMusicLocationType },
})
}
const targetList = allList[id]
if (!targetList) return
if (targetList.list.some(s => s.songmid === musicInfo.songmid)) return
targetList.list.push(musicInfo)
switch (addMusicLocationType) {
case 'top':
targetList.list.unshift(musicInfo)
break
case 'bottom':
default:
targetList.list.push(musicInfo)
break
}
},
listMove(state, { fromId, musicInfo, toId }) {
listMove(state, { fromId, musicInfo, toId, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_move',
data: { fromId, musicInfo, toId },
})
}
const fromList = allList[fromId]
const toList = allList[toId]
if (!fromList || !toList) return
fromList.list.splice(fromList.list.indexOf(musicInfo), 1)
fromList.list.splice(fromList.list.findIndex(s => s.songmid === musicInfo.songmid), 1)
let index = toList.list.findIndex(s => s.songmid === musicInfo.songmid)
if (index < 0) toList.list.push(musicInfo)
if (index < 0) {
switch (this.state.setting.list.addMusicLocationType) {
case 'top':
toList.list.unshift(musicInfo)
break
case 'bottom':
default:
toList.list.push(musicInfo)
break
}
}
},
listAddMultiple(state, { id, list }) {
listAddMultiple(state, { id, list, addMusicLocationType, isSync }) {
if (!addMusicLocationType) addMusicLocationType = this.state.setting.list.addMusicLocationType
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_add_multiple',
data: { id, list, addMusicLocationType },
})
}
let targetList = allList[id]
if (!targetList) return
let newList = [...targetList.list, ...list]
let map = {}
let ids = []
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
let newList
const map = {}
const ids = []
switch (addMusicLocationType) {
case 'top':
newList = [...list, ...targetList.list]
for (let i = newList.length - 1; i > -1; i--) {
const item = newList[i]
if (map[item.songmid]) continue
ids.unshift(item.songmid)
map[item.songmid] = item
}
break
case 'bottom':
default:
newList = [...targetList.list, ...list]
for (const item of newList) {
if (map[item.songmid]) continue
ids.push(item.songmid)
map[item.songmid] = item
}
break
}
targetList.list.splice(0, targetList.list.length, ...ids.map(id => map[id]))
},
// { fromId, toId, list }
listMoveMultiple(state, { fromId, toId, list }) {
listMoveMultiple(state, { fromId, toId, list, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_move_multiple',
data: { fromId, toId, list },
})
}
// console.log(state.commit)
this.commit('list/listRemoveMultiple', { id: fromId, list })
this.commit('list/listAddMultiple', { id: toId, list })
this.commit('list/listRemoveMultiple', { listId: fromId, ids: list.map(s => s.songmid), isSync: true })
this.commit('list/listAddMultiple', { id: toId, list, isSync: true })
},
listRemove(state, { id, index }) {
let targetList = allList[id]
listRemove(state, { listId, id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_remove',
data: { listId, id },
})
}
let targetList = allList[listId]
if (!targetList) return
const index = targetList.list.findIndex(item => item.songmid == id)
if (index < 0) return
targetList.list.splice(index, 1)
},
listRemoveMultiple(state, { id, list }) {
let targetList = allList[id]
listRemoveMultiple(state, { listId, ids: musicIds, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_remove_multiple',
data: { listId, ids: musicIds },
})
}
let targetList = allList[listId]
if (!targetList) return
let map = {}
let ids = []
@@ -137,25 +257,50 @@ const mutations = {
ids.push(item.songmid)
map[item.songmid] = item
}
for (const item of list) {
if (map[item.songmid]) delete map[item.songmid]
for (const songmid of musicIds) {
if (map[songmid]) delete map[songmid]
}
let newList = []
for (const id of ids) if (map[id]) newList.push(map[id])
targetList.list.splice(0, targetList.list.length, ...newList)
},
listClear(state, id) {
listClear(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'list_clear',
data: { id },
})
}
let targetList = allList[id]
if (!targetList) return
targetList.list.splice(0, targetList.list.length)
},
updateMusicInfo(state, { id, index, data, musicInfo = {} }) {
let targetList = allList[id]
if (!targetList) return Object.assign(musicInfo, data)
Object.assign(targetList.list[index], data)
updateMusicInfo(state, { listId, id, data, musicInfo, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'update_music_info',
data: { listId, id, data, musicInfo },
})
}
let targetList = allList[listId]
if (!targetList) {
if (musicInfo) Object.assign(musicInfo, data)
return
}
const targetMusicInfo = targetList.list.find(item => item.songmid == id)
if (targetMusicInfo) Object.assign(targetMusicInfo, data)
},
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId }) {
createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, position, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'create_user_list',
data: { name, id, list, source, sourceListId, position },
})
}
let newList = state.userList.find(item => item.id === id)
if (!newList) {
newList = {
@@ -166,38 +311,82 @@ const mutations = {
source,
sourceListId,
}
state.userList.push(newList)
if (position == null) {
state.userList.push(newList)
} else {
state.userList.splice(position + 1, 0, newList)
}
allListUpdate(newList)
}
this.commit('list/listAddMultiple', { id, list })
this.commit('list/listAddMultiple', { id, list, isSync: true })
},
removeUserList(state, index) {
removeUserList(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'remove_user_list',
data: { id },
})
}
const index = state.userList.findIndex(l => l.id === id)
if (index < 0) return
let list = state.userList.splice(index, 1)[0]
allListRemove(list)
},
setUserListName(state, { index, name }) {
let list = state.userList[index]
setUserListName(state, { id, name, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_user_list_name',
data: { id, name },
})
}
let list = allList[id]
if (!list) return
list.name = name
},
moveupUserList(state, index) {
let targetList = state.userList[index]
moveupUserList(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'moveup_user_list',
data: { id },
})
}
const index = state.userList.findIndex(l => l.id == id)
if (index < 0) return
let targetList = allList[id]
state.userList.splice(index, 1)
state.userList.splice(index - 1, 0, targetList)
},
movedownUserList(state, index) {
let targetList = state.userList[index]
movedownUserList(state, { id, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'movedown_user_list',
data: { id },
})
}
const index = state.userList.findIndex(l => l.id == id)
if (index < 0) return
let targetList = allList[id]
state.userList.splice(index, 1)
state.userList.splice(index + 1, 0, targetList)
},
setListScroll(state, { id, location }) {
if (allList[id]) allList[id].location = location
},
sortList(state, { id, sortNum, musicInfos }) {
let targetList = allList[id]
this.commit('list/listRemoveMultiple', { id, list: musicInfos })
setMusicPosition(state, { id, position, list, isSync }) {
if (!isSync) {
window.eventHub.$emit(eventSyncName.send_action_list, {
action: 'set_music_position',
data: { id, position, list },
})
}
targetList.list.splice(sortNum - 1, 0, ...musicInfos)
let targetList = allList[id]
this.commit('list/listRemoveMultiple', { listId: id, ids: list.map(m => m.songmid), isSync: true })
targetList.list.splice(position - 1, 0, ...list)
},
clearCache() {
const lists = Object.values(allList)

View File

@@ -40,7 +40,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
canPlayList.push(item)
// 排除已播放音乐
let index = filteredPlayedList.indexOf(item)
let index = filteredPlayedList.findIndex(m => (m.songmid || m.musicInfo.songmid) == item.musicInfo.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
continue
@@ -52,7 +52,7 @@ const filterList = async({ playedList, listInfo, savePath, commit }) => {
// if (!assertApiSupport(s.source)) return false
canPlayList.push(s)
let index = filteredPlayedList.indexOf(s)
let index = filteredPlayedList.findIndex(m => (m.songmid || m.musicInfo.songmid) == s.songmid)
if (index > -1) {
filteredPlayedList.splice(index, 1)
return false
@@ -141,6 +141,7 @@ const getLyric = function(musicInfo, retryedSource = [], originMusic) {
})
}
let prevListPlayIndex
// getters
const getters = {
list: state => state.listInfo.list,
@@ -152,17 +153,31 @@ const getters = {
const isTempPlay = !!state.playMusicInfo.isTempPlay
const isPlayList = listId === playListId
let playIndex = -1
let listPlayIndex = state.playIndex
let listPlayIndex = Math.min(state.playIndex, state.listInfo.list.length - 1)
if (listId != '__temp__') {
const currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
if (isPlayList) {
playIndex = state.listInfo.list.indexOf(state.playMusicInfo.musicInfo)
playIndex = state.listInfo.list.findIndex(m => (m.songmid || m.musicInfo.songmid) == currentSongmid)
if (!isTempPlay) listPlayIndex = playIndex
} else if (listId == 'download') {
playIndex = window.downloadList.findIndex(m => m.musicInfo.songmid == currentSongmid)
} else {
let list = window.allList[listId]
if (list) playIndex = list.list.indexOf(state.playMusicInfo.musicInfo)
if (list) playIndex = list.list.findIndex(m => m.songmid == currentSongmid)
}
}
if (listPlayIndex >= 0) prevListPlayIndex = listPlayIndex
// if (listPlayIndex < 0) {
// let length = state.listInfo.list.length
// if (length) {
// let index = Math.min(prevListPlayIndex, 0)
// if (index > length - 1) index = length - 1
// listPlayIndex = prevListPlayIndex = index
// }
// } else {
// prevListPlayIndex = listPlayIndex
// }
// console.log({
// listId,
// playIndex,
@@ -170,7 +185,7 @@ const getters = {
// listPlayIndex,
// isPlayList,
// isTempPlay,
// musicInfo: state.playMusicInfo.musicInfo,
// // musicInfo: state.playMusicInfo.musicInfo,
// })
return {
listId,
@@ -247,12 +262,21 @@ const actions = {
async playPrev({ state, rootState, commit, getters }) {
const currentListId = state.listInfo.id
const currentList = state.listInfo.list
const playInfo = getters.playInfo
if (state.playedList.length) {
let currentSongmid
if (state.playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
if (musicInfo) currentSongmid = musicInfo.songmid || musicInfo.musicInfo.songmid
} else {
currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) - 1; index > -1; index--) {
for (index = state.playedList.findIndex(m => (m.musicInfo.songmid || m.musicInfo.musicInfo.songmid) === currentSongmid) - 1; index > -1; index--) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => (m.songmid || m.musicInfo.songmid) === currentSongmid)) {
commit('removePlayedList', index)
continue
}
@@ -272,9 +296,14 @@ const actions = {
commit,
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
let currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
if (currentIndex == -1) currentIndex = 0
let listPlayIndex = playInfo.listPlayIndex
const currentListLength = state.listInfo.list.length - 1
if (listPlayIndex == -1 && currentListLength) {
listPlayIndex = prevListPlayIndex >= currentListLength ? 0 : prevListPlayIndex + 1
}
let currentIndex = listPlayIndex
if (currentIndex < 0) currentIndex = 0
let nextIndex = currentIndex
if (!playInfo.isTempPlay) {
switch (rootState.setting.player.togglePlayMethod) {
@@ -308,12 +337,22 @@ const actions = {
}
const currentListId = state.listInfo.id
const currentList = state.listInfo.list
const playInfo = getters.playInfo
if (state.playedList.length) {
let currentSongmid
if (state.playMusicInfo.isTempPlay) {
const musicInfo = currentList[playInfo.listPlayIndex]
if (musicInfo) currentSongmid = musicInfo.songmid || musicInfo.musicInfo.songmid
} else {
currentSongmid = state.playMusicInfo.musicInfo.songmid || state.playMusicInfo.musicInfo.musicInfo.songmid
}
// 从已播放列表移除播放列表已删除的歌曲
let index
for (index = state.playedList.indexOf(state.playMusicInfo) + 1; index < state.playedList.length; index++) {
for (index = state.playedList.findIndex(m => (m.musicInfo.songmid || m.musicInfo.musicInfo.songmid) === currentSongmid) + 1; index < state.playedList.length; index++) {
const playMusicInfo = state.playedList[index]
if (playMusicInfo.listId == currentListId && !currentList.includes(playMusicInfo.musicInfo)) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
if (playMusicInfo.listId == currentListId && !currentList.some(m => (m.songmid || m.musicInfo.songmid) === currentSongmid)) {
commit('removePlayedList', index)
continue
}
@@ -333,9 +372,14 @@ const actions = {
})
if (!filteredList.length) return commit('setPlayMusicInfo', null)
const playInfo = getters.playInfo
const currentIndex = filteredList.indexOf(currentList[playInfo.listPlayIndex])
let listPlayIndex = playInfo.listPlayIndex
const currentListLength = state.listInfo.list.length - 1
if (listPlayIndex == -1 && currentListLength) {
listPlayIndex = prevListPlayIndex > currentListLength ? currentListLength : prevListPlayIndex - 1
}
const currentIndex = listPlayIndex
let nextIndex = currentIndex
switch (rootState.setting.player.togglePlayMethod) {
case 'listLoop':
nextIndex = currentIndex === filteredList.length - 1 ? 0 : currentIndex + 1
@@ -433,7 +477,10 @@ const mutations = {
playIndex = -1
} else {
let listId = playMusicInfo.listId
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) playIndex = state.listInfo.list.indexOf(playMusicInfo.musicInfo)
if (listId != '__temp__' && !playMusicInfo.isTempPlay && listId === state.listInfo.id) {
const currentSongmid = playMusicInfo.musicInfo.songmid || playMusicInfo.musicInfo.musicInfo.songmid
playIndex = state.listInfo.list.findIndex(m => (m.songmid || m.musicInfo.songmid) == currentSongmid)
}
}
state.playMusicInfo = playMusicInfo

View File

@@ -3,6 +3,8 @@ import path from 'path'
import { shell, clipboard } from 'electron'
import crypto from 'crypto'
import { rendererSend, rendererInvoke, NAMES } from '../../common/ipc'
import iconv from 'iconv-lite'
import { gzip, gunzip } from 'zlib'
/**
* 获取两个数之间的随机整数大于等于min小于max
@@ -80,7 +82,7 @@ const encodeNames = {
'&apos;': "'",
'&#039;': "'",
}
export const decodeName = (str = '') => str.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;)/gm, s => encodeNames[s])
export const decodeName = (str = '') => str?.replace(/(?:&amp;|&lt;|&gt;|&quot;|&apos;|&#039;)/gm, s => encodeNames[s]) || ''
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2
@@ -276,11 +278,22 @@ export const setMeta = (filePath, meta) => {
* 保存歌词文件
* @param {*} filePath
* @param {*} lrc
* @param {*} format
*/
export const saveLrc = (filePath, lrc) => {
fs.writeFile(filePath, lrc, 'utf8', err => {
if (err) console.log(err)
})
export const saveLrc = (filePath, lrc, format) => {
switch (format) {
case 'gbk':
fs.writeFile(filePath, iconv.encode(lrc, 'gbk', { addBOM: true }), err => {
if (err) console.log(err)
})
break
case 'utf8':
default:
fs.writeFile(filePath, lrc, 'utf8', err => {
if (err) console.log(err)
})
break
}
}
/**
@@ -421,3 +434,39 @@ export const setMusicUrl = (musicInfo, type, url) => rendererSend(NAMES.mainWind
url,
})
export const clearMusicUrl = () => rendererSend(NAMES.mainWindow.clear_music_url)
export const gzipData = str => {
return new Promise((resolve, reject) => {
gzip(str, (err, result) => {
if (err) return reject(err)
resolve(result)
})
})
}
export const gunzipData = buf => {
return new Promise((resolve, reject) => {
gunzip(buf, (err, result) => {
if (err) return reject(err)
resolve(result.toString())
})
})
}
export const saveLxConfigFile = async(path, data) => {
if (!path.endsWith('.lxmc')) path += '.lxmc'
fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => {
console.log(err)
})
}
export const readLxConfigFile = async path => {
let isJSON = path.endsWith('.json')
let data = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary')
if (!data || isJSON) return data
data = await gunzipData(Buffer.from(data, 'binary'))
return data.toString('utf8')
}
const fileNameRxp = /[\\/:*?#"<>|]/g
export const filterFileName = name => name.replace(fileNameRxp, '')

View File

@@ -120,6 +120,7 @@ module.exports = class Lyric {
return {
text: line.text,
time: line.time,
translation: line.translation,
dom_line: fontPlayer.lineContent,
}
})

View File

@@ -48,7 +48,7 @@ module.exports = class LinePlayer {
_initLines() {
this.lines = []
this.translationLines = []
const lines = this.lyric.split('\n')
const lines = this.lyric.split(/\r\n|\r|\n/)
const linesMap = {}
// const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < lines.length; i++) {

View File

@@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
getMusicUrl(songInfo, type) {
@@ -8,6 +9,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@@ -1,6 +1,6 @@
import { httpFetch } from '../../request'
// import { formatPlayTime } from '../../index'
// import jshtmlencode from 'js-htmlencode'
const boardList = [
// { id: 'bd__601', name: '歌单榜', bangid: '601' },

View File

@@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { formatPlayTime } from '../../index'
// import { debug } from '../../utils/env'

View File

@@ -1,6 +1,7 @@
import { httpFetch } from '../../request'
import { requestMsg } from '../../message'
import { headers, timeout } from '../options'
import { dnsLookup } from '../utils'
const api_test = {
getMusicUrl(songInfo, type) {
@@ -8,6 +9,7 @@ const api_test = {
method: 'get',
timeout,
headers,
lookup: dnsLookup,
family: 4,
})
requestObj.promise = requestObj.promise.then(({ body }) => {

View File

@@ -63,17 +63,19 @@ export default {
reply: [],
}
return item.pcontent ? {
id: item.id,
text: decodeName(item.pcontent).split('\n'),
time: null,
userName: item.puser,
avatar: null,
userId: item.puser_id,
likedCount: null,
replyNum: null,
reply: [data],
} : data
return item.pcontent
? {
id: item.id,
text: decodeName(item.pcontent).split('\n'),
time: null,
userName: item.puser,
avatar: null,
userId: item.puser_id,
likedCount: null,
replyNum: null,
reply: [data],
}
: data
})
},
}

View File

@@ -1,5 +1,5 @@
// import '../../polyfill/array.find'
// import jshtmlencode from 'js-htmlencode'
import { httpFetch } from '../../request'
import { decodeName, formatPlayTime, sizeFormate } from '../../index'
// import { debug } from '../../utils/env'

View File

@@ -165,7 +165,9 @@ export default {
: result.body.err_code
) !== 0)
) return this.createHttp(url, options, ++retryNum)
return result.body.data || result.body.info
if (result.body.data) return result.body.data
if (Array.isArray(result.body.info)) return result.body
return result.body.info
},
createTask(hashs) {
@@ -183,9 +185,9 @@ export default {
let list = hashs
let tasks = []
while (list.length) {
tasks.push(Object.assign({ data: list.slice(0, 20) }, data))
if (list.length < 20) break
list = list.slice(20)
tasks.push(Object.assign({ data: list.slice(0, 100) }, data))
if (list.length < 100) break
list = list.slice(100)
}
let url = 'http://kmr.service.kugou.com/v2/album_audio/audio'
return tasks.map(task => this.createHttp(url, {
@@ -267,6 +269,68 @@ export default {
}
},
deDuplication(datas) {
let ids = new Set()
return datas.filter(({ hash }) => {
if (ids.has(hash)) return false
ids.add(hash)
return true
})
},
async getUserListDetailByLink({ info }, link) {
let listInfo = info['0']
let total = listInfo.count
let tasks = []
let page = 0
while (total) {
const limit = total > 90 ? 90 : total
total -= limit
page += 1
tasks.push(this.createHttp(link.replace(/pagesize=\d+/, 'pagesize=' + limit).replace(/page=\d+/, 'page=' + page), {
headers: {
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1',
Referer: link,
},
}).then(data => data.list.info))
}
let result = await Promise.all(tasks).then(([...datas]) => datas.flat())
result = await Promise.all(this.createTask(this.deDuplication(result).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(result)
return {
list: this.filterData2(result) || [],
page,
limit: this.listDetailLimit,
total: listInfo.count,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.list_create_username,
// play_count: this.formatPlayCount(listInfo.count),
},
}
},
createGetListDetail2Task(id, total) {
let tasks = []
let page = 0
while (total) {
const limit = total > 300 ? 300 : total
total -= limit
page += 1
tasks.push(this.createHttp('https://mobiles.kugou.com/api/v5/special/song_v2?appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&page=' + page + '&pagesize=' + limit + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-&signature=' + toMD5('NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtappid=1058clienttime=1586163263991clientver=20000dfid=-global_specialid=' + id + 'mid=1586163263991page=' + page + 'pagesize=' + limit + 'plat=0specialid=0srcappid=2919uuid=1586163263991version=8000NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'), {
headers: {
mid: '1586163263991',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163263991',
},
}).then(data => data.info))
}
return Promise.all(tasks).then(([...datas]) => datas.flat())
},
async getUserListDetail2(global_collection_id) {
let id = global_collection_id
if (id.length > 1000) throw new Error('get list error')
@@ -279,22 +343,14 @@ export default {
clienttime: '1586163242519',
},
})
let songInfo = await this.createHttp('https://mobiles.kugou.com/api/v5/special/song_v2?appid=1058&global_specialid=' + id + '&specialid=0&plat=0&version=8000&pagesize=' + info.songcount + '&srcappid=2919&clientver=20000&clienttime=1586163263991&mid=1586163263991&uuid=1586163263991&dfid=-&signature=' + toMD5('NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtappid=1058clienttime=1586163263991clientver=20000dfid=-global_specialid=' + id + 'mid=1586163263991pagesize=' + info.songcount + 'plat=0specialid=0srcappid=2919uuid=1586163263991version=8000NVPh5oo715z5DIWAeQlhMDsWXXQV4hwt'), {
headers: {
mid: '1586163263991',
Referer: 'https://m3ws.kugou.com/share/index.php',
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',
dfid: '-',
clienttime: '1586163263991',
},
})
let result = await Promise.all(this.createTask(songInfo.info.map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
const songInfo = await this.createGetListDetail2Task(id, info.songcount)
let result = await Promise.all(this.createTask(this.deDuplication(songInfo).map(item => ({ hash: item.hash })))).then(([...datas]) => datas.flat())
// console.log(info, songInfo)
return {
list: this.filterData2(result) || [],
page: 1,
limit: songInfo.total,
total: songInfo.total,
limit: this.listDetailLimit,
total: info.songcount,
source: 'kg',
info: {
name: info.specialname,
@@ -333,10 +389,9 @@ export default {
// console.log(body, location)
if (statusCode > 400) return this.getUserListDetail(link, page, ++retryNum)
if (location) {
// console.log(location)
if (location.includes('global_collection_id')) return this.getUserListDetail2(location.replace(/^.*?global_collection_id=(\w+)(?:&.*$|#.*$|$)/, '$1'))
if (location.includes('chain=')) return this.getUserListDetail3(location.replace(/^.*?chain=(\w+)(?:&.*$|#.*$|$)/, '$1'), page)
// console.log('location', location)
if (location.includes('.html')) {
if (location.includes('zlist.html')) {
let link = location.replace(/^(.*)zlist\.html/, 'https://m3ws.kugou.com/zlist/list')
@@ -348,27 +403,12 @@ export default {
return this.getUserListDetail(link, page, ++retryNum)
} else return this.getUserListDetail3(location.replace(/.+\/(\w+).html(?:\?.*|&.*$|#.*$|$)/, '$1'), page)
}
// console.log('location', location)
return this.getUserListDetail(link, page, ++retryNum)
}
if (typeof body == 'string') return this.getUserListDetail2(body.replace(/^[\s\S]+?"global_collection_id":"(\w+)"[\s\S]+?$/, '$1'))
if (body.errcode !== 0) return this.getUserListDetail(link, page, ++retryNum)
let listInfo = body.info['0']
let result = body.list.info.map(item => ({ hash: item.hash }))
result = await Promise.all(this.createTask(result)).then(([...datas]) => datas.flat())
return {
list: this.filterData2(result) || [],
page,
limit: this.listDetailLimit,
total: listInfo.count,
source: 'kg',
info: {
name: listInfo.name,
img: listInfo.pic && listInfo.pic.replace('{size}', 240),
// desc: body.result.info.list_desc,
author: listInfo.list_create_username,
// play_count: this.formatPlayCount(listInfo.count),
},
}
return this.getUserListDetailByLink(body, link)
},
getListDetail(id, page, tryNum = 0) { // 获取歌曲列表内的音乐

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