Compare commits

...

195 Commits

Author SHA1 Message Date
底层用户
7de1355f18 Merge pull request #573 from MoYingJi/docs-it
docs: 小改 Issue Template
2025-11-17 14:19:59 +08:00
MoYingJi
0d66ced637 docs: 小改 Issue Template 2025-11-16 00:29:09 +08:00
底层用户
ae1ee71e75 Merge pull request #567 from MoYingJi/fix-ldc
fix(player): 修复极端状态下播放异常
2025-11-14 09:29:01 +08:00
底层用户
84d9c999eb Merge pull request #566 from MoYingJi/docs-it
docs: 更新 Issue Template
2025-11-14 09:28:14 +08:00
MoYingJi
1a36fbf1d5 fix(player): 修复极端状态下播放异常
由于 `jumpSeek` 现在会减去 `offset`,可能会导致当 `offset` 超出预期时引发一些奇怪的行为
2025-11-14 03:06:29 +08:00
MoYingJi
e0e62cd906 docs: 更新 Issue Template 2025-11-14 01:14:46 +08:00
底层用户
2ed6acf934 Merge pull request #563 from MoYingJi/fix-ldc
fix(Lyric): 点击歌词跳转进度未应用偏移
2025-11-14 00:37:56 +08:00
MoYingJi
236ee0a345 style: 补全行尾分号 统一代码风格 2025-11-14 00:06:19 +08:00
MoYingJi
fe0f7a0f25 fix(Lyric): 点击歌词跳转进度未应用偏移 2025-11-13 20:16:55 +08:00
底层用户
8529663ea5 Merge pull request #559 from MoYingJi/feat-lr
feat(lyric): 支持解析本地 LRC 歌词中的音译
2025-11-12 22:29:16 +08:00
MoYingJi
100bae7488 feat(lyric): 支持解析本地 LRC 歌词中的音译
根据已解析歌词中是否有时间相同来判断,因此最先遍历的歌词行会被作为主歌词

若具有翻译或音译,则判断主歌词中是否有翻译,若没有则将此句作为翻译,音译同理

如果出现时间相同的歌词行,第一行会被作为主歌词,第二行翻译,第三行音译,其余舍去
2025-11-12 21:56:04 +08:00
imsyy
91927b8d76 🎈 perf: 优化桌面歌词 hover 效果,避免长时间不消失 2025-11-12 10:47:32 +08:00
imsyy
8be209837d 🐞 fix: 修复窗口关闭问题 #547 2025-11-12 10:28:07 +08:00
底层用户
9ae09b5711 Merge pull request #556 from MoYingJi/fix
fix(lyric): 关闭在线 TTML 歌词后不获取本地 TTML 歌词了
2025-11-12 10:23:57 +08:00
MoYingJi
3509d73ecc chore(lyric): 修正注释 2025-11-12 10:18:04 +08:00
MoYingJi
0bdb9c07b7 chore(lyric): 添加注释说明 TTML 在线获取逻辑
因为好像确实不太好看懂哈,我的代码有点抽象?
2025-11-12 10:16:38 +08:00
MoYingJi
3218ab05f1 fix(lyric): 关闭在线 TTML 歌词后不获取本地 TTML 歌词了 2025-11-12 10:05:10 +08:00
底层用户
e40343f91f Merge pull request #554 from MoYingJi/feat-ll
feat(LocalLyric): 使本地覆盖在线歌词的文件更好管理
2025-11-12 09:47:54 +08:00
MoYingJi
fcf1c235c3 fix: 现在更改本地歌词目录时也会检测是否为子目录 2025-11-12 05:42:51 +08:00
MoYingJi
ef0433c645 feat(LocalLyric): 使本地覆盖在线歌词的文件更好管理
使「本地歌词覆盖在线歌词」功能中的用户存放的文件更好管理

## 改动
 - 支持将歌词命名为 `任意前缀.歌曲ID.后缀名` 的格式
 - 支持将歌词文件放在子文件夹

## 代码改动
 - 将 `ipc-file` 中的 `read-local-lyric` 里写死的路径改成了 FastGlob 查找,因此支持了上述的两项功能
 - 更改 `LocalSettings.vue` 中的信息
2025-11-12 02:47:10 +08:00
底层用户
207f84fcd7 Merge pull request #550 from MoYingJi/feat-settings
refactor(LyricsSetting): 增加歌词内容分组
2025-11-11 22:40:36 +08:00
底层用户
2985aa5977 Merge pull request #549 from MoYingJi/feat-unlock
refactor(unlock): 重写部分 getUnlockSongUrl
2025-11-11 22:39:24 +08:00
底层用户
ffd7aeff49 Merge pull request #548 from MoYingJi/fix-ttml
fix(ttml): 行结束时间过早
2025-11-11 22:37:57 +08:00
MoYingJi
7f4a88daa1 refactor(LyricsSetting): 增加歌词内容分组 2025-11-11 19:26:33 +08:00
MoYingJi
91171465b2 refactor(unlock): 重写部分 getUnlockSongUrl
只是重写,没有改变逻辑,没有改变源和优先级,但使源更容易更改
2025-11-11 19:11:27 +08:00
MoYingJi
1744a5a678 fix(ttml): 行结束时间过早
TTML 有时会为了触发一些效果(比如和下一行连起来)而故意延后行的结束时间(同时词的结束时间不变),`parseTTMLToAMLL` 直接忽略了行的开始结束时间而采用词的开始结束时间
2025-11-11 19:04:30 +08:00
imsyy
ce3d469547 🐞 fix: 桌面歌词无法拖动 #545 2025-11-11 18:16:46 +08:00
imsyy
7cc5b85fc5 🐞 fix: 托盘右键无法正常关闭软件 #546 2025-11-11 14:46:53 +08:00
imsyy
3b3f321a3d 🐞 fix: 修复窗口 IPC 报错 & 歌单去重 2025-11-10 23:43:13 +08:00
imsyy
58e3c6e21c 🐞 fix: 修复本地歌词覆盖失效 2025-11-10 17:07:43 +08:00
imsyy
68756f2502 🐞 fix: 修复进度条点击及拖拽问题 #535 2025-11-10 14:37:52 +08:00
imsyy
cea9f7b025 🐞 fix: 修复TTML到LRC的回退逻辑 2025-11-10 02:00:33 +08:00
imsyy
6b653bc5e8 🌈 style: 优化歌词与桌面歌词样式问题 2025-11-10 00:36:18 +08:00
imsyy
1b2985892b 🐞 fix: 修复退出应用时进程异常阻断 2025-11-09 01:57:48 +08:00
imsyy
eb0094c189 feat: 优化列表播放方式 & 添加一些桌面歌词配置 #536 2025-11-09 00:39:55 +08:00
imsyy
5edbd66398 feat: 完善本地歌词逐字效果 & 修复锁定后无法点击 2025-11-08 21:52:22 +08:00
底层用户
4e731e976c Merge pull request #539 from MoYingJi/fix
fix: 本地音乐内嵌歌词加载状态未恢复
2025-11-08 21:40:51 +08:00
MoYingJi
bd6e23435e fix: 本地音乐内嵌歌词加载状态未恢复 2025-11-08 19:29:25 +08:00
imsyy
8ec91e1392 feat: 添加桌面歌词锁定 2025-11-07 17:38:49 +08:00
imsyy
8f69b56378 🐞 fix: 尝试修复缩放问题 2025-11-07 12:34:32 +08:00
imsyy
ccd0c6bdeb 🐞 fix: 修复系统开启缩放时,拖拽桌面歌词导致宽高异常 2025-11-07 01:06:36 +08:00
imsyy
1c9109af73 🐞 fix: 修复桌面歌词字体调节 2025-11-06 15:04:40 +08:00
imsyy
a556a2e102 🌈 style: 优化桌面歌词样式 2025-11-06 01:07:04 +08:00
imsyy
9fcd5b0e98 feat: 完善歌词窗口 IPC 2025-11-05 18:21:17 +08:00
imsyy
a1be1e16b2 feat: 更换解灰音源 2025-11-04 17:49:38 +08:00
imsyy
fc7fc08a6e 🌈 style: 优化评论展示样式 2025-11-04 16:30:52 +08:00
imsyy
d74515142d 🎈 perf: 优化歌词并发限制 2025-11-04 14:28:43 +08:00
imsyy
0aae10e8a0 🐞 fix: 修复歌词加载过慢仍旧展示上一首 #532 2025-11-04 12:01:27 +08:00
imsyy
242c6f2ca7 🎈 perf: 优化相关引用 2025-11-03 23:25:22 +08:00
imsyy
413b74bf9a 🎈 perf: 优化播放器 2025-11-03 17:14:52 +08:00
imsyy
aad5c9461f Merge branch 'dev' of https://github.com/imsyy/SPlayer into dev 2025-11-03 09:24:10 +08:00
imsyy
23cccbb660 🐞 fix: 修复单曲循环模式加载状态异常 #531 2025-11-03 09:22:37 +08:00
底层用户
ba7666526c Merge pull request #525 from MoYingJi/feat
fix: dev.mjs linux/macOS 设置 UTF-8
2025-11-01 20:10:08 +08:00
底层用户
82ea11b6fa Merge pull request #526 from MoYingJi/fix
fix(AMLyric): 有时处理歌词语言不正常工作
2025-11-01 20:09:45 +08:00
MoYingJi
9771cea25f optimize(AMLyric): 组件被挂载时重复处理歌词
因为已经侦听了 `lyricPlayerRef`,所以 `onMounted` 中处理歌词是不必要的
2025-11-01 17:56:29 +08:00
MoYingJi
6f4afd85d7 fix(AMLyric): 有时处理歌词语言不正常工作 2025-11-01 01:54:05 +08:00
MoYingJi
fe82fa623b fix: dev.mjs linux/macOS 设置 UTF-8 2025-10-31 21:03:33 +08:00
imsyy
28b521a192 🎈 perf: 完善部分桌面歌词 2025-10-31 00:55:47 +08:00
imsyy
bdfbb4d2b1 🐞 fix: 修复网页端获取 ttml 2025-10-30 14:04:08 +08:00
imsyy
6937a93d17 feat: 完善 store 通信 2025-10-29 23:59:13 +08:00
imsyy
2bc4fca4dd 🐞 fix: 修复 ttml 返回 404 2025-10-29 22:35:51 +08:00
imsyy
92428593e2 🐞 fix: 修复参数错误 2025-10-29 20:42:20 +08:00
imsyy
95e3f74301 🔧 build: 添加所需文件 2025-10-29 18:34:48 +08:00
imsyy
9145eb034b 🌈 style: 优化部分样式 2025-10-29 18:29:58 +08:00
底层用户
829dc3b591 🐞 fix: 修复路由重定向 2025-10-28 17:23:15 +08:00
底层用户
c4c6178b9e 🌈 style: 优化评论区效果 2025-10-28 14:51:58 +08:00
imsyy
9967bcb102 🌈 style: 优化播放样式 2025-10-28 00:33:40 +08:00
底层用户
feb186db65 🌈 style: 优化播放器切歌效果 2025-10-27 18:21:05 +08:00
底层用户
84999338e6 🎈 perf: 优化歌词组件样式性能 2025-10-27 15:15:54 +08:00
imsyy
7943b56dbb 🐞 fix: 修复工具栏图标 & 添加调试工具 2025-10-27 00:46:02 +08:00
imsyy
6a1657cf20 🦄 refactor: 主进程重构
修复导航栏不及时响应窗口状态
修复 thumb 展示异常
2025-10-25 23:43:54 +08:00
底层用户
0eaf32b8a7 Merge pull request #517 from MoYingJi/feat
feat: 支持收藏专辑
2025-10-25 22:17:04 +08:00
MoYingJi
424c1e76f0 优化格式 去掉一层嵌套 2025-10-25 03:36:08 +08:00
MoYingJi
e854dbb816 fix: 未使用的 import 和注释错误 2025-10-25 03:33:12 +08:00
MoYingJi
5e37678348 refactor(auth): 抽离收藏/订阅逻辑到通用函数
`toLikeSomething` 的 `request` 参数必须套一层 getter 才行,不然报错在定义前使用,不是很懂 TypeScript 🤔
2025-10-25 03:28:37 +08:00
MoYingJi
9fcb5ca633 feat: 支持收藏专辑 2025-10-25 03:06:42 +08:00
底层用户
55aedc33c3 Merge pull request #516 from MoeFurina/dev
ci(vercel): 添加vercel配置文件, 方便下次部署就不需要在outputdirectory输入"out/renderer"了
2025-10-24 23:09:59 +08:00
底层用户
1d4aa43185 Merge pull request #514 from MoYingJi/feat
feat(lyric): 优化歌词排除
2025-10-24 23:08:40 +08:00
ElyPrism
8014eedd72 ci(vercel): 添加vercel配置文件, 方便下次部署就不需要在outputdirectory输入"out/renderer"了 2025-10-24 22:43:15 +08:00
MoYingJi
4f018775ba fix(lyric): 一些错误 2025-10-24 22:16:05 +08:00
MoYingJi
4e7bd95366 feat(lyric): 本地歌词排除设置项 2025-10-24 22:06:57 +08:00
MoYingJi
3035056ded 优化代码 2025-10-24 21:13:55 +08:00
MoYingJi
df42292f0a fix: 不排除 TTML 歌词 设置不生效 2025-10-24 20:34:52 +08:00
MoYingJi
ca2ff2a890 feat(lyric): 设置项「TTML 歌词排除」 2025-10-24 20:01:36 +08:00
MoYingJi
bd06c3f8f4 fix(storage): 原来不能直接存正则 2025-10-24 17:43:59 +08:00
MoYingJi
a44cd904dc feat(lyric): 改个名 ExcludeKeywords -> ExcludeLyrics 2025-10-24 17:27:32 +08:00
MoYingJi
09838e632d feat(lyric): 修改歌词排除配置页面 2025-10-24 17:11:15 +08:00
MoYingJi
7f77ccb18a feat(lyric): 换用歌词排除函数 2025-10-24 16:40:01 +08:00
MoYingJi
f3f05aa72b feat(lyric): 歌词行正则排除 基本设置 2025-10-24 16:34:49 +08:00
底层用户
6beb9c78e1 feat: 新增 每日推荐 - 不感兴趣 2025-10-24 15:23:51 +08:00
底层用户
2efc0a5228 🐞 fix: 歌词排除内容不生效 #509 2025-10-23 16:23:35 +08:00
底层用户
bb0cae2be1 🐞 fix: 修复 单曲循环索引问题 #510 2025-10-23 15:50:41 +08:00
底层用户
22a6fdd0c4 Merge pull request #507 from MoYingJi/feat
feat: 优化启用 TTML 选项的逻辑
2025-10-23 09:12:23 +08:00
MoYingJi
d257a441c2 feat: 优化启用 TTML 选项的逻辑
原先的逻辑不太统一,一边受选项控制,一边不受选项控制

现在将所有的本地 TTML 加载都改为不受选项控制,只要本地文件存在则始终启用,且将设置项「启用 TTML 歌词」更名为「启用在线 TTML 歌词」
2025-10-22 23:10:36 +08:00
底层用户
49c81b6afd feat: 调整路由结构
- 修复网页端报错 #505
2025-10-22 17:57:48 +08:00
底层用户
045497a7c8 🌈 style: 优化一些样式 2025-10-22 15:15:19 +08:00
底层用户
521970b8a1 Merge pull request #503 from MoYingJi/feat
fix: 更改函数名后构建失败
2025-10-22 14:56:01 +08:00
MoYingJi
a1e9dd7746 fix: 更改函数名后构建失败
千万不要随便改函数名,因为 IDE 可能根本搜不到调用的地方...
2025-10-22 14:47:50 +08:00
底层用户
61d2467916 Merge pull request #502 from MoYingJi/feat
feat: 对本地歌词的修改
2025-10-22 14:33:05 +08:00
底层用户
f3adf773a7 Merge pull request #501 from MoYingJi/fix
fix: 恢复窗口最大化状态时过早显示窗口
2025-10-22 14:17:20 +08:00
MoYingJi
ad807cf478 chore: 规范代码 添加分号 2025-10-22 13:59:56 +08:00
MoYingJi
a1194232af log 怎么又忘删了 2025-10-22 13:48:25 +08:00
MoYingJi
9fc1746495 feat: 支持从本地读取歌词覆盖线上歌词 2025-10-22 13:33:33 +08:00
MoYingJi
103bf7948d 本地音乐歌词的修改
- 优先使用外部歌词
- 支持本地 TMLL 歌词
2025-10-22 11:58:01 +08:00
MoYingJi
12c2dea226 fix: 恢复窗口最大化状态时过早显示窗口
在恢复窗口的最大化状态时,窗口还未加载完成就调用了 `maximize` 导致窗口过早显示
2025-10-22 11:16:56 +08:00
底层用户
7fbe8ac6a5 Merge pull request #500 from MoYingJi/fix
fix: dev.mjs linux/mac 无法启动开发环境
2025-10-22 09:40:48 +08:00
MoYingJi
ef6868fd40 fix: dev.mjs linux/mac 无法启动开发环境
在 isWindows 分支中,startElectronVite 函数正常启动
在 else 分支中,startElectronVite 函数在定义之前就被调用了
2025-10-22 02:41:33 +08:00
imsyy
f238845f9b 🐞 fix: 调整均衡器 2025-10-22 00:49:31 +08:00
底层用户
d4d16b71ae feat: 新增 均衡器 2025-10-21 18:15:51 +08:00
底层用户
9baf571478 🐞 fix: 修复文件导入 2025-10-21 16:42:15 +08:00
底层用户
6370ac77e6 feat: 优化歌词偏移管理 2025-10-21 16:30:34 +08:00
底层用户
2bf3f2a5c6 🗑️ refactor: 移除不再使用的组件声明文件,更新类型配置 2025-10-21 14:08:24 +08:00
底层用户
e8bf42891e 🐞 fix: 修复大小写 #498 2025-10-21 13:50:56 +08:00
imsyy
c21f970b86 feat: 新增 定时关闭 & 播放速度 2025-10-21 00:12:48 +08:00
底层用户
56045cd338 完善功能 2025-10-20 18:22:40 +08:00
imsyy
dbf6121098 feat: 优化与新增播放器控制组件 2025-10-20 03:44:17 +08:00
imsyy
103b2fe923 feat: 优化播放器布局,修复缺失组件 2025-10-19 02:56:31 +08:00
imsyy
c70a266b22 🐞 fix: 修复本地歌词及元信息错误 2025-10-19 01:36:13 +08:00
底层用户
a7b34ca4b0 Merge pull request #497 from xiaoQQya/unblock-source
修改默认 unblock source 为 kugou bodian pyncmd
2025-10-18 23:33:07 +08:00
底层用户
caa6fa838f Merge pull request #496 from xiaoQQya/dev
修复 UnblockNeteaseMusic 返回非 https 协议链接时反代失败的问题
2025-10-18 23:32:29 +08:00
底层用户
e98140d0ac Merge pull request #495 from MoYingJi/feat-window
feat: 记忆窗口最大化状态
2025-10-18 23:32:11 +08:00
底层用户
ac47b38773 Merge pull request #494 from MoYingJi/feat
fix: AM 歌词翻译错位
2025-10-18 23:30:14 +08:00
xiaoQQya
fef6e4f6fa chore: 修改默认 unblock source 为 kugou bodian pyncmd 2025-10-18 19:56:20 +08:00
xiaoQQya
0215e5944d fix: 修复 UnblockNeteaseMusic 返回非 https 协议链接时反代失败的问题 2025-10-18 19:52:45 +08:00
MoYingJi
1b6ad163e3 log 忘删了 2025-10-18 19:35:26 +08:00
MoYingJi
080de35545 fix(window): Linux 兼容性 2025-10-18 19:22:17 +08:00
MoYingJi
163423222d 移动根据记忆将窗口最大化的逻辑 2025-10-18 19:06:52 +08:00
imsyy
fc49b7ad00 🐳 chore: 优化构建流程 2025-10-18 18:30:15 +08:00
MoYingJi
b83cd99d8c feat: 记录窗口最大化状态 2025-10-18 18:11:34 +08:00
MoYingJi
74a5de96c0 fix: AM 歌词翻译错位
解析 lrc 或 yrc 时,当歌词中有部分歌词行没有翻译时,原先 `parseAMData` 通过 `index` 获取翻译的方法会使得歌词翻译错位

我仿照 `alignLyrics` 写了 `alignAMLyrics`,并修改 `parseAMData` 使其调用此方法对齐
2025-10-18 17:15:18 +08:00
底层用户
fcf78cdd08 🦄 refactor: electron-builder to ts 2025-10-17 17:26:48 +08:00
底层用户
98fbc81d2f ci(workflow): 更新发布工作流配置
移除冗余的文件上传配置并设置矩阵策略不快速失败
2025-10-17 13:55:44 +08:00
底层用户
5ad562ab1c 🐳 chore: 更新构建配置和工作流 2025-10-17 12:38:14 +08:00
底层用户
10dc011f9a 🐳 chore: 更新构建配置和工作流 2025-10-17 12:03:38 +08:00
底层用户
3df6e91f95 🐳 chore: 更新构建配置和工作流 2025-10-17 11:50:18 +08:00
底层用户
31ae15d242 🐳 chore: 移除不必要的环境变量 2025-10-17 11:29:34 +08:00
底层用户
a2cfbb5e52 🐞 fix: 修复构建 pnpm 失败 2025-10-17 11:18:33 +08:00
底层用户
c4bd94daae 🐳 chore: 测试构建 2025-10-17 10:25:49 +08:00
imsyy
e18828cd08 🔧 build: 更新构建内容 2025-10-17 02:47:02 +08:00
imsyy
82f6b4607b 🐳 chore: 修复工作流格式 2025-10-17 01:13:55 +08:00
imsyy
3874ab3483 🐳 chore: 更新工作流 2025-10-17 01:09:01 +08:00
imsyy
fc297cb198 🐳 chore: 测试自动发版 2025-10-17 00:33:46 +08:00
imsyy
aec06c5a55 🐳 chore: 修复构建 2025-10-17 00:11:01 +08:00
imsyy
87ce076f26 🐳 chore: 修复构建打包 2025-10-16 23:09:23 +08:00
底层用户
45b1bf130d feat: 去除报错弹窗 2025-10-16 18:29:27 +08:00
底层用户
3966578015 🐞 fix: 修复 TTML 缺失翻译及音译 2025-10-16 17:50:14 +08:00
底层用户
fed7b3678b feat: 默认歌词支持 TTML 2025-10-16 16:35:37 +08:00
底层用户
ac6ce257b8 Merge pull request #491 from awsl1414/feat/ci-multi-platform
🔧 build: 优化 CI/CD 构建流程,增加多架构支持
2025-10-16 10:30:59 +08:00
awsl1414
b8b8f747d3 🔧 build: 全平台采用原生架构 runner 替代交叉编译
- feat: Windows ARM64 使用 windows-11-arm 原生 runner
- feat: macOS 拆分为独立的 x64 和 ARM64 构建任务
- feat: macOS x64 使用 macos-13 原生 Intel runner
- feat: macOS ARM64 使用 macos-15 原生 Apple Silicon runner
- feat: Linux ARM64 使用 ubuntu-22.04-arm 原生 runner
- fix: 修复 Windows ARM64 交叉编译生成错误 I386 指令集问题
- improve: 移除 QEMU 和交叉编译工具链依赖
- improve: 简化构建配置,提升构建可靠性和性能
- improve: 确保所有平台生成正确架构的可执行文件
2025-10-15 20:16:56 +08:00
awsl1414
53468b2e3a 🔧 build: 优化 CI/CD 构建流程,增加多架构支持
- feat: 为 Windows 构建添加 x64 和 ARM64 分离任务
- feat: 为 Linux 构建添加 ARM64 架构支持
- feat: 优化构建产物命名,包含架构标识
- chore: 更新构建相关依赖版本
- improve: Linux ARM64 使用 QEMU 交叉编译
- improve: 优化工作流任务命名和注释
2025-10-15 04:24:09 +08:00
底层用户
ecadf6ade7 🔧 build: 更新依赖 2025-10-13 09:43:34 +08:00
底层用户
8187b09fcb Merge pull request #484 from MoYingJi/feat
feat: 支持为日语单独设置字体
2025-10-13 09:06:02 +08:00
MoYingJi
89117d4198 规范代码
- 添加了一些注释
- 补全行尾分号 以符合总体代码风格
2025-10-11 01:21:19 +08:00
MoYingJi
6158dd2750 Merge branch 'imsyy:dev' into feat 2025-10-10 00:16:46 +08:00
MoYingJi
86f33d054a 日语单独字体 AM 支持 2025-10-10 00:10:21 +08:00
MoYingJi
52e8458590 fix 2025-10-09 23:13:39 +08:00
MoYingJi
554cf45500 fix 2025-10-09 23:11:50 +08:00
MoYingJi
60f751713a 日文字形设置和 MainLyric 的基础支持 2025-10-09 23:07:57 +08:00
底层用户
edd9b38cfc 🐞 fix: 修复构建失败 2025-10-09 11:43:05 +08:00
底层用户
9a87d73289 Merge pull request #479 from q1zhen/dev
fix: adjust minimum window size 调整最小窗口大小
2025-10-09 10:51:06 +08:00
底层用户
317763e2c3 Merge pull request #481 from MoeFurina/dev
feat(LyricWithTTMLFormat): 支持从steveXMH仓库获取TTML歌词
2025-10-09 10:50:08 +08:00
ImFurina
aa3f9d2ca8 feat(LyricWithTTMLFormat): 支持从steveXMH仓库获取TTML歌词 2025-10-02 12:25:04 +08:00
Yang Chyi-Jen
e64a5ba1bc fix: adjust minimum window size 2025-10-02 11:00:05 +08:00
底层用户
00e6f7bb60 Merge pull request #472 from MoeFurina/dev
fix(getOnlineUrl): 修复网页端登录后获取url的CORS问题
2025-09-25 13:35:15 +08:00
ImFurina
883ef05ab4 fix(getOnlineUrl): 修复网页端登录后获取url的CORS问题 2025-09-24 15:32:30 +08:00
底层用户
d0b5eb3371 Merge pull request #452 from serious-snow/dev
fix: 修复 macos 和linux 本地歌曲路径问题
2025-09-23 18:13:26 +08:00
底层用户
590ef96aa7 Merge pull request #469 from Pissofdvpe/dev-fix
fix:修复歌词显示相关
2025-09-23 18:10:16 +08:00
Pissofdvpe
172ca5a2f3 Update lyric.html
feat:优化调整桌面歌词字体大小设置
2025-09-21 19:26:22 +08:00
Pissofdvpe
6e1e56c1bd Update lyric.html
feat:优化桌面歌词字体调整
2025-09-21 19:18:46 +08:00
Pissofdvpe
105fed4bd0 Update LyricsSetting.vue
feat:优化调整歌词大小设置
2025-09-21 00:38:10 +08:00
Pissofdvpe
8bd3dc56d8 Update MainLyric.vue
fix:修复播放页面歌词设置在居中和居右状态下不能自动换行
2025-09-21 00:32:02 +08:00
底层用户
8e88bf64b1 🐞 fix: 修复类型错误 2025-09-15 12:06:50 +08:00
底层用户
244a832c52 🔧 build: 更新依赖 2025-09-15 11:45:02 +08:00
底层用户
146af3aeba 切换上游 api 库 #459 2025-09-15 11:22:55 +08:00
wangjian
143e8e29d7 fix: 修复 编辑歌曲信息弹窗无法复制(路径和md5) 2025-08-21 13:54:30 +08:00
wangjian
c702e6e01a fix: 修复 macos 和linux 本地歌曲路径问题
- 在 macos 和 linux 读取本地文件需要添加 file:// 前缀
- windows 也支持 file:// 前缀
- 在创建播放器时,为本地歌曲路径添加 file:// 前缀
- 在修改歌曲封面时,为本地路径添加 file:// 前缀
- 在保存元数据时,移除 file:// 前缀以保持兼容性
2025-08-21 09:37:25 +08:00
底层用户
b2ddb9f4e2 feat: 优化随机播放问题 & 播放页字体抖动效果优化 2025-08-20 11:35:07 +08:00
底层用户
201186bab2 🐞 fix: 修复同时请求问题 #448 2025-08-20 10:35:43 +08:00
底层用户
bfcd59daca Merge pull request #417 from serious-snow/dev
feat(Setting): 更新音质选项并添加新选择
2025-05-23 09:12:17 +08:00
wangjian
d5c3843c3f feat(Setting): 更新音质选项并添加新选择
- 参考手机端,更新音质说明文案
- 增加 jyeffect、vivid、dolby 音质
2025-05-22 10:55:17 +08:00
imsyy
d3f307eac5 🐞 fix: 修复登录错误 2025-04-19 22:56:33 +08:00
imsyy
675a52b8d1 🎈 perf: 优化 cookie 登录体验 2025-04-19 00:31:41 +08:00
imsyy
aee90e9c4e 🔧 build: 更新依赖 2025-04-18 23:34:22 +08:00
底层用户
eb39b81d8d Merge pull request #375 from xiaoQQya/dev
Docker 镜像内置 UnblockNeteaseMusic,  支持播放部分无版权歌曲
2025-03-18 14:56:37 +08:00
xiaoQQya
436df47104 feat: Docker 镜像内置 UnblockNeteaseMusic, 支持播放部分无版权歌曲 2025-03-15 11:28:12 +08:00
imsyy
e04e5e34c6 Merge branch 'dev' of github.com:imsyy/SPlayer into dev 2025-01-17 17:03:33 +08:00
imsyy
b57d685c03 🔧 build: 更新依赖 2025-01-17 17:03:16 +08:00
底层用户
6684172592 Merge pull request #333 from serious-snow/dev
fix: 更新日志中的超链接跳转由新窗口打开
2025-01-08 11:33:21 +08:00
wangjian
0257e74ff0 fix: 更新日志中的超链接跳转由新窗口打开
- 在 AboutSetting 组件中添加了 jumpLink 函数,实现更新日志中链接的点击跳转
- 修复 CoverList 组件移入移出会出现黑角的bug
2025-01-08 10:59:10 +08:00
imsyy
16c8865651 Merge branch 'dev' of github.com:imsyy/SPlayer into dev 2024-12-27 10:25:39 +08:00
imsyy
1a61aa2458 🐞 fix: 修复网页端跨域 #317 2024-12-27 10:25:01 +08:00
底层用户
e543f07d8e Merge pull request #307 from 239144498/patch-1
Update idMeta.json
2024-12-24 18:22:30 +08:00
imsyy
96a0495a88 🐞 fix: 修复本地目录无法读取 #315 2024-12-23 11:21:22 +08:00
imsyy
02befcd8a4 🐳 chore: 修复工作流 2024-12-12 14:04:30 +08:00
Naihe
1edceeebdd Update idMeta.json
增加会员雷达,里面的每日推荐歌曲质量还不错
https://music.163.com/#/playlist?id=8402996200
2024-12-09 13:23:10 +08:00
169 changed files with 12839 additions and 6626 deletions

View File

@@ -1,35 +1,75 @@
name: 遇到问题
description: 关于使用过程中遇到的问题
title: 请填写标题
labels: [bug]
body:
- type: input
- type: markdown
attributes:
value: |
---
在此之前,我们默认你已经知道该如何提问。简而言之,**你要精确地描述问题并提供充足的信息**
此外,如果需要,你可以使用 <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> 打开开发者工具为我们提供信息
- type: checkboxes
validations:
required: false
attributes:
label: "检查清单"
description: |
我们需要了解一些信息,你需要检查下面的检查项 <br />
**这里并不是每一个检查项都必要,根据你的真实情况勾选即可**
options:
- label: "我已检索仓库中所有的 Issues确保我**没有重复提交问题**;或有相似 Issue但我觉得我的情况不包含在那个相似 Issue 之内"
- label: "我已经找到了可以复现这个问题的方法,并且写在了下面的「具体信息」中"
- label: "此问题可以在我的设备和当前环境中**稳定复现**"
- label: "此问题可以在最新版本 (Latest Release) 中复现"
- label: "此问题是在我更新到当前版本后**才**出现的"
- type: dropdown
validations:
required: true
attributes:
label: "是网页端还是客户端"
placeholder: "客户端"
options:
- "客户端"
- "网页端"
default: 0
- type: input
validations:
required: true
attributes:
label: "当前系统环境"
placeholder: "win11"
placeholder: "Windows 11"
- type: input
validations:
required: true
required: false
attributes:
label: "当前 Node.js 及 npm 版本"
placeholder: "v18.16.0 / v9.6.7"
placeholder: "如:v18.16.0 / v9.6.7 (选填)"
- type: input
validations:
required: true
attributes:
label: "当前版本"
placeholder: "v1.0.0"
description: |
填写关于软件里的或 Releases 中版本号即可 <br />
如果是自行构建或从 GitHub Actions 下载的开发版,还需要提供 Commit ID
placeholder: "如v1.0.0"
- type: textarea
id: other
attributes:
label: "具体信息"
description: "请填写完整的复现步骤和遇到的问题,包括但不限于报错信息、控制台输出、网络请求等"
description: |
请填写完整的复现步骤和遇到的问题,还请尽量提供所有可能的信息,提供包括但不限于:
- 分步的复现步骤,可以使用 `1. xxx` (换行) `2. xxx` 的格式
- 截图(如果结合复现步骤还是无法详细表述,甚至可以录屏)
- 开发者工具中的:控制台输出报错、网络请求等
- 出现问题的在线歌曲链接、出现问题的文件(本地歌曲、歌词等)的下载链接
等信息,以更好地帮助我们解决你的问题
placeholder: "请填写具体的复现步骤和遇到的问题"

View File

@@ -1,61 +0,0 @@
# Dev 分支推送部署预览
## 仅部署 Win 端
name: Build Dev
on:
push:
branches:
- dev
jobs:
release:
name: Build and electron app
runs-on: windows-latest
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "22.x"
# 复制环境变量文件
- name: Copy .env.example
run: |
if (-not (Test-Path .env)) {
Copy-Item .env.example .env
} else {
Write-Host ".env file already exists. Skipping the copy step."
}
# 安装项目依赖
- name: Install Dependencies
run: npm install
# 构建 Electron App
- name: Build Electron App
run: npm run build:win
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# 清理不必要的构建产物
- name: Cleanup Artifacts
run: npx del-cli "dist/**/*" "!dist/*.exe"
# 上传构建产物
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: SPlayer-dev
path: dist
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
tag_name: ${{ github.ref }}
name: ${{ github.ref }}-rc
body: This version is still under development, currently only provides windows version, non-developers please do not use!
draft: false
prerelease: true
files: dist/*.exe
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

67
.github/workflows/dev.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
# Dev 分支推送部署预览
## 部署 Windows x64 和 ARM64 版本
name: Build Dev
on:
push:
branches:
- dev
env:
NODE_VERSION: 22.x
PNPM_VERSION: 8
jobs:
# Windows x64 架构
build-win:
name: Build Electron App for Windows
runs-on: windows-latest
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4
# 设置 pnpm 版本
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: ${{ env.PNPM_VERSION }}
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
# 复制环境变量文件
- name: Copy .env.example
run: |
if (-not (Test-Path .env)) {
Copy-Item .env.example .env
} else {
Write-Host ".env file already exists. Skipping the copy step."
}
# 安装项目依赖
- name: Install Dependencies
run: pnpm install
# 清理旧的构建产物
- name: Clean dist folder
run: |
if (Test-Path dist) {
Remove-Item -Recurse -Force dist
}
# 构建 Electron App (x64)
- name: Build Electron App for Windows x64
# 仅 x64
run: pnpm run build:win -- --arch=x64
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 清理不必要的构建产物(保留 .exe 和 .blockmap 文件)
- name: Cleanup Artifacts
run: npx del-cli "dist/**/*.yaml" "dist/**/*.yml"
# 上传构建产物(仅上传 x64 架构的 .exe 文件)
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: SPlayer-dev
path: |
dist/**.exe

View File

@@ -1,164 +1,169 @@
# Release 发行版本部署
## 多端部署
name: Build Release
name: Build & Release
on:
push:
tags:
- v*
- v* # 只在 tag v* 时触发
workflow_dispatch:
env:
NODE_VERSION: 22.x
PNPM_VERSION: 8
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
jobs:
# Windows
build-windows:
name: Build for Windows
runs-on: windows-latest
# ===================================================================
# 并行构建所有平台和架构
# ===================================================================
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
# 矩阵策略
# 即使一个矩阵任务失败,其他任务也会继续运行
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
# 开始步骤
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4
# 设置 pnpm 版本
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
node-version: "20.x"
version: ${{ env.PNPM_VERSION }}
# 安装 Node.js
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
cache: "pnpm"
# 清理旧的构建产物
- name: Clean workspace on Windows
if: runner.os == 'Windows'
run: |
Write-Host "🧹 Cleaning workspace, node_modules, and Electron caches..."
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
if (Test-Path out) { Remove-Item -Recurse -Force out }
if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }
if (Test-Path "$env:LOCALAPPDATA\electron-builder") {
Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue
}
if (Test-Path "$env:LOCALAPPDATA\electron") {
Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue
}
- name: Clean workspace on macOS & Linux
if: runner.os == 'macOS' || runner.os == 'Linux'
run: |
echo "🧹 Cleaning workspace, node_modules, and Electron caches..."
rm -rf dist out node_modules ~/.cache/electron-builder ~/.cache/electron
# 安装项目依赖
- name: Install dependencies
run: pnpm install
# 复制环境变量文件
- name: Copy .env.example
- name: Copy .env file on Windows
if: runner.os == 'Windows'
run: |
if (-not (Test-Path .env)) {
Copy-Item .env.example .env
} else {
Write-Host ".env file already exists. Skipping the copy step."
}
# 安装项目依赖
- name: Install Dependencies
run: npm install
# 构建 Electron App
- name: Build Electron App for Windows
run: npm run build:win || true
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# 上传构建产物
- name: Upload Windows artifact
uses: actions/upload-artifact@v4
with:
name: SPlarer-Win
if-no-files-found: ignore
path: dist/*.*
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: dist/*.*
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# Mac
build-macos:
name: Build for macOS
runs-on: macos-latest
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
# 复制环境变量文件
- name: Copy .env.example
- name: Copy .env file on macOS & Linux
if: runner.os == 'macOS' || runner.os == 'Linux'
run: |
if [ ! -f .env ]; then
cp .env.example .env
else
echo ".env file already exists. Skipping the copy step."
fi
# 安装项目依赖
- name: Install Dependencies
run: npm install
# 构建 Electron App
- name: Build Electron App for macOS
run: npm run build:mac || true
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# 上传构建产物
- name: Upload macOS artifact
uses: actions/upload-artifact@v4
with:
name: SPlarer-Macos
if-no-files-found: ignore
path: dist/*.*
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
with:
files: dist/*.*
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# Linux
build-linux:
name: Build for Linux
runs-on: ubuntu-22.04
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
# 更新 Ubuntu 软件源
- name: Ubuntu Update with sudo
if: runner.os == 'Linux'
run: sudo apt-get update
# 安装依赖
- name: Install RPM & Pacman
if: runner.os == 'Linux'
run: |
sudo apt-get install --no-install-recommends -y rpm &&
sudo apt-get install --no-install-recommends -y libarchive-tools &&
sudo apt-get install --no-install-recommends -y libopenjp2-tools
# 安 Snapcraft
# 安 Snapcraft
- name: Install Snapcraft
if: runner.os == 'Linux'
uses: samuelmeuli/action-snapcraft@v2
with:
snapcraft_token: ${{ secrets.SNAPCRAFT_TOKEN }}
# 复制环境变量文件
- name: Copy .env.example
run: |
if [ ! -f .env ]; then
cp .env.example .env
else
echo ".env file already exists. Skipping the copy step."
fi
# 安装项目依赖
- name: Install Dependencies
run: npm install
# 构建 Electron App
- name: Build Electron App for Linux
run: npm run build:linux || true
- name: Build Windows x64 & ARM64 App
if: runner.os == 'Windows'
run: pnpm build:win || true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build macOS Universal App
if: runner.os == 'macOS'
run: pnpm build:mac || true
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Linux x64 & ARM64 App
if: runner.os == 'Linux'
run: pnpm build:linux || true
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 上传 Snap 包到 Snapcraft 商店
- name: Publish Snap to Snap Store
if: runner.os == 'Linux'
run: snapcraft upload dist/*.snap --release stable
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
# 上传构建产物
- name: Upload Linux artifact
continue-on-error: true
# 合并所有构建
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: SPlarer-Linux
if-no-files-found: ignore
name: SPlayer-${{ runner.os }}
path: dist/*.*
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v')
# ===================================================================
# 收集并发布 Release
# ===================================================================
release:
name: Create GitHub Release
needs: build
runs-on: ubuntu-latest
# 需要写入权限
permissions:
contents: write
steps:
# 将所有产物下载到 artifacts 文件夹
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
files: dist/*.*
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
path: artifacts
# 创建 GitHub Release 并上传所有产物
- name: Create GitHub Release and Upload Assets
uses: softprops/action-gh-release@v2
continue-on-error: true
with:
token: ${{ secrets.GITHUB_TOKEN }}
# 自动生成 Release 说明
generate_release_notes: true
# 发布为草稿
draft: false
# 发布为预发布
prerelease: false
# 全部上传
files: |
!artifacts/**/*-unpacked/**
artifacts/**/*.exe
artifacts/**/*.dmg
artifacts/**/*.zip
artifacts/**/*.AppImage
artifacts/**/*.deb
artifacts/**/*.rpm
artifacts/**/*.pacman
artifacts/**/*.snap
artifacts/**/*.tar.gz
artifacts/**/*.yml
artifacts/**/*.blockmap

2
.gitignore vendored
View File

@@ -29,4 +29,4 @@ components.d.ts
*.sln
*.sw?
.env.development
.env.production
.env.production

View File

@@ -23,8 +23,16 @@ COPY --from=builder /app/out/renderer /usr/share/nginx/html
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
RUN apk add --no-cache npm
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
RUN npm install -g NeteaseCloudMusicApi
RUN apk add --no-cache npm python3 youtube-dl \
&& npm install -g @unblockneteasemusic/server NeteaseCloudMusicApi \
&& wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
&& chmod +x /usr/local/bin/yt-dlp \
&& chmod +x /docker-entrypoint.sh
CMD nginx && npx NeteaseCloudMusicApi
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["npx", "NeteaseCloudMusicApi"]

View File

@@ -1,6 +1,12 @@
# SPlayer
> 一个简约的音乐播放器
> A simple music player
![Stars](https://img.shields.io/github/stars/imsyy/SPlayer?style=flat)
![Version](https://img.shields.io/github/v/release/imsyy/SPlayer)
[![Build Release](https://github.com/imsyy/SPlayer/actions/workflows/release.yml/badge.svg)](https://github.com/imsyy/SPlayer/actions/workflows/release.yml)
![License](https://img.shields.io/github/license/imsyy/SPlayer)
![Issues](https://img.shields.io/github/issues/imsyy/SPlayer)
![main](/screenshots/SPlayer.jpg)

View File

@@ -13,6 +13,9 @@ export default {
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"ShallowRef": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"asyncComputed": true,
@@ -29,6 +32,7 @@ export default {
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
@@ -43,6 +47,7 @@ export default {
"extendRef": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"getCurrentWatcher": true,
"h": true,
"ignorableWatch": true,
"inject": true,
@@ -52,6 +57,7 @@ export default {
"isReactive": true,
"isReadonly": true,
"isRef": true,
"isShallow": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
@@ -63,6 +69,7 @@ export default {
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onElementRemoval": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
@@ -145,6 +152,7 @@ export default {
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCountdown": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
@@ -229,12 +237,14 @@ export default {
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePreferredReducedTransparency": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useSSRWidth": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
@@ -261,6 +271,7 @@ export default {
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeAgoIntl": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,

10
auto-imports.d.ts vendored
View File

@@ -21,6 +21,7 @@ declare global {
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createRef: typeof import('@vueuse/core')['createRef']
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
@@ -35,6 +36,7 @@ declare global {
const extendRef: typeof import('@vueuse/core')['extendRef']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
@@ -44,6 +46,7 @@ declare global {
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const isShallow: typeof import('vue')['isShallow']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
@@ -55,6 +58,7 @@ declare global {
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onLongPress: typeof import('@vueuse/core')['onLongPress']
@@ -137,6 +141,7 @@ declare global {
const useCloned: typeof import('@vueuse/core')['useCloned']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCountdown: typeof import('@vueuse/core')['useCountdown']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
@@ -221,12 +226,14 @@ declare global {
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
const usePrevious: typeof import('@vueuse/core')['usePrevious']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
@@ -253,6 +260,7 @@ declare global {
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
@@ -296,6 +304,6 @@ declare global {
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

20
components.d.ts vendored
View File

@@ -2,6 +2,7 @@
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
@@ -9,7 +10,9 @@ declare module 'vue' {
export interface GlobalComponents {
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
ArtistList: typeof import('./src/components/List/ArtistList.vue')['default']
BatchList: typeof import('./src/components/Modal/batchList.vue')['default']
AutoClose: typeof import('./src/components/Modal/AutoClose.vue')['default']
BatchList: typeof import('./src/components/Modal/BatchList.vue')['default']
ChangeRate: typeof import('./src/components/Modal/ChangeRate.vue')['default']
CloudMatch: typeof import('./src/components/Modal/CloudMatch.vue')['default']
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
@@ -17,7 +20,8 @@ declare module 'vue' {
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
ExcludeKeywords: typeof import('./src/components/Modal/ExcludeKeywords.vue')['default']
Equalizer: typeof import('./src/components/Modal/Equalizer.vue')['default']
ExcludeLyrics: typeof import('./src/components/Modal/ExcludeLyrics.vue')['default']
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
@@ -28,6 +32,7 @@ declare module 'vue' {
LoginPhone: typeof import('./src/components/Modal/Login/LoginPhone.vue')['default']
LoginQRCode: typeof import('./src/components/Modal/Login/LoginQRCode.vue')['default']
LoginUID: typeof import('./src/components/Modal/Login/LoginUID.vue')['default']
LyricMenu: typeof import('./src/components/Player/LyricMenu.vue')['default']
LyricsSetting: typeof import('./src/components/Setting/LyricsSetting.vue')['default']
MainAMLyric: typeof import('./src/components/Player/MainAMLyric.vue')['default']
MainLyric: typeof import('./src/components/Player/MainLyric.vue')['default']
@@ -56,11 +61,8 @@ declare module 'vue' {
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex']
NFloatButton: typeof import('naive-ui')['NFloatButton']
NFloatButtonGroup: typeof import('naive-ui')['NFloatButtonGroup']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
@@ -68,7 +70,6 @@ declare module 'vue' {
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1']
NH2: typeof import('naive-ui')['NH2']
NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
@@ -90,19 +91,16 @@ declare module 'vue' {
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
NOl: typeof import('naive-ui')['NOl']
NP: typeof import('naive-ui')['NP']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NProgress: typeof import('naive-ui')['NProgress']
NQrCode: typeof import('naive-ui')['NQrCode']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NResult: typeof import('naive-ui')['NResult']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSlider: typeof import('naive-ui')['NSlider']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
@@ -117,6 +115,8 @@ declare module 'vue' {
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
PlayerData: typeof import('./src/components/Player/PlayerData.vue')['default']
PlayerMenu: typeof import('./src/components/Player/PlayerMenu.vue')['default']
PlayerRightMenu: typeof import('./src/components/Player/PlayerRightMenu.vue')['default']
PlayerSlider: typeof import('./src/components/Player/PlayerSlider.vue')['default']
PlayerSpectrum: typeof import('./src/components/Player/PlayerSpectrum.vue')['default']
PlaylistAdd: typeof import('./src/components/Modal/PlaylistAdd.vue')['default']
PlaySetting: typeof import('./src/components/Setting/PlaySetting.vue')['default']

View File

@@ -10,3 +10,21 @@ services:
ports:
- 25884:25884
restart: always
environment:
# 所有变量都不是必填项
# 网易云服务端 IP, 可在宿主机通过 ping music.163.com 获得
- NETEASE_SERVER_IP=220.197.30.65
# UnblockNeteaseMusic 使用的音源, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E9%9F%B3%E6%BA%90%E6%B8%85%E5%8D%95
- UNBLOCK_SOURCES=kugou kuwo bilibili
# 可添加 UnblockNeteaseMusic 支持的任何环境变量, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F
- ENABLE_FLAC=false
- ENABLE_HTTPDNS=false
- BLOCK_ADS=true
- DISABLE_UPGRADE_CHECK=false
- DEVELOPMENT=false
- FOLLOW_SOURCE_ORDER=true
- JSON_LOG=false
- NO_CACHE=false
- SELECT_MAX_BR=true
- LOG_LEVEL=info
- SEARCH_ALBUM=true

29
docker-entrypoint.sh Normal file
View File

@@ -0,0 +1,29 @@
#!/bin/sh
set -e
# start unblock service in the background
npx unblockneteasemusic -p 80:443 -s -f ${NETEASE_SERVER_IP:-220.197.30.65} -o ${UNBLOCK_SOURCES:-kugou bodian pyncmd} 2>&1 &
# point the neteasemusic address to the unblock service
if ! grep -q "music.163.com" /etc/hosts; then
echo "127.0.0.1 music.163.com" >> /etc/hosts
fi
if ! grep -q "interface.music.163.com" /etc/hosts; then
echo "127.0.0.1 interface.music.163.com" >> /etc/hosts
fi
if ! grep -q "interface3.music.163.com" /etc/hosts; then
echo "127.0.0.1 interface3.music.163.com" >> /etc/hosts
fi
if ! grep -q "interface.music.163.com.163jiasu.com" /etc/hosts; then
echo "127.0.0.1 interface.music.163.com.163jiasu.com" >> /etc/hosts
fi
if ! grep -q "interface3.music.163.com.163jiasu.com" /etc/hosts; then
echo "127.0.0.1 interface3.music.163.com.163jiasu.com" >> /etc/hosts
fi
# start the nginx daemon
nginx
# start the main process
exec "$@"

173
electron-builder.config.ts Normal file
View File

@@ -0,0 +1,173 @@
import type { Configuration } from "electron-builder";
const config: Configuration = {
// 应用程序的唯一标识符
appId: "com.imsyy.splayer",
// 应用程序的产品名称
productName: "SPlayer",
copyright: "Copyright © imsyy 2023",
// 构建资源所在的目录
directories: {
buildResources: "build",
},
// 包含在最终应用程序构建中的文件列表
// 使用通配符 ! 表示排除不需要的文件
files: [
"public/**",
"out/**",
"!**/.vscode/*",
"!src/*",
"!electron.vite.config.{js,ts,mjs,cjs}",
"!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}",
"!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
],
// 哪些文件将不会被压缩,而是解压到构建目录
asarUnpack: ["public/**"],
win: {
// 可执行文件名
executableName: "SPlayer",
// 应用程序的图标文件路径
icon: "public/icons/logo.ico",
// Windows 平台全局文件名模板
artifactName: "${productName}-${version}-${arch}.${ext}",
// 是否对可执行文件进行签名和编辑
// signAndEditExecutable: false,
// 构建类型(架构由命令行参数 --x64 或 --arm64 指定)
target: [
// 安装版
{
target: "nsis",
arch: ["x64", "arm64"],
},
// 打包版
{
target: "portable",
arch: ["x64", "arm64"],
},
],
},
// NSIS 安装器配置
nsis: {
// 是否一键式安装
oneClick: false,
// 安装程序的生成名称
artifactName: "${productName}-${version}-${arch}-setup.${ext}",
// 创建的桌面快捷方式名称
shortcutName: "${productName}",
// 卸载时显示的名称
uninstallDisplayName: "${productName}",
// 创建桌面图标
createDesktopShortcut: "always",
// 是否允许 UAC 提升权限
allowElevation: true,
// 是否允许用户更改安装目录
allowToChangeInstallationDirectory: true,
// 安装包图标
installerIcon: "public/icons/favicon.ico",
// 卸载命令图标
uninstallerIcon: "public/icons/favicon.ico",
},
// Portable 便携版配置
portable: {
// 便携版文件名
artifactName: "${productName}-${version}-${arch}-portable.${ext}",
},
// macOS 平台配置
mac: {
// 可执行文件名
executableName: "SPlayer",
// 应用程序的图标文件路径
icon: "public/icons/favicon-512x512.png",
// 权限继承的文件路径
entitlementsInherit: "build/entitlements.mac.plist",
// macOS 平台全局文件名模板
artifactName: "${productName}-${version}-${arch}.${ext}",
// 扩展信息,如权限描述
extendInfo: {
NSCameraUsageDescription: "Application requests access to the device's camera.",
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
NSDocumentsFolderUsageDescription:
"Application requests access to the user's Documents folder.",
NSDownloadsFolderUsageDescription:
"Application requests access to the user's Downloads folder.",
},
// 是否启用应用程序的 Notarization苹果的安全审核
notarize: false,
darkModeSupport: true,
category: "public.app-category.music",
target: [
// DMG 安装版
{
target: "dmg",
arch: ["x64", "arm64"],
},
// 压缩包安装版
{
target: "zip",
arch: ["x64", "arm64"],
},
],
},
// Linux 平台配置
linux: {
// 可执行文件名
executableName: "SPlayer",
// 应用程序的图标文件路径
icon: "public/icons/favicon-512x512.png",
// Linux 所有格式的统一文件名模板
artifactName: "${name}-${version}-${arch}.${ext}",
// 构建类型 - 支持 x64 和 ARM64 架构
target: [
// Pacman 包管理器
{
target: "pacman",
arch: ["x64", "arm64"],
},
// AppImage 格式
{
target: "AppImage",
arch: ["x64", "arm64"],
},
// Debian 包管理器
{
target: "deb",
arch: ["x64", "arm64"],
},
// RPM 包管理器
{
target: "rpm",
arch: ["x64", "arm64"],
},
// Snap 包管理器(仅支持 x64 架构)
{
target: "snap",
arch: ["x64"],
},
// 压缩包格式
{
target: "tar.gz",
arch: ["x64", "arm64"],
},
],
// 维护者信息
maintainer: "imsyy.top",
// 应用程序类别
category: "Audio;Music",
},
// AppImage 特定配置
appImage: {
// AppImage 文件的生成名称
artifactName: "${name}-${version}-${arch}.${ext}",
},
// 是否在构建之前重新编译原生模块
npmRebuild: false,
// Electron 下载镜像配置
electronDownload: {
mirror: "https://npmmirror.com/mirrors/electron/",
},
// 发布配置
// 先留空,不自动上传
publish: [],
};
export default config;

View File

@@ -1,115 +0,0 @@
# 应用程序的唯一标识符
appId: com.imsyy.splayer
# 应用程序的产品名称
productName: SPlayer
copyright: Copyright © imsyy 2023
# 构建资源所在的目录
directories:
buildResources: public
# 包含在最终应用程序构建中的文件列表
# 使用通配符 ! 表示排除不需要的文件
files:
- "public/**"
- "out/**"
- "!**/.vscode/*"
- "!src/*"
- "!electron.vite.config.{js,ts,mjs,cjs}"
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
# 哪些文件将不会被压缩,而是解压到构建目录
asarUnpack:
- public/**
win:
# 可执行文件名
executableName: SPlayer
# 应用程序的图标文件路径
icon: public/icons/favicon-512x512.png
# 构建类型
target:
# 安装版
- nsis
# 打包版
- portable
# NSIS 安装器配置
nsis:
# 是否一键式安装
oneClick: false
# 安装程序的生成名称
artifactName: ${productName}-${version}-setup.${ext}
# 创建的桌面快捷方式名称
shortcutName: ${productName}
# 卸载时显示的名称
uninstallDisplayName: ${productName}
# 创建桌面图标
createDesktopShortcut: always
# 是否允许 UAC 提升权限
allowElevation: true
# 是否允许用户更改安装目录
allowToChangeInstallationDirectory: true
# 安装包图标
installerIcon: public/icons/favicon.ico
# 卸载命令图标
uninstallerIcon: public/icons/favicon.ico
# macOS 平台配置
mac:
# 可执行文件名
executableName: SPlayer
# 应用程序的图标文件路径
icon: public/icons/favicon-512x512.png
# 权限继承的文件路径
entitlementsInherit: build/entitlements.mac.plist
# 扩展信息,如权限描述
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
# 是否启用应用程序的 Notarization苹果的安全审核
notarize: false
darkModeSupport: true
category: public.app-category.music
target:
- target: dmg
arch:
- x64
- arm64
- target: zip
arch:
- x64
- arm64
# macOS 平台的 DMG 配置
dmg:
# DMG 文件的生成名称
artifactName: ${name}-${version}.${ext}
# Linux 平台配置
linux:
# 可执行文件名
executableName: splayer
# 应用程序的图标文件路径
icon: public/icons/favicon-512x512.png
# 构建类型
target:
- pacman
- AppImage
- deb
- rpm
- snap
- tar.gz
# 维护者信息
maintainer: imsyy.top
# 应用程序类别
category: Audio;Music
# AppImage 配置
appImage:
# AppImage 文件的生成名称
artifactName: ${name}-${version}.${ext}
# 是否在构建之前重新编译原生模块
npmRebuild: false
# 自动更新的配置
publish:
# 更新提供商
provider: github
# 自动更新检查的 URL
# url: https://example.com/auto-updates
owner: "imsyy"
repo: "SPlayer"

View File

@@ -6,6 +6,7 @@ import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import viteCompression from "vite-plugin-compression";
// import VueDevTools from "vite-plugin-vue-devtools";
import wasm from "vite-plugin-wasm";
export default defineConfig(({ command, mode }) => {
@@ -28,7 +29,6 @@ export default defineConfig(({ command, mode }) => {
input: {
index: resolve(__dirname, "electron/main/index.ts"),
lyric: resolve(__dirname, "web/lyric.html"),
loading: resolve(__dirname, "web/loading.html"),
},
},
},
@@ -49,6 +49,7 @@ export default defineConfig(({ command, mode }) => {
root: ".",
plugins: [
vue(),
// mode === "development" && VueDevTools(),
AutoImport({
imports: [
"vue",
@@ -88,7 +89,7 @@ export default defineConfig(({ command, mode }) => {
"/api": {
target: `http://127.0.0.1:${servePort}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, "/api/"),
rewrite: (path) => path.replace(/^\/api/, "/api"),
},
},
},
@@ -101,6 +102,7 @@ export default defineConfig(({ command, mode }) => {
rollupOptions: {
input: {
index: resolve(__dirname, "index.html"),
loading: resolve(__dirname, "web/loading/index.html"),
},
output: {
manualChunks: {

View File

@@ -1,206 +1,60 @@
import { app, shell, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { join } from "path";
import { app, BrowserWindow } from "electron";
import { electronApp } from "@electron-toolkit/utils";
import { release, type } from "os";
import { isDev, isMac, appName } from "./utils";
import { registerAllShortcuts, unregisterShortcuts } from "./shortcut";
import { isMac } from "./utils/config";
import { unregisterShortcuts } from "./shortcut";
import { initTray, MainTray } from "./tray";
import { initThumbar, Thumbar } from "./thumbar";
import { type StoreType, initStore } from "./store";
import Store from "electron-store";
import { processLog } from "./logger";
import initAppServer from "../server";
import initIpcMain from "./ipcMain";
import log from "./logger";
// icon
import icon from "../../public/icons/favicon.png?asset";
import { initSingleLock } from "./utils/single-lock";
import loadWindow from "./windows/load-window";
import mainWindow from "./windows/main-window";
import initIpc from "./ipc";
// 屏蔽报错
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
// 模拟打包
Object.defineProperty(app, "isPackaged", {
get() {
return true;
},
});
// 主进程
class MainProcess {
// 窗口
mainWindow: BrowserWindow | null = null;
lyricWindow: BrowserWindow | null = null;
loadingWindow: BrowserWindow | null = null;
// store
store: Store<StoreType> | null = null;
loadWindow: BrowserWindow | null = null;
// 托盘
mainTray: MainTray | null = null;
// 工具栏
thumbar: Thumbar | null = null;
// 是否退出
isQuit: boolean = false;
constructor() {
log.info("🚀 Main process startup");
processLog.info("🚀 Main process startup");
// 程序单例锁
initSingleLock();
// 禁用 Windows 7 的 GPU 加速功能
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
// 单例锁
if (!app.requestSingleInstanceLock()) {
log.error("❌ There is already a program running and this process is terminated");
app.quit();
process.exit(0);
} else this.showWindow();
// 准备就绪
app.on("ready", async () => {
log.info("🚀 Application Process Startup");
// 监听应用事件
this.handleAppEvents();
// Electron 初始化完成后
// 某些API只有在此事件发生后才能使用
app.whenReady().then(async () => {
processLog.info("🚀 Application Process Startup");
// 设置应用程序名称
electronApp.setAppUserModelId("com.imsyy.splayer");
// 初始化 store
this.store = initStore();
// 启动主服务进程
await initAppServer();
// 启动进程
this.createLoadingWindow();
this.createMainWindow();
this.createLyricsWindow();
this.handleAppEvents();
this.handleWindowEvents();
// 启动窗口
this.loadWindow = loadWindow.create();
this.mainWindow = mainWindow.create();
// 注册其他服务
this.mainTray = initTray(this.mainWindow!, this.lyricWindow!);
this.thumbar = initThumbar(this.mainWindow!);
// 注册主进程事件
initIpcMain(
this.mainWindow,
this.lyricWindow,
this.loadingWindow,
this.mainTray,
this.thumbar,
this.store,
);
// 注册快捷键
registerAllShortcuts(this.mainWindow!);
this.mainTray = initTray(this.mainWindow!);
// 注册 IPC 通信
initIpc();
});
}
// 创建窗口
createWindow(options: BrowserWindowConstructorOptions = {}): BrowserWindow {
const defaultOptions: BrowserWindowConstructorOptions = {
title: appName,
width: 1280,
height: 720,
frame: false,
center: true,
// 图标
icon,
webPreferences: {
preload: join(__dirname, "../preload/index.mjs"),
// 禁用渲染器沙盒
sandbox: false,
// 禁用同源策略
webSecurity: false,
// 允许 HTTP
allowRunningInsecureContent: true,
// 禁用拼写检查
spellcheck: false,
// 启用 Node.js
nodeIntegration: true,
nodeIntegrationInWorker: true,
// 启用上下文隔离
contextIsolation: false,
},
};
// 合并参数
options = Object.assign(defaultOptions, options);
// 创建窗口
const win = new BrowserWindow(options);
return win;
}
// 创建主窗口
createMainWindow() {
// 窗口配置项
const options: BrowserWindowConstructorOptions = {
width: this.store?.get("window").width,
height: this.store?.get("window").height,
minHeight: 800,
minWidth: 1280,
// 菜单栏
titleBarStyle: "customButtonsOnHover",
// 立即显示窗口
show: false,
};
// 初始化窗口
this.mainWindow = this.createWindow(options);
// 渲染路径
if (isDev && process.env["ELECTRON_RENDERER_URL"]) {
this.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
} else {
const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
this.mainWindow.loadURL(`http://127.0.0.1:${port}`);
}
// 配置网络代理
if (this.store?.get("proxy")) {
this.mainWindow.webContents.session.setProxy({ proxyRules: this.store?.get("proxy") });
}
// 窗口打开处理程序
this.mainWindow.webContents.setWindowOpenHandler((details) => {
const { url } = details;
if (url.startsWith("https://") || url.startsWith("http://")) {
shell.openExternal(url);
}
return { action: "deny" };
});
}
// 创建加载窗口
createLoadingWindow() {
// 初始化窗口
this.loadingWindow = this.createWindow({
width: 800,
height: 560,
maxWidth: 800,
maxHeight: 560,
resizable: false,
});
// 渲染路径
this.loadingWindow.loadFile(join(__dirname, "../main/web/loading.html"));
}
// 创建桌面歌词窗口
createLyricsWindow() {
// 初始化窗口
this.lyricWindow = this.createWindow({
width: this.store?.get("lyric").width || 800,
height: this.store?.get("lyric").height || 180,
minWidth: 440,
minHeight: 120,
maxWidth: 1600,
maxHeight: 300,
// 窗口位置
x: this.store?.get("lyric").x,
y: this.store?.get("lyric").y,
transparent: true,
backgroundColor: "rgba(0, 0, 0, 0)",
alwaysOnTop: true,
resizable: true,
movable: true,
// 不在任务栏显示
skipTaskbar: true,
// 窗口不能最小化
minimizable: false,
// 窗口不能最大化
maximizable: false,
// 窗口不能进入全屏状态
fullscreenable: false,
show: false,
});
// 渲染路径
this.lyricWindow.loadFile(join(__dirname, "../main/web/lyric.html"));
}
// 应用程序事件
handleAppEvents() {
// 窗口被关闭时
app.on("window-all-closed", () => {
if (!isMac) app.quit();
this.mainWindow = null;
this.loadingWindow = null;
this.loadWindow = null;
});
// 应用被激活
@@ -208,24 +62,12 @@ class MainProcess {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
this.createMainWindow();
}
});
// 新增 session
app.on("second-instance", () => {
this.showWindow();
});
// 开发环境控制台
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
// 自定义协议
app.on("open-url", (_, url) => {
console.log("Received custom protocol URL:", url);
processLog.log("Received custom protocol URL:", url);
});
// 将要退出
@@ -239,61 +81,6 @@ class MainProcess {
this.isQuit = true;
});
}
// 窗口事件
handleWindowEvents() {
this.mainWindow?.on("ready-to-show", () => {
if (!this.mainWindow) return;
this.thumbar = initThumbar(this.mainWindow);
});
this.mainWindow?.on("show", () => {
// this.mainWindow?.webContents.send("lyricsScroll");
});
this.mainWindow?.on("focus", () => {
this.saveBounds();
});
// 移动或缩放
this.mainWindow?.on("resized", () => {
// 若处于全屏则不保存
if (this.mainWindow?.isFullScreen()) return;
this.saveBounds();
});
this.mainWindow?.on("moved", () => {
this.saveBounds();
});
// 歌词窗口缩放
this.lyricWindow?.on("resized", () => {
const bounds = this.lyricWindow?.getBounds();
if (bounds) {
const { width, height } = bounds;
this.store?.set("lyric", { ...this.store?.get("lyric"), width, height });
}
});
// 窗口关闭
this.mainWindow?.on("close", (event) => {
event.preventDefault();
if (this.isQuit) {
app.exit();
} else {
this.mainWindow?.hide();
}
});
}
// 更新窗口大小
saveBounds() {
if (this.mainWindow?.isFullScreen()) return;
const bounds = this.mainWindow?.getBounds();
if (bounds) this.store?.set("window", bounds);
}
// 显示窗口
showWindow() {
if (this.mainWindow) {
this.mainWindow.show();
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
this.mainWindow.focus();
}
}
}
export default new MainProcess();

View File

@@ -0,0 +1,27 @@
import initFileIpc from "./ipc-file";
import initLyricIpc from "./ipc-lyric";
import initShortcutIpc from "./ipc-shortcut";
import initStoreIpc from "./ipc-store";
import initSystemIpc from "./ipc-system";
import initThumbarIpc from "./ipc-thumbar";
import initTrayIpc from "./ipc-tray";
import initUpdateIpc from "./ipc-update";
import initWindowsIpc from "./ipc-window";
/**
* 初始化全部 IPC 通信
* @returns void
*/
const initIpc = (): void => {
initSystemIpc();
initWindowsIpc();
initUpdateIpc();
initFileIpc();
initTrayIpc();
initLyricIpc();
initStoreIpc();
initThumbarIpc();
initShortcutIpc();
};
export default initIpc;

View File

@@ -0,0 +1,390 @@
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
import { basename, isAbsolute, join, relative, resolve } from "path";
import { access, readFile, stat, unlink, writeFile } from "fs/promises";
import { parseFile } from "music-metadata";
import { getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "../utils/helper";
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
import { ipcLog } from "../logger";
import FastGlob from "fast-glob";
import { download } from "electron-dl";
/**
* 文件相关 IPC
*/
const initFileIpc = (): void => {
// 默认文件夹
ipcMain.handle(
"get-default-dir",
(_event, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => {
return app.getPath(type);
},
);
// 遍历音乐文件
ipcMain.handle("get-music-files", async (_, dirPath: string) => {
try {
// 规范化路径
const filePath = resolve(dirPath).replace(/\\/g, "/");
console.info(`📂 Fetching music files from: ${filePath}`);
// 查找指定目录下的所有音乐文件
const musicFiles = await FastGlob("**/*.{mp3,wav,flac,aac,webm}", { cwd: filePath });
// 解析元信息
const metadataPromises = musicFiles.map(async (file) => {
const filePath = join(dirPath, file);
// 处理元信息
const { common, format } = await parseFile(filePath);
// 获取文件大小
const { size } = await stat(filePath);
// 判断音质等级
let quality: string;
if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) {
quality = "Hi-Res";
} else if ((format.sampleRate || 0) >= 44100) {
quality = "HQ";
} else {
quality = "SQ";
}
return {
id: getFileID(filePath),
name: common.title || basename(filePath),
artists: common.artists?.[0] || common.artist,
album: common.album || "",
alia: common.comment?.[0]?.text || "",
duration: (format?.duration ?? 0) * 1000,
size: (size / (1024 * 1024)).toFixed(2),
path: filePath,
quality,
};
});
const metadataArray = await Promise.all(metadataPromises);
return metadataArray;
} catch (error) {
ipcLog.error("❌ Error fetching music metadata:", error);
throw error;
}
});
// 获取音乐元信息
ipcMain.handle("get-music-metadata", async (_, path: string) => {
try {
const filePath = resolve(path).replace(/\\/g, "/");
const { common, format } = await parseFile(filePath);
return {
// 文件名称
fileName: basename(filePath),
// 文件大小
fileSize: (await stat(filePath)).size / (1024 * 1024),
// 元信息
common,
// 歌词
lyric:
metaDataLyricsArrayToLrc(common?.lyrics?.[0]?.syncText || []) ||
common?.lyrics?.[0]?.text ||
"",
// 音质信息
format,
// md5
md5: await getFileMD5(filePath),
};
} catch (error) {
ipcLog.error("❌ Error fetching music metadata:", error);
throw error;
}
});
// 修改音乐元信息
ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => {
try {
const { name, artist, album, alia, lyric, cover } = metadata;
// 规范化路径
const songPath = resolve(path);
const coverPath = cover ? resolve(cover) : null;
// 读取歌曲文件
const songFile = File.createFromPath(songPath);
// 读取封面文件
const songCover = coverPath ? Picture.fromPath(coverPath) : null;
// 保存元数据
Id3v2Settings.forceDefaultVersion = true;
Id3v2Settings.defaultVersion = 3;
songFile.tag.title = name || "未知曲目";
songFile.tag.performers = [artist || "未知艺术家"];
songFile.tag.album = album || "未知专辑";
songFile.tag.albumArtists = [artist || "未知艺术家"];
songFile.tag.lyrics = lyric || "";
songFile.tag.description = alia || "";
songFile.tag.comment = alia || "";
if (songCover) songFile.tag.pictures = [songCover];
// 保存元信息
songFile.save();
songFile.dispose();
return true;
} catch (error) {
ipcLog.error("❌ Error setting music metadata:", error);
throw error;
}
});
// 获取音乐歌词
ipcMain.handle(
"get-music-lyric",
async (
_,
path: string,
): Promise<{
lyric: string;
format: "lrc" | "ttml";
}> => {
try {
const filePath = resolve(path).replace(/\\/g, "/");
const { common } = await parseFile(filePath);
// 尝试获取同名的歌词文件
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, "");
for (const ext of ["ttml", "lrc"] as const) {
const lyricPath = `${filePathWithoutExt}.${ext}`;
ipcLog.info("lyricPath", lyricPath);
try {
await access(lyricPath);
const lyric = await readFile(lyricPath, "utf-8");
if (lyric && lyric != "") return { lyric, format: ext };
} catch {
/* empty */
}
}
// 尝试获取元数据
const lyric = common?.lyrics?.[0]?.syncText;
if (lyric && lyric.length > 0) {
return { lyric: metaDataLyricsArrayToLrc(lyric), format: "lrc" };
} else if (common?.lyrics?.[0]?.text) {
return { lyric: common?.lyrics?.[0]?.text, format: "lrc" };
}
// 没有歌词
return { lyric: "", format: "lrc" };
} catch (error) {
ipcLog.error("❌ Error fetching music lyric:", error);
throw error;
}
},
);
// 获取音乐封面
ipcMain.handle(
"get-music-cover",
async (_, path: string): Promise<{ data: Buffer; format: string } | null> => {
try {
const { common } = await parseFile(path);
// 获取封面数据
const picture = common.picture?.[0];
if (picture) {
return { data: Buffer.from(picture.data), format: picture.format };
} else {
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
try {
await access(coverFilePath);
const coverData = await readFile(coverFilePath);
return { data: coverData, format: "image/jpeg" };
} catch {
return null;
}
}
} catch (error) {
console.error("❌ Error fetching music cover:", error);
throw error;
}
},
);
// 读取本地歌词
ipcMain.handle(
"read-local-lyric",
async (_, lyricDir: string, id: number, ext: string): Promise<string> => {
const pattern = `**/{,*.}${id}.${ext}`;
try {
const files = await FastGlob(pattern, { cwd: lyricDir });
if (files.length > 0) {
const firstMatch = join(lyricDir, files[0]);
await access(firstMatch);
const lyric = await readFile(firstMatch, "utf-8");
if (lyric) return lyric;
}
} catch {
/* empty */
}
return "";
},
);
// 删除文件
ipcMain.handle("delete-file", async (_, path: string) => {
try {
// 规范化路径
const resolvedPath = resolve(path);
// 检查文件是否存在
try {
await access(resolvedPath);
} catch {
throw new Error("❌ File not found");
}
// 删除文件
await unlink(resolvedPath);
return true;
} catch (error) {
ipcLog.error("❌ File delete error", error);
return false;
}
});
// 打开文件夹
ipcMain.on("open-folder", async (_, path: string) => {
try {
// 规范化路径
const resolvedPath = resolve(path);
// 检查文件夹是否存在
try {
await access(resolvedPath);
} catch {
throw new Error("❌ Folder not found");
}
// 打开文件夹
shell.showItemInFolder(resolvedPath);
} catch (error) {
ipcLog.error("❌ Folder open error", error);
throw error;
}
});
// 图片选择窗口
ipcMain.handle("choose-image", async () => {
try {
const { filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }],
});
if (!filePaths || filePaths.length === 0) return null;
return filePaths[0];
} catch (error) {
ipcLog.error("❌ Image choose error", error);
return null;
}
});
// 路径选择窗口
ipcMain.handle("choose-path", async () => {
try {
const { filePaths } = await dialog.showOpenDialog({
title: "选择文件夹",
defaultPath: app.getPath("downloads"),
properties: ["openDirectory", "createDirectory"],
buttonLabel: "选择文件夹",
});
if (!filePaths || filePaths.length === 0) return null;
return filePaths[0];
} catch (error) {
ipcLog.error("❌ Path choose error", error);
return null;
}
});
// 下载文件
ipcMain.handle(
"download-file",
async (
event,
url: string,
options: {
fileName: string;
fileType: string;
path: string;
downloadMeta?: boolean;
downloadCover?: boolean;
downloadLyric?: boolean;
saveMetaFile?: boolean;
lyric?: string;
songData?: any;
} = {
fileName: "未知文件名",
fileType: "mp3",
path: app.getPath("downloads"),
},
): Promise<boolean> => {
try {
// 获取窗口
const win = BrowserWindow.fromWebContents(event.sender);
if (!win) return false;
// 获取配置
const {
fileName,
fileType,
path,
lyric,
downloadMeta,
downloadCover,
downloadLyric,
saveMetaFile,
songData,
} = options;
// 规范化路径
const downloadPath = resolve(path);
// 检查文件夹是否存在
try {
await access(downloadPath);
} catch {
throw new Error("❌ Folder not found");
}
// 下载文件
const songDownload = await download(win, url, {
directory: downloadPath,
filename: `${fileName}.${fileType}`,
});
if (!downloadMeta || !songData?.cover) return true;
// 下载封面
const coverUrl = songData?.coverSize?.l || songData.cover;
const coverDownload = await download(win, coverUrl, {
directory: downloadPath,
filename: `${fileName}.jpg`,
});
// 读取歌曲文件
const songFile = File.createFromPath(songDownload.getSavePath());
// 生成图片信息
const songCover = Picture.fromPath(coverDownload.getSavePath());
// 保存修改后的元数据
Id3v2Settings.forceDefaultVersion = true;
Id3v2Settings.defaultVersion = 3;
songFile.tag.title = songData?.name || "未知曲目";
songFile.tag.album = songData?.album?.name || "未知专辑";
songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
if (lyric && downloadLyric) songFile.tag.lyrics = lyric;
if (songCover && downloadCover) songFile.tag.pictures = [songCover];
// 保存元信息
songFile.save();
songFile.dispose();
// 创建同名歌词文件
if (lyric && saveMetaFile && downloadLyric) {
const lrcPath = join(downloadPath, `${fileName}.lrc`);
await writeFile(lrcPath, lyric, "utf-8");
}
// 是否删除封面
if (!saveMetaFile || !downloadCover) await unlink(coverDownload.getSavePath());
return true;
} catch (error) {
ipcLog.error("❌ Error downloading file:", error);
return false;
}
},
);
// 检查是否是子文件夹
ipcMain.handle("check-if-subfolder", (_, localFilesPath: string[], selectedDir: string) => {
const resolvedSelectedDir = resolve(selectedDir);
const allPaths = localFilesPath.map((p) => resolve(p));
return allPaths.some((existingPath) => {
const relativePath = relative(existingPath, resolvedSelectedDir);
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath);
});
});
};
export default initFileIpc;

View File

@@ -0,0 +1,201 @@
import { BrowserWindow, ipcMain, screen } from "electron";
import { useStore } from "../store";
import lyricWindow from "../windows/lyric-window";
import mainWindow from "../windows/main-window";
/**
* 歌词相关 IPC
*/
const initLyricIpc = (): void => {
const store = useStore();
// 歌词窗口
let lyricWin: BrowserWindow | null = null;
/**
* 窗口是否存活
* @param win 窗口实例
* @returns 是否存活
*/
const isWinAlive = (win: BrowserWindow | null): win is BrowserWindow =>
!!win && !win.isDestroyed();
// 切换桌面歌词
ipcMain.on("toggle-desktop-lyric", (_event, val: boolean) => {
if (val) {
if (!isWinAlive(lyricWin)) {
lyricWin = lyricWindow.create();
// 监听关闭,置空引用,防止后续调用报错
lyricWin?.on("closed", () => {
lyricWin = null;
});
// 设置位置
const { x, y } = store.get("lyric");
const xPos = Number(x);
const yPos = Number(y);
if (Number.isFinite(xPos) && Number.isFinite(yPos)) {
lyricWin?.setPosition(Math.round(xPos), Math.round(yPos));
}
} else {
lyricWin.show();
}
if (isWinAlive(lyricWin)) {
lyricWin.setAlwaysOnTop(true, "screen-saver");
}
} else {
// 关闭:不销毁窗口,直接隐藏,保留位置与状态
if (!isWinAlive(lyricWin)) return;
lyricWin.hide();
}
});
// 更新歌词窗口数据
ipcMain.on("update-desktop-lyric-data", (_, lyricData) => {
if (!lyricData || !isWinAlive(lyricWin)) return;
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
});
// 更新歌词窗口配置
ipcMain.on("update-desktop-lyric-option", (_, option, callback: boolean = false) => {
const mainWin = mainWindow.getWin();
if (!option || !isWinAlive(lyricWin)) return;
// 增量更新
const prevOption = store.get("lyric.config");
if (prevOption) {
option = { ...prevOption, ...option };
}
store.set("lyric.config", option);
// 触发窗口更新
if (callback && isWinAlive(lyricWin)) {
lyricWin.webContents.send("update-desktop-lyric-option", option);
}
if (isWinAlive(mainWin)) {
mainWin?.webContents.send("update-desktop-lyric-option", option);
}
});
// 播放状态更改
ipcMain.on("play-status-change", (_, status) => {
if (!isWinAlive(lyricWin)) return;
lyricWin.webContents.send("update-desktop-lyric-data", { playStatus: status });
});
// 音乐名称更改
ipcMain.on("play-song-change", (_, title) => {
if (!title || !isWinAlive(lyricWin)) return;
lyricWin.webContents.send("update-desktop-lyric-data", { playName: title });
});
// 音乐歌词更改
ipcMain.on("play-lyric-change", (_, lyricData) => {
if (!lyricData || !isWinAlive(lyricWin)) return;
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
});
// 获取窗口位置
ipcMain.handle("get-window-bounds", () => {
if (!isWinAlive(lyricWin)) return {};
return lyricWin.getBounds();
});
// 获取屏幕尺寸
ipcMain.handle("get-screen-size", () => {
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
return { width, height };
});
// 获取多屏虚拟边界(支持负坐标)
ipcMain.handle("get-virtual-screen-bounds", () => {
const displays = screen.getAllDisplays();
const bounds = displays.map((d) => d.workArea);
const minX = Math.min(...bounds.map((b) => b.x));
const minY = Math.min(...bounds.map((b) => b.y));
const maxX = Math.max(...bounds.map((b) => b.x + b.width));
const maxY = Math.max(...bounds.map((b) => b.y + b.height));
return { minX, minY, maxX, maxY };
});
// 移动窗口
ipcMain.on("move-window", (_, x, y, width, height) => {
if (!isWinAlive(lyricWin)) return;
lyricWin.setBounds({ x, y, width, height });
// 保存配置
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
});
// 更新歌词窗口宽高
ipcMain.on("update-lyric-size", (_, width, height) => {
if (!isWinAlive(lyricWin)) return;
// 更新窗口宽度
lyricWin.setBounds({ width, height });
store.set("lyric", { ...store.get("lyric"), width, height });
});
// 更新高度
ipcMain.on("update-window-height", (_, height) => {
if (!isWinAlive(lyricWin)) return;
const store = useStore();
const { width } = lyricWin.getBounds();
// 更新窗口高度
lyricWin.setBounds({ width, height });
store.set("lyric", { ...store.get("lyric"), height });
});
// 是否固定当前最大宽高
ipcMain.on(
"toggle-fixed-max-size",
(_, options: { width: number; height: number; fixed: boolean }) => {
if (!isWinAlive(lyricWin)) return;
const { width, height, fixed } = options;
if (fixed) {
lyricWin.setMaximumSize(width, height);
} else {
lyricWin.setMaximumSize(1400, 360);
}
},
);
// 请求歌词数据
ipcMain.on("request-desktop-lyric-data", () => {
const mainWin = mainWindow.getWin();
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
// 触发窗口更新
mainWin?.webContents.send("request-desktop-lyric-data");
});
// 请求歌词配置
ipcMain.handle("request-desktop-lyric-option", () => {
const config = store.get("lyric.config");
if (isWinAlive(lyricWin)) {
lyricWin.webContents.send("update-desktop-lyric-option", config);
}
return config;
});
// 关闭桌面歌词
ipcMain.on("closeDesktopLyric", () => {
const mainWin = mainWindow.getWin();
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
lyricWin.hide();
mainWin?.webContents.send("closeDesktopLyric");
});
// 锁定/解锁桌面歌词
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean, isTemp: boolean = false) => {
const mainWin = mainWindow.getWin();
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
// 是否穿透
if (isLock) {
lyricWin.setIgnoreMouseEvents(true, { forward: true });
} else {
lyricWin.setIgnoreMouseEvents(false);
}
if (isTemp) return;
store.set("lyric.config", { ...store.get("lyric.config"), isLock });
// 触发窗口更新
const config = store.get("lyric.config");
mainWin?.webContents.send("update-desktop-lyric-option", config);
});
};
export default initLyricIpc;

View File

@@ -0,0 +1,36 @@
import { ipcMain } from "electron";
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "../shortcut";
import mainWindow from "../windows/main-window";
/**
* 初始化快捷键 IPC 主进程
* @returns void
*/
const initShortcutIpc = (): void => {
// 快捷键是否被注册
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
// 注册快捷键
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
const mainWin = mainWindow.getWin();
if (!mainWin || !allShortcuts) return false;
// 卸载所有快捷键
unregisterShortcuts();
// 注册快捷键
const failedShortcuts: string[] = [];
for (const key in allShortcuts) {
const shortcut = allShortcuts[key].globalShortcut;
if (!shortcut) continue;
// 快捷键回调
const callback = () => mainWin.webContents.send(key);
const isSuccess = registerShortcut(shortcut, callback);
if (!isSuccess) failedShortcuts.push(shortcut);
}
return failedShortcuts;
});
// 卸载所有快捷键
ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts());
};
export default initShortcutIpc;

View File

@@ -0,0 +1,45 @@
import { ipcMain } from "electron";
import { useStore } from "../store";
import type { StoreType } from "../store";
/**
* 初始化 store IPC 主进程
*/
const initStoreIpc = (): void => {
const store = useStore();
if (!store) return;
// 获取配置项
ipcMain.handle("store-get", (_event, key: keyof StoreType) => {
return store.get(key as any);
});
// 设置配置项
ipcMain.handle("store-set", (_event, key: keyof StoreType, value: unknown) => {
store.set(key as any, value as any);
return true;
});
// 判断配置项是否存在
ipcMain.handle("store-has", (_event, key: keyof StoreType) => {
return store.has(key as any);
});
// 删除配置项
ipcMain.handle("store-delete", (_event, key: keyof StoreType) => {
store.delete(key as any);
return true;
});
// 重置配置(支持指定 keys 或全部重置)
ipcMain.handle("store-reset", (_event, keys?: (keyof StoreType)[]) => {
if (keys && keys.length > 0) {
store.reset(...(keys as any));
} else {
store.reset();
}
return true;
});
};
export default initStoreIpc;

View File

@@ -0,0 +1,105 @@
import { app, ipcMain, net, powerSaveBlocker, session } from "electron";
import { ipcLog } from "../logger";
import { getFonts } from "font-list";
import { useStore } from "../store";
import mainWindow from "../windows/main-window";
/**
* 初始化系统 IPC 通信
* @returns void
*/
const initSystemIpc = (): void => {
const store = useStore();
/** 阻止系统息屏 ID */
let preventId: number | null = null;
// 是否阻止系统息屏
ipcMain.on("prevent-sleep", (_event, val: boolean) => {
if (val) {
preventId = powerSaveBlocker.start("prevent-display-sleep");
ipcLog.info("⏾ System sleep prevention started");
} else {
if (preventId !== null) {
powerSaveBlocker.stop(preventId);
ipcLog.info("✅ System sleep prevention stopped");
}
}
});
// 退出应用
ipcMain.on("quit-app", () => {
app.exit(0);
app.quit();
});
// 获取系统全部字体
ipcMain.handle("get-all-fonts", async () => {
try {
const fonts = await getFonts();
return fonts;
} catch (error) {
ipcLog.error(`❌ Failed to get all system fonts: ${error}`);
return [];
}
});
// 取消代理
ipcMain.on("remove-proxy", () => {
const mainWin = mainWindow.getWin();
store.set("proxy", "");
if (mainWin) {
mainWin?.webContents.session.setProxy({ proxyRules: "" });
}
ipcLog.info("✅ Remove proxy successfully");
});
// 配置网络代理
ipcMain.on("set-proxy", (_, config) => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
store.set("proxy", proxyRules);
mainWin?.webContents.session.setProxy({ proxyRules });
ipcLog.info("✅ Set proxy successfully:", proxyRules);
});
// 代理测试
ipcMain.handle("test-proxy", async (_, config) => {
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
try {
// 设置代理
const ses = session.defaultSession;
await ses.setProxy({ proxyRules });
// 测试请求
const request = net.request({ url: "https://www.baidu.com" });
return new Promise((resolve) => {
request.on("response", (response) => {
if (response.statusCode === 200) {
ipcLog.info("✅ Proxy test successful");
resolve(true);
} else {
ipcLog.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
resolve(false);
}
});
request.on("error", (error) => {
ipcLog.error("❌ Error testing proxy:", error);
resolve(false);
});
request.end();
});
} catch (error) {
ipcLog.error("❌ Error testing proxy:", error);
return false;
}
});
// 重置全部设置
ipcMain.on("reset-setting", () => {
store.reset();
ipcLog.info("✅ Reset setting successfully");
});
};
export default initSystemIpc;

View File

@@ -0,0 +1,15 @@
import { ipcMain } from "electron";
import { getThumbar } from "../thumbar";
const initThumbarIpc = (): void => {
// 更新工具栏
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
const thumbar = getThumbar();
if (!thumbar) {
return;
}
thumbar.updateThumbar(playStatus);
});
};
export default initThumbarIpc;

View File

@@ -0,0 +1,48 @@
import { ipcMain } from "electron";
import { getMainTray } from "../tray";
import lyricWindow from "../windows/lyric-window";
/**
* 托盘 IPC
*/
const initTrayIpc = (): void => {
const tray = getMainTray();
// 音乐播放状态更改
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
const lyricWin = lyricWindow.getWin();
tray?.setPlayState(playStatus ? "play" : "pause");
if (!lyricWin) return;
lyricWin.webContents.send("play-status-change", playStatus);
});
// 音乐名称更改
ipcMain.on("play-song-change", (_, title) => {
if (!title) return;
// 更改标题
tray?.setTitle(title);
tray?.setPlayName(title);
});
// 播放模式切换
ipcMain.on("play-mode-change", (_, mode) => {
tray?.setPlayMode(mode);
});
// 喜欢状态切换
ipcMain.on("like-status-change", (_, likeStatus: boolean) => {
tray?.setLikeState(likeStatus);
});
// 桌面歌词开关
ipcMain.on("toggle-desktop-lyric", (_, val: boolean) => {
tray?.setDesktopLyricShow(val);
});
// 锁定/解锁桌面歌词
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
tray?.setDesktopLyricLock(isLock);
});
};
export default initTrayIpc;

View File

@@ -0,0 +1,17 @@
import { ipcMain } from "electron";
import { checkUpdate, startDownloadUpdate } from "../update";
import mainWindow from "../windows/main-window";
const initUpdateIpc = () => {
// 检查更新
ipcMain.on("check-update", (_event, showTip) => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
checkUpdate(mainWin, showTip);
});
// 开始下载更新
ipcMain.on("start-download-update", () => startDownloadUpdate());
};
export default initUpdateIpc;

View File

@@ -0,0 +1,152 @@
import { app, ipcMain } from "electron";
import { useStore } from "../store";
import { isDev } from "../utils/config";
import { initThumbar } from "../thumbar";
import mainWindow from "../windows/main-window";
import loadWindow from "../windows/load-window";
import loginWindow from "../windows/login-window";
/**
* 窗口 IPC 通信
* @returns void
*/
const initWindowsIpc = (): void => {
// store
const store = useStore();
// 当前窗口状态
ipcMain.on("win-state", (event) => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
event.returnValue = mainWin?.isMaximized();
});
// 加载完成
ipcMain.on("win-loaded", () => {
const loadWin = loadWindow.getWin();
const mainWin = mainWindow.getWin();
if (loadWin && !loadWin.isDestroyed()) loadWin.destroy();
const isMaximized = store.get("window")?.maximized;
if (isMaximized) mainWin?.maximize();
if (!mainWin) return;
mainWin?.show();
mainWin?.focus();
// 解决窗口不立即显示
mainWin?.setAlwaysOnTop(true);
// 100ms 后取消置顶
const timer = setTimeout(() => {
if (mainWin && !mainWin.isDestroyed()) {
mainWin.setAlwaysOnTop(false);
mainWin.focus();
clearTimeout(timer);
}
}, 100);
// 初始化缩略图工具栏
if (mainWin) initThumbar(mainWin);
});
// 最小化
ipcMain.on("win-min", (event) => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
event.preventDefault();
mainWin?.minimize();
});
// 最大化
ipcMain.on("win-max", () => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
mainWin?.maximize();
});
// 还原
ipcMain.on("win-restore", () => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
mainWin?.restore();
});
// 隐藏
ipcMain.on("win-hide", () => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
mainWin?.hide();
});
// 显示
ipcMain.on("win-show", () => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
mainWin?.show();
mainWin?.focus();
});
// 重启
ipcMain.on("win-reload", () => {
app.quit();
app.relaunch();
});
// 向主窗口发送事件
ipcMain.on("send-to-mainWin", (_, eventName, ...args) => {
const mainWin = mainWindow.getWin();
if (!mainWin || mainWin.isDestroyed() || mainWin.webContents.isDestroyed()) return;
mainWin.webContents.send(eventName, ...args);
});
// 显示进度
ipcMain.on("set-bar", (_event, val: number | "none" | "indeterminate" | "error" | "paused") => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
switch (val) {
case "none":
mainWin?.setProgressBar(-1);
break;
case "indeterminate":
mainWin?.setProgressBar(2, { mode: "indeterminate" });
break;
case "error":
mainWin?.setProgressBar(1, { mode: "error" });
break;
case "paused":
mainWin?.setProgressBar(1, { mode: "paused" });
break;
default:
if (typeof val === "number") {
mainWin?.setProgressBar(val / 100);
} else {
mainWin?.setProgressBar(-1);
}
break;
}
});
// 开启控制台
ipcMain.on("open-dev-tools", () => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
mainWin?.webContents.openDevTools({
title: "SPlayer DevTools",
mode: isDev ? "right" : "detach",
});
});
// 开启登录窗口
ipcMain.on("open-login-web", () => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
loginWindow.create(mainWin);
});
// 开启设置
ipcMain.on("open-setting", (_, type) => {
const mainWin = mainWindow.getWin();
if (!mainWin) return;
mainWin?.show();
mainWin?.focus();
mainWin?.webContents.send("openSetting", type);
});
};
export default initWindowsIpc;

View File

@@ -1,711 +0,0 @@
import {
app,
ipcMain,
BrowserWindow,
powerSaveBlocker,
screen,
shell,
dialog,
net,
session,
} from "electron";
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
import { parseFile } from "music-metadata";
import { getFonts } from "font-list";
import { MainTray } from "./tray";
import { Thumbar } from "./thumbar";
import { StoreType } from "./store";
import { isDev, getFileID, getFileMD5 } from "./utils";
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "./shortcut";
import { join, basename, resolve } from "path";
import { download } from "electron-dl";
import { checkUpdate, startDownloadUpdate } from "./update";
import fs from "fs/promises";
import log from "../main/logger";
import Store from "electron-store";
import fg from "fast-glob";
import openLoginWin from "./loginWin";
// 注册 ipcMain
const initIpcMain = (
win: BrowserWindow | null,
lyricWin: BrowserWindow | null,
loadingWin: BrowserWindow | null,
tray: MainTray | null,
thumbar: Thumbar | null,
store: Store<StoreType>,
) => {
initWinIpcMain(win, loadingWin, lyricWin, store);
initLyricIpcMain(lyricWin, win, store);
initTrayIpcMain(tray, win, lyricWin);
initThumbarIpcMain(thumbar);
initStoreIpcMain(store);
initOtherIpcMain(win);
};
// win
const initWinIpcMain = (
win: BrowserWindow | null,
loadingWin: BrowserWindow | null,
lyricWin: BrowserWindow | null,
store: Store<StoreType>,
) => {
let preventId: number | null = null;
// 当前窗口状态
ipcMain.on("win-state", (ev) => {
ev.returnValue = win?.isMaximized();
});
// 加载完成
ipcMain.on("win-loaded", () => {
if (loadingWin && !loadingWin.isDestroyed()) loadingWin.close();
win?.show();
win?.focus();
});
// 最小化
ipcMain.on("win-min", (ev) => {
ev.preventDefault();
win?.minimize();
});
// 最大化
ipcMain.on("win-max", () => {
win?.maximize();
});
// 还原
ipcMain.on("win-restore", () => {
win?.restore();
});
// 关闭
ipcMain.on("win-close", (ev) => {
ev.preventDefault();
win?.close();
app.quit();
});
// 隐藏
ipcMain.on("win-hide", () => {
win?.hide();
});
// 显示
ipcMain.on("win-show", () => {
win?.show();
});
// 重启
ipcMain.on("win-reload", () => {
app.quit();
app.relaunch();
});
// 显示进度
ipcMain.on("set-bar", (_, val: number | "none" | "indeterminate" | "error" | "paused") => {
switch (val) {
case "none":
win?.setProgressBar(-1);
break;
case "indeterminate":
win?.setProgressBar(2, { mode: "indeterminate" });
break;
case "error":
win?.setProgressBar(1, { mode: "error" });
break;
case "paused":
win?.setProgressBar(1, { mode: "paused" });
break;
default:
if (typeof val === "number") {
win?.setProgressBar(val / 100);
} else {
win?.setProgressBar(-1);
}
break;
}
});
// 开启控制台
ipcMain.on("open-dev-tools", () => {
win?.webContents.openDevTools({
title: "SPlayer DevTools",
mode: isDev ? "right" : "detach",
});
});
// 获取系统全部字体
ipcMain.handle("get-all-fonts", async () => {
try {
const fonts = await getFonts();
return fonts;
} catch (error) {
log.error(`❌ Failed to get all system fonts: ${error}`);
return [];
}
});
// 切换桌面歌词
ipcMain.on("change-desktop-lyric", (_, val: boolean) => {
if (val) {
lyricWin?.show();
lyricWin?.setAlwaysOnTop(true, "screen-saver");
} else lyricWin?.hide();
});
// 是否阻止系统息屏
ipcMain.on("prevent-sleep", (_, val: boolean) => {
if (val) {
preventId = powerSaveBlocker.start("prevent-display-sleep");
log.info("⏾ System sleep prevention started");
} else {
if (preventId !== null) {
powerSaveBlocker.stop(preventId);
log.info("✅ System sleep prevention stopped");
}
}
});
// 默认文件夹
ipcMain.handle(
"get-default-dir",
(_, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => {
return app.getPath(type);
},
);
// 遍历音乐文件
ipcMain.handle("get-music-files", async (_, dirPath: string) => {
try {
// 查找指定目录下的所有音乐文件
const musicFiles = await fg("**/*.{mp3,wav,flac}", { cwd: dirPath });
// 解析元信息
const metadataPromises = musicFiles.map(async (file) => {
const filePath = join(dirPath, file);
// 处理元信息
const { common, format } = await parseFile(filePath);
// 获取文件大小
const { size } = await fs.stat(filePath);
// 判断音质等级
let quality: string;
if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) {
quality = "Hi-Res";
} else if ((format.sampleRate || 0) >= 44100) {
quality = "HQ";
} else {
quality = "SQ";
}
return {
id: getFileID(filePath),
name: common.title || basename(filePath),
artists: common.artists?.[0] || common.artist,
album: common.album || "",
alia: common.comment?.[0],
duration: (format?.duration ?? 0) * 1000,
size: (size / (1024 * 1024)).toFixed(2),
path: filePath,
quality,
};
});
const metadataArray = await Promise.all(metadataPromises);
return metadataArray;
} catch (error) {
log.error("❌ Error fetching music metadata:", error);
throw error;
}
});
// 获取音乐元信息
ipcMain.handle("get-music-metadata", async (_, path: string) => {
try {
const { common, format } = await parseFile(path);
return {
// 文件名称
fileName: basename(path),
// 文件大小
fileSize: (await fs.stat(path)).size / (1024 * 1024),
// 元信息
common,
// 音质信息
format,
// md5
md5: await getFileMD5(path),
};
} catch (error) {
log.error("❌ Error fetching music metadata:", error);
throw error;
}
});
// 获取音乐歌词
ipcMain.handle("get-music-lyric", async (_, path: string): Promise<string> => {
try {
const { common, native } = await parseFile(path);
const lyric = common?.lyrics;
if (lyric && lyric.length > 0) return String(lyric[0]);
else {
// 尝试读取 UNSYNCEDLYRICS
const nativeTags = native["ID3v2.3"] || native["ID3v2.4"];
const usltTag = nativeTags?.find((tag) => tag.id === "USLT");
if (usltTag) return String(usltTag.value.text);
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
else {
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
try {
await fs.access(lrcFilePath);
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
return lrcData || "";
} catch {
return "";
}
}
}
} catch (error) {
log.error("❌ Error fetching music lyric:", error);
throw error;
}
});
// 获取音乐封面
ipcMain.handle(
"get-music-cover",
async (_, path: string): Promise<{ data: Buffer; format: string } | null> => {
try {
const { common } = await parseFile(path);
// 获取封面数据
const picture = common.picture?.[0];
if (picture) {
return { data: Buffer.from(picture.data), format: picture.format };
} else {
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
try {
await fs.access(coverFilePath);
const coverData = await fs.readFile(coverFilePath);
return { data: coverData, format: "image/jpeg" };
} catch {
return null;
}
}
} catch (error) {
console.error("❌ Error fetching music cover:", error);
throw error;
}
},
);
// 删除文件
ipcMain.handle("delete-file", async (_, path: string) => {
try {
// 规范化路径
const resolvedPath = resolve(path);
// 检查文件是否存在
try {
await fs.access(resolvedPath);
} catch {
throw new Error("❌ File not found");
}
// 删除文件
await fs.unlink(resolvedPath);
return true;
} catch (error) {
log.error("❌ File delete error", error);
return false;
}
});
// 打开文件夹
ipcMain.on("open-folder", async (_, path: string) => {
try {
// 规范化路径
const resolvedPath = resolve(path);
// 检查文件夹是否存在
try {
await fs.access(resolvedPath);
} catch {
throw new Error("❌ Folder not found");
}
// 打开文件夹
shell.showItemInFolder(resolvedPath);
} catch (error) {
log.error("❌ Folder open error", error);
throw error;
}
});
// 图片选择窗口
ipcMain.handle("choose-image", async () => {
try {
const { filePaths } = await dialog.showOpenDialog({
properties: ["openFile"],
filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }],
});
if (!filePaths || filePaths.length === 0) return null;
return filePaths[0];
} catch (error) {
log.error("❌ Image choose error", error);
return null;
}
});
// 路径选择窗口
ipcMain.handle("choose-path", async () => {
try {
const { filePaths } = await dialog.showOpenDialog({
title: "选择文件夹",
defaultPath: app.getPath("downloads"),
properties: ["openDirectory", "createDirectory"],
buttonLabel: "选择文件夹",
});
if (!filePaths || filePaths.length === 0) return null;
return filePaths[0];
} catch (error) {
log.error("❌ Path choose error", error);
return null;
}
});
// 修改音乐元信息
ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => {
try {
const { name, artist, album, alia, lyric, cover } = metadata;
// 规范化路径
const songPath = resolve(path);
const coverPath = cover ? resolve(cover) : null;
// 读取歌曲文件
const songFile = File.createFromPath(songPath);
// 读取封面文件
const songCover = coverPath ? Picture.fromPath(coverPath) : null;
// 保存元数据
Id3v2Settings.forceDefaultVersion = true;
Id3v2Settings.defaultVersion = 3;
songFile.tag.title = name || "未知曲目";
songFile.tag.performers = [artist || "未知艺术家"];
songFile.tag.album = album || "未知专辑";
songFile.tag.albumArtists = [artist || "未知艺术家"];
songFile.tag.lyrics = lyric || "";
songFile.tag.description = alia || "";
songFile.tag.comment = alia || "";
if (songCover) songFile.tag.pictures = [songCover];
// 保存元信息
songFile.save();
songFile.dispose();
return true;
} catch (error) {
log.error("❌ Error setting music metadata:", error);
throw error;
}
});
// 下载文件
ipcMain.handle(
"download-file",
async (
_,
url: string,
options: {
fileName: string;
fileType: string;
path: string;
downloadMeta?: boolean;
downloadCover?: boolean;
downloadLyric?: boolean;
saveMetaFile?: boolean;
lyric?: string;
songData?: any;
} = {
fileName: "未知文件名",
fileType: "mp3",
path: app.getPath("downloads"),
},
): Promise<boolean> => {
try {
if (!win) return false;
// 获取配置
const {
fileName,
fileType,
path,
lyric,
downloadMeta,
downloadCover,
downloadLyric,
saveMetaFile,
songData,
} = options;
// 规范化路径
const downloadPath = resolve(path);
// 检查文件夹是否存在
try {
await fs.access(downloadPath);
} catch {
throw new Error("❌ Folder not found");
}
// 下载文件
const songDownload = await download(win, url, {
directory: downloadPath,
filename: `${fileName}.${fileType}`,
});
if (!downloadMeta || !songData?.cover) return true;
// 下载封面
const coverUrl = songData?.coverSize?.l || songData.cover;
const coverDownload = await download(win, coverUrl, {
directory: downloadPath,
filename: `${fileName}.jpg`,
});
// 读取歌曲文件
const songFile = File.createFromPath(songDownload.getSavePath());
// 生成图片信息
const songCover = Picture.fromPath(coverDownload.getSavePath());
// 保存修改后的元数据
Id3v2Settings.forceDefaultVersion = true;
Id3v2Settings.defaultVersion = 3;
songFile.tag.title = songData?.name || "未知曲目";
songFile.tag.album = songData?.album?.name || "未知专辑";
songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
if (lyric && downloadLyric) songFile.tag.lyrics = lyric;
if (songCover && downloadCover) songFile.tag.pictures = [songCover];
// 保存元信息
songFile.save();
songFile.dispose();
// 创建同名歌词文件
if (lyric && saveMetaFile && downloadLyric) {
const lrcPath = join(downloadPath, `${fileName}.lrc`);
await fs.writeFile(lrcPath, lyric, "utf-8");
}
// 是否删除封面
if (!saveMetaFile || !downloadCover) await fs.unlink(coverDownload.getSavePath());
return true;
} catch (error) {
log.error("❌ Error downloading file:", error);
return false;
}
},
);
// 取消代理
ipcMain.on("remove-proxy", () => {
store.set("proxy", "");
win?.webContents.session.setProxy({ proxyRules: "" });
log.info("✅ Remove proxy successfully");
});
// 配置网络代理
ipcMain.on("set-proxy", (_, config) => {
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
store.set("proxy", proxyRules);
win?.webContents.session.setProxy({ proxyRules });
log.info("✅ Set proxy successfully:", proxyRules);
});
// 代理测试
ipcMain.handle("test-proxy", async (_, config) => {
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
try {
// 设置代理
const ses = session.defaultSession;
await ses.setProxy({ proxyRules });
// 测试请求
const request = net.request({ url: "https://www.baidu.com" });
return new Promise((resolve) => {
request.on("response", (response) => {
if (response.statusCode === 200) {
log.info("✅ Proxy test successful");
resolve(true);
} else {
log.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
resolve(false);
}
});
request.on("error", (error) => {
log.error("❌ Error testing proxy:", error);
resolve(false);
});
request.end();
});
} catch (error) {
log.error("❌ Error testing proxy:", error);
return false;
}
});
// 重置全部设置
ipcMain.on("reset-setting", () => {
store.reset();
log.info("✅ Reset setting successfully");
});
// 检查更新
ipcMain.on("check-update", (_, showTip) => checkUpdate(win!, showTip));
// 开始下载更新
ipcMain.on("start-download-update", () => startDownloadUpdate());
// 新建窗口
ipcMain.on("open-login-web", () => openLoginWin(win!));
};
// lyric
const initLyricIpcMain = (
lyricWin: BrowserWindow | null,
mainWin: BrowserWindow | null,
store: Store<StoreType>,
): void => {
// 音乐名称更改
ipcMain.on("play-song-change", (_, title) => {
if (!title) return;
lyricWin?.webContents.send("play-song-change", title);
});
// 音乐歌词更改
ipcMain.on("play-lyric-change", (_, lyricData) => {
if (!lyricData) return;
lyricWin?.webContents.send("play-lyric-change", lyricData);
});
// 获取窗口位置
ipcMain.handle("get-window-bounds", () => {
return lyricWin?.getBounds();
});
// 获取屏幕尺寸
ipcMain.handle("get-screen-size", () => {
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
return { width, height };
});
// 移动窗口
ipcMain.on("move-window", (_, x, y, width, height) => {
lyricWin?.setBounds({ x, y, width, height });
// 保存配置
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
// 保持置顶
lyricWin?.setAlwaysOnTop(true, "screen-saver");
});
// 更新高度
ipcMain.on("update-window-height", (_, height) => {
if (!lyricWin) return;
const { width } = lyricWin.getBounds();
// 更新窗口高度
lyricWin.setBounds({ width, height });
});
// 获取配置
ipcMain.handle("get-desktop-lyric-option", () => {
return store.get("lyric");
});
// 保存配置
ipcMain.on("set-desktop-lyric-option", (_, option, callback: boolean = false) => {
store.set("lyric", option);
// 触发窗口更新
if (callback && lyricWin) {
lyricWin.webContents.send("desktop-lyric-option-change", option);
}
mainWin?.webContents.send("desktop-lyric-option-change", option);
});
// 发送主程序事件
ipcMain.on("send-main-event", (_, name, val) => {
mainWin?.webContents.send(name, val);
});
// 关闭桌面歌词
ipcMain.on("closeDesktopLyric", () => {
lyricWin?.hide();
mainWin?.webContents.send("closeDesktopLyric");
});
// 锁定/解锁桌面歌词
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
if (!lyricWin) return;
// 是否穿透
if (isLock) {
lyricWin.setIgnoreMouseEvents(true, { forward: true });
} else {
lyricWin.setIgnoreMouseEvents(false);
}
});
};
// tray
const initTrayIpcMain = (
tray: MainTray | null,
win: BrowserWindow | null,
lyricWin: BrowserWindow | null,
): void => {
// 音乐播放状态更改
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
tray?.setPlayState(playStatus ? "play" : "pause");
lyricWin?.webContents.send("play-status-change", playStatus);
});
// 音乐名称更改
ipcMain.on("play-song-change", (_, title) => {
if (!title) return;
// 更改标题
win?.setTitle(title);
tray?.setTitle(title);
tray?.setPlayName(title);
});
// 播放模式切换
ipcMain.on("play-mode-change", (_, mode) => {
tray?.setPlayMode(mode);
});
// 喜欢状态切换
ipcMain.on("like-status-change", (_, likeStatus: boolean) => {
tray?.setLikeState(likeStatus);
});
// 桌面歌词开关
ipcMain.on("change-desktop-lyric", (_, val: boolean) => {
tray?.setDesktopLyricShow(val);
});
// 锁定/解锁桌面歌词
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
tray?.setDesktopLyricLock(isLock);
});
};
// thumbar
const initThumbarIpcMain = (thumbar: Thumbar | null): void => {
if (!thumbar) return;
// 更新工具栏
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
thumbar?.updateThumbar(playStatus);
});
};
// store
const initStoreIpcMain = (store: Store<StoreType>): void => {
if (!store) return;
};
// other
const initOtherIpcMain = (mainWin: BrowserWindow | null): void => {
// 快捷键是否被注册
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
// 注册快捷键
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
if (!mainWin || !allShortcuts) return false;
// 卸载所有快捷键
unregisterShortcuts();
// 注册快捷键
const failedShortcuts: string[] = [];
for (const key in allShortcuts) {
const shortcut = allShortcuts[key].globalShortcut;
if (!shortcut) continue;
// 快捷键回调
const callback = () => mainWin.webContents.send(key);
const isSuccess = registerShortcut(shortcut, callback);
if (!isSuccess) failedShortcuts.push(shortcut);
}
return failedShortcuts;
});
// 卸载所有快捷键
ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts());
};
export default initIpcMain;

View File

@@ -1,31 +0,0 @@
// 日志输出
import { join } from "path";
import { app } from "electron";
import { isDev } from "./utils";
import log from "electron-log";
// 绑定事件
Object.assign(console, log.functions);
// 日志配置
log.transports.file.level = "info";
log.transports.file.maxSize = 2 * 1024 * 1024; // 2M
if (log.transports.ipc) log.transports.ipc.level = false;
// 控制台输出
log.transports.console.useStyles = true;
// 文件输出
log.transports.file.format = "{y}-{m}-{d} {h}:{i}:{s}:{ms} {text}";
// 本地输出
if (!isDev) {
log.transports.file.resolvePathFn = () =>
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
} else {
log.transports.file.level = false;
}
log.info("📃 logger initialized");
export default log;

View File

@@ -0,0 +1,47 @@
// 日志输出
import { existsSync, mkdirSync } from "fs";
import { join } from "path";
import { app } from "electron";
import log from "electron-log";
// 日志文件路径
const logDir = join(app.getPath("logs"));
// 是否存在日志目录
if (!existsSync(logDir)) mkdirSync(logDir);
// 获取日期 - YYYY-MM-DD
const dateString = new Date().toISOString().slice(0, 10);
const logFilePath = join(logDir, `${dateString}.log`);
// 配置日志系统
log.transports.console.useStyles = true; // 颜色输出
log.transports.file.level = "info"; // 仅记录 info 及以上级别
log.transports.file.resolvePathFn = (): string => logFilePath; // 日志文件路径
log.transports.file.maxSize = 2 * 1024 * 1024; // 文件最大 2MB
// 日志格式化
// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}";
// 绑定默认事件
const defaultLog = log.scope("default");
console.log = defaultLog.log;
console.info = defaultLog.info;
console.warn = defaultLog.warn;
console.error = defaultLog.error;
// 分作用域导出
export { defaultLog };
export const ipcLog = log.scope("ipc");
export const trayLog = log.scope("tray");
export const thumbarLog = log.scope("thumbar");
export const storeLog = log.scope("store");
export const updateLog = log.scope("update");
export const systemLog = log.scope("system");
export const configLog = log.scope("config");
export const windowsLog = log.scope("windows");
export const processLog = log.scope("process");
export const preloadLog = log.scope("preload");
export const rendererLog = log.scope("renderer");
export const shortcutLog = log.scope("shortcut");
export const serverLog = log.scope("server");

View File

@@ -1,123 +0,0 @@
import {
BrowserWindow,
MenuItemConstructorOptions,
Menu,
session,
dialog,
ipcMain,
} from "electron";
import icon from "../../public/icons/favicon.png?asset";
const openLoginWin = (mainWin: BrowserWindow) => {
const loginSession = session.fromPartition("login-win");
const loginWin = new BrowserWindow({
parent: mainWin,
title: "登录网易云音乐",
width: 1280,
height: 800,
center: true,
modal: true,
icon,
// resizable: false,
// movable: false,
// minimizable: false,
// maximizable: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
partition: "login-win",
},
});
// 打开网易云
loginWin.loadURL("https://music.163.com/#/my/");
// 阻止新窗口创建
loginWin.webContents.setWindowOpenHandler(() => {
return { action: "deny" };
});
// 登录完成
const loginFinish = async () => {
if (!loginWin) return;
// 获取 Cookie
const cookies = await loginWin.webContents.session.cookies.get({ name: "MUSIC_U" });
if (!cookies?.[0]?.value) {
dialog.showMessageBox({
type: "info",
title: "登录失败",
message: "未查找到登录信息,请重试",
});
return;
}
const value = `MUSIC_U=${cookies[0].value};`;
// 发送回主进程
mainWin?.webContents.send("send-cookies", value);
await loginSession?.clearStorageData();
loginWin.close();
};
// 页面注入
// loginWin.webContents.once("did-finish-load", () => {
// const script = `
// const style = document.createElement('style');
// style.innerHTML = \`
// .login-btn {
// position: fixed;
// left: 0;
// bottom: 0;
// width: 100%;
// height: 80px;
// display: flex;
// align-items: center;
// justify-content: center;
// background-color: #242424;
// z-index: 99999;
// }
// .login-btn span {
// color: white;
// margin-right: 20px;
// }
// .login-btn button {
// border: none;
// outline: none;
// background-color: #c20c0c;
// border-radius: 25px;
// color: white;
// height: 40px;
// padding: 0 20px;
// cursor: pointer;
// }
// \`;
// document.head.appendChild(style);
// const div = document.createElement('div');
// div.className = 'login-btn';
// div.innerHTML = \`
// <span>请在登录成功后点击</span>
// <button>登录完成</button>
// \`;
// div.querySelector('button').addEventListener('click', () => {
// window.electron.ipcRenderer.send("login-success");
// });
// document.body.appendChild(div);
// `;
// loginWin.webContents.executeJavaScript(script);
// });
// 监听事件
ipcMain.on("login-success", loginFinish);
// 菜单栏
const menuTemplate: MenuItemConstructorOptions[] = [
{
label: "登录完成",
click: loginFinish,
},
];
const menu = Menu.buildFromTemplate(menuTemplate);
loginWin.setMenu(menu);
};
export default openLoginWin;

View File

@@ -1,43 +0,0 @@
import { BrowserWindow, globalShortcut } from "electron";
import { isDev } from "./utils";
import log from "../main/logger";
// 注册快捷键并检查
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
try {
const success = globalShortcut.register(shortcut, callback);
if (!success) {
log.error(`❌ Failed to register shortcut: ${shortcut}`);
return false;
} else {
log.info(`✅ Shortcut registered: ${shortcut}`);
return true;
}
} catch (error) {
log.error(` Error registering shortcut ${shortcut}:`, error);
return false;
}
};
// 检查快捷键是否被注册
export const isShortcutRegistered = (shortcut: string): boolean => {
return globalShortcut.isRegistered(shortcut);
};
// 卸载所有快捷键
export const unregisterShortcuts = () => {
globalShortcut.unregisterAll();
log.info("🚫 All shortcuts unregistered.");
};
// 注册所有快捷键
export const registerAllShortcuts = (win: BrowserWindow) => {
// 开启控制台
registerShortcut("CmdOrCtrl+Shift+I", () => {
win.webContents.openDevTools({
title: "SPlayer DevTools",
// 客户端分离
mode: isDev ? "right" : "detach",
});
});
};

View File

@@ -0,0 +1,30 @@
import { globalShortcut } from "electron";
import { shortcutLog } from "../logger";
// 注册快捷键并检查
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
try {
const success = globalShortcut.register(shortcut, callback);
if (!success) {
shortcutLog.error(`❌ Failed to register shortcut: ${shortcut}`);
return false;
} else {
shortcutLog.info(`✅ Shortcut registered: ${shortcut}`);
return true;
}
} catch (error) {
shortcutLog.error(` Error registering shortcut ${shortcut}:`, error);
return false;
}
};
// 检查快捷键是否被注册
export const isShortcutRegistered = (shortcut: string): boolean => {
return globalShortcut.isRegistered(shortcut);
};
// 卸载所有快捷键
export const unregisterShortcuts = () => {
globalShortcut.unregisterAll();
shortcutLog.info("🚫 All shortcuts unregistered.");
};

View File

@@ -1,47 +0,0 @@
import Store from "electron-store";
import { screen } from "electron";
import log from "./logger";
log.info("🌱 Store init");
export interface StoreType {
window: {
width: number;
height: number;
x?: number;
y?: number;
};
lyric: {
fontSize: number;
mainColor: string;
shadowColor: string;
// 窗口位置
x?: number;
y?: number;
width?: number;
height?: number;
};
proxy: string;
}
// 初始化仓库
export const initStore = () => {
return new Store<StoreType>({
defaults: {
window: {
width: 1280,
height: 800,
},
lyric: {
fontSize: 30,
mainColor: "#fff",
shadowColor: "rgba(0, 0, 0, 0.5)",
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
width: 800,
height: 180,
},
proxy: "",
},
});
};

View File

@@ -0,0 +1,52 @@
import { screen } from "electron";
import { storeLog } from "../logger";
import type { LyricConfig } from "../../../src/types/desktop-lyric";
import defaultLyricConfig from "../../../src/assets/data/lyricConfig";
import Store from "electron-store";
storeLog.info("🌱 Store init");
export interface StoreType {
window: {
width: number;
height: number;
x?: number;
y?: number;
maximized?: boolean;
};
lyric: {
// 窗口位置
x?: number;
y?: number;
width?: number;
height?: number;
// 配置
config?: LyricConfig;
};
proxy: string;
}
/**
* 使用 Store
* @returns Store<StoreType>
*/
export const useStore = () => {
// 获取主屏幕
const screenData = screen.getPrimaryDisplay();
return new Store<StoreType>({
defaults: {
window: {
width: 1280,
height: 800,
},
lyric: {
x: screenData.workAreaSize.width / 2 - 400,
y: screenData.workAreaSize.height - 90,
width: 800,
height: 136,
config: defaultLyricConfig,
},
proxy: "",
},
});
};

View File

@@ -1,7 +1,7 @@
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
import { join } from "path";
import { isWin } from "./utils";
import log from "./logger";
import { isWin } from "../utils/config";
import { thumbarLog } from "../logger";
enum ThumbarKeys {
Play = "play",
@@ -17,6 +17,9 @@ export interface Thumbar {
updateThumbar(playing: boolean, clean?: boolean): void;
}
// 缩略图单例
let thumbar: Thumbar | null = null;
// 工具栏图标
const thumbarIcon = (filename: string) => {
// 是否为暗色
@@ -63,6 +66,9 @@ class createThumbar implements Thumbar {
private _next: ThumbarButton;
private _play: ThumbarButton;
private _pause: ThumbarButton;
// 当前播放状态
private _isPlaying: boolean = false;
constructor(win: BrowserWindow) {
// 初始化数据
this._win = win;
@@ -74,26 +80,62 @@ class createThumbar implements Thumbar {
this._next = this._thumbar.get(ThumbarKeys.Next)!;
// 初始化工具栏
this.updateThumbar();
// 监听主题变化
this.initThemeListener();
}
// 初始化主题监听器
private initThemeListener() {
nativeTheme.on("updated", () => {
this.refreshThumbarButtons();
});
}
// 刷新工具栏按钮(主题变化时)
private refreshThumbarButtons() {
// 重新创建按钮
this._thumbar = createThumbarButtons(this._win);
this._play = this._thumbar.get(ThumbarKeys.Play)!;
this._pause = this._thumbar.get(ThumbarKeys.Pause)!;
this._prev = this._thumbar.get(ThumbarKeys.Prev)!;
this._next = this._thumbar.get(ThumbarKeys.Next)!;
// 更新工具栏
this.updateThumbar(this._isPlaying);
}
// 更新工具栏
updateThumbar(playing: boolean = false, clean: boolean = false) {
this._isPlaying = playing;
if (clean) return this.clearThumbar();
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
}
// 清除工具栏
clearThumbar() {
this._win.setThumbarButtons([]);
}
}
/**
*
* @param win
* @returns
*/
export const initThumbar = (win: BrowserWindow) => {
try {
// 若非 Win
if (!isWin) return null;
log.info("🚀 ThumbarButtons Startup");
return new createThumbar(win);
thumbarLog.info("🚀 ThumbarButtons Startup");
thumbar = new createThumbar(win);
return thumbar;
} catch (error) {
log.error("❌ ThumbarButtons Error", error);
thumbarLog.error("❌ ThumbarButtons Error", error);
throw error;
}
};
/**
*
* @returns
*/
export const getThumbar = () => thumbar;

View File

@@ -7,9 +7,11 @@ import {
nativeImage,
nativeTheme,
} from "electron";
import { isWin, isLinux, isDev, appName } from "./utils";
import { isWin, appName } from "../utils/config";
import { join } from "path";
import log from "./logger";
import { trayLog } from "../logger";
import { useStore } from "../store";
import lyricWindow from "../windows/lyric-window";
// 播放模式
type PlayMode = "repeat" | "repeat-once" | "shuffle";
@@ -34,6 +36,9 @@ export interface MainTray {
destroyTray(): void;
}
// 托盘单例
let mainTrayInstance: MainTray | null = null;
// 托盘图标
const trayIcon = (filename: string) => {
// const rootPath = isDev
@@ -44,10 +49,7 @@ const trayIcon = (filename: string) => {
};
// 托盘菜单
const createTrayMenu = (
win: BrowserWindow,
lyricWin: BrowserWindow,
): MenuItemConstructorOptions[] => {
const createTrayMenu = (win: BrowserWindow): MenuItemConstructorOptions[] => {
// 区分明暗图标
const showIcon = (iconName: string) => {
const isDark = nativeTheme.shouldUseDarkColors;
@@ -143,7 +145,16 @@ const createTrayMenu = (
label: `${desktopLyricLock ? "解锁" : "锁定"}桌面歌词`,
icon: showIcon(desktopLyricLock ? "lock" : "unlock"),
visible: desktopLyricShow,
click: () => lyricWin.webContents.send("toogleDesktopLyricLock", !desktopLyricLock),
click: () => {
const store = useStore();
// 更新锁定状态
store.set("lyric.config", { ...store.get("lyric.config"), isLock: !desktopLyricLock });
// 触发窗口更新
const config = store.get("lyric.config");
const lyricWin = lyricWindow.getWin();
if (!lyricWin) return;
lyricWin.webContents.send("update-desktop-lyric-option", config);
},
},
{
type: "separator",
@@ -166,8 +177,8 @@ const createTrayMenu = (
label: "退出",
icon: showIcon("power"),
click: () => {
win.close();
// app.exit(0);
// win.close();
app.exit(0);
app.quit();
},
},
@@ -179,14 +190,13 @@ const createTrayMenu = (
class CreateTray implements MainTray {
// 窗口
private _win: BrowserWindow;
private _lyricWin: BrowserWindow;
// 托盘
private _tray: Tray;
// 菜单
private _menu: MenuItemConstructorOptions[];
private _contextMenu: Menu;
constructor(win: BrowserWindow, lyricWin: BrowserWindow) {
constructor(win: BrowserWindow) {
// 托盘图标
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
height: 32,
@@ -194,9 +204,8 @@ class CreateTray implements MainTray {
});
// 初始化数据
this._win = win;
this._lyricWin = lyricWin;
this._tray = new Tray(icon);
this._menu = createTrayMenu(this._win, this._lyricWin);
this._menu = createTrayMenu(this._win);
this._contextMenu = Menu.buildFromTemplate(this._menu);
// 初始化事件
this.initTrayMenu();
@@ -205,7 +214,7 @@ class CreateTray implements MainTray {
}
// 托盘菜单
private initTrayMenu() {
this._menu = createTrayMenu(this._win, this._lyricWin);
this._menu = createTrayMenu(this._win);
this._contextMenu = Menu.buildFromTemplate(this._menu);
this._tray.setContextMenu(this._contextMenu);
}
@@ -219,11 +228,19 @@ class CreateTray implements MainTray {
});
}
// 设置标题
/**
*
* @param title
*/
setTitle(title: string) {
this._win.setTitle(title);
this._tray.setTitle(title);
this._tray.setToolTip(title);
}
// 设置播放名称
/**
*
* @param name
*/
setPlayName(name: string) {
// 超长处理
if (name.length > 20) name = name.slice(0, 20) + "...";
@@ -231,52 +248,80 @@ class CreateTray implements MainTray {
// 更新菜单
this.initTrayMenu();
}
// 设置播放状态
/**
*
* @param state
*/
setPlayState(state: PlayState) {
playState = state;
// 更新菜单
this.initTrayMenu();
}
// 设置播放模式
/**
*
* @param mode
*/
setPlayMode(mode: PlayMode) {
playMode = mode;
// 更新菜单
this.initTrayMenu();
}
// 设置喜欢状态
/**
*
* @param like
*/
setLikeState(like: boolean) {
likeSong = like;
// 更新菜单
this.initTrayMenu();
}
// 桌面歌词开关
/**
*
* @param show
*/
setDesktopLyricShow(show: boolean) {
desktopLyricShow = show;
// 更新菜单
this.initTrayMenu();
}
// 锁定桌面歌词
/**
*
* @param lock
*/
setDesktopLyricLock(lock: boolean) {
desktopLyricLock = lock;
// 更新菜单
this.initTrayMenu();
}
// 销毁托盘
/**
*
*/
destroyTray() {
this._tray.destroy();
}
}
export const initTray = (win: BrowserWindow, lyricWin: BrowserWindow) => {
/**
*
* @param win
* @param lyricWin
* @returns
*/
export const initTray = (win: BrowserWindow) => {
try {
// 若为 MacOS
if (isWin || isLinux || isDev) {
log.info("🚀 Tray Process Startup");
return new CreateTray(win, lyricWin);
}
return null;
trayLog.info("🚀 Tray Process Startup");
const tray = new CreateTray(win);
// 保存单例实例
mainTrayInstance = tray;
return tray;
} catch (error) {
log.error("❌ Tray Process Error", error);
trayLog.error("❌ Tray Process Error", error);
return null;
}
};
/**
*
* @returns
*/
export const getMainTray = (): MainTray | null => mainTrayInstance;

View File

@@ -1,10 +1,18 @@
import { type BrowserWindow } from "electron";
import { app, type BrowserWindow } from "electron";
import { updateLog } from "../logger";
import electronUpdater from "electron-updater";
import log from "./logger";
import { isDev } from "../utils/config";
// import
const { autoUpdater } = electronUpdater;
// 开发环境启用
if (isDev) {
Object.defineProperty(app, "isPackaged", {
get: () => true,
});
}
// 更新源
autoUpdater.setFeedURL({
provider: "github",
@@ -28,19 +36,19 @@ const initUpdaterListeners = (win: BrowserWindow) => {
// 当有新版本可用时
autoUpdater.on("update-available", (info) => {
win.webContents.send("update-available", info);
log.info(`🚀 New version available: ${info.version}`);
updateLog.info(`🚀 New version available: ${info.version}`);
});
// 更新下载进度
autoUpdater.on("download-progress", (progress) => {
win.webContents.send("download-progress", progress);
log.info(`🚀 Downloading: ${progress.percent}%`);
updateLog.info(`🚀 Downloading: ${progress.percent}%`);
});
// 当下载完成时
autoUpdater.on("update-downloaded", (info) => {
win.webContents.send("update-downloaded", info);
log.info(`🚀 Update downloaded: ${info.version}`);
updateLog.info(`🚀 Update downloaded: ${info.version}`);
// 安装更新
autoUpdater.quitAndInstall();
});
@@ -48,13 +56,13 @@ const initUpdaterListeners = (win: BrowserWindow) => {
// 当没有新版本时
autoUpdater.on("update-not-available", (info) => {
if (isShowTip) win.webContents.send("update-not-available", info);
log.info(`✅ No new version available: ${info.version}`);
updateLog.info(`✅ No new version available: ${info.version}`);
});
// 更新错误
autoUpdater.on("error", (err) => {
win.webContents.send("update-error", err);
log.error(`❌ Update error: ${err.message}`);
updateLog.error(`❌ Update error: ${err.message}`);
});
isInit = true;

View File

@@ -1,32 +0,0 @@
import { app } from "electron";
import { is } from "@electron-toolkit/utils";
import fs from "fs/promises";
import crypto from "crypto";
// 系统判断
export const isDev = is.dev;
export const isWin = process.platform === "win32";
export const isMac = process.platform === "darwin";
export const isLinux = process.platform === "linux";
// 程序名称
export const appName = app.getName() || "SPlayer";
// 生成唯一ID
export const getFileID = (filePath: string): number => {
// SHA-256
const hash = crypto.createHash("sha256");
hash.update(filePath);
const digest = hash.digest("hex");
// 将哈希值的前 16 位转换为十进制数字
const uniqueId = parseInt(digest.substring(0, 16), 16);
return Number(uniqueId.toString().padStart(16, "0"));
};
// 生成文件 MD5
export const getFileMD5 = async (path: string): Promise<string> => {
const data = await fs.readFile(path);
const hash = crypto.createHash("md5");
hash.update(data);
return hash.digest("hex");
};

View File

@@ -0,0 +1,60 @@
import { is } from "@electron-toolkit/utils";
import { app } from "electron";
/**
* 是否为开发环境
* @returns boolean
*/
export const isDev = is.dev;
/** 是否为 Windows 系统 */
export const isWin = process.platform === "win32";
/** 是否为 macOS 系统 */
export const isMac = process.platform === "darwin";
/** 是否为 Linux 系统 */
export const isLinux = process.platform === "linux";
/**
* 软件版本
* @returns string
*/
export const appVersion = app.getVersion();
/**
* 程序名称
* @returns string
*/
export const appName = app.getName() || "SPlayer";
/**
* 服务器端口
* @returns number
*/
export const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
/**
* 主窗口加载地址
* @returns string
*/
export const mainWinUrl =
isDev && process.env["ELECTRON_RENDERER_URL"]
? process.env["ELECTRON_RENDERER_URL"]
: `http://localhost:${port}`;
/**
* 歌词窗口加载地址
* @returns string
*/
export const lyricWinUrl =
isDev && process.env["ELECTRON_RENDERER_URL"]
? `${process.env["ELECTRON_RENDERER_URL"]}/#/desktop-lyric`
: `http://localhost:${port}/#/desktop-lyric`;
/**
* 加载窗口地址
* @returns string
*/
export const loadWinUrl =
isDev && process.env["ELECTRON_RENDERER_URL"]
? `${process.env["ELECTRON_RENDERER_URL"]}/web/loading/index.html`
: `http://localhost:${port}/web/loading/index.html`;

View File

@@ -0,0 +1,58 @@
import { createHash } from "crypto";
import { readFile } from "fs/promises";
/**
* 生成文件唯一ID
* @param filePath 文件路径
* @returns 唯一ID
*/
export const getFileID = (filePath: string): number => {
// SHA-256
const hash = createHash("sha256");
hash.update(filePath);
const digest = hash.digest("hex");
// 将哈希值的前 16 位转换为十进制数字
const uniqueId = parseInt(digest.substring(0, 16), 16);
return Number(uniqueId.toString().padStart(16, "0"));
};
/**
* 生成文件 MD5
* @param path 文件路径
* @returns MD5值
*/
export const getFileMD5 = async (path: string): Promise<string> => {
const data = await readFile(path);
const hash = createHash("md5");
hash.update(data);
return hash.digest("hex");
};
/**
* 将 music-metadata 库中的歌词数组转换为LRC格式字符串
* @param lyrics 歌词数组,每个元素包含时间戳(毫秒)和歌词文本
* @returns LRC格式的字符串
*/
export const metaDataLyricsArrayToLrc = (
lyrics: {
text: string;
timestamp?: number;
}[],
): string => {
return lyrics
.map(({ timestamp, text }) => {
if (!timestamp) return "";
const totalSeconds = Math.floor(timestamp / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const centiseconds = Math.floor((timestamp % 1000) / 10);
// 格式化为两位数字
const mm = String(minutes).padStart(2, "0");
const ss = String(seconds).padStart(2, "0");
const cs = String(centiseconds).padStart(2, "0");
return `[${mm}:${ss}.${cs}]${text}`;
})
.join("\n");
};

View File

@@ -0,0 +1,25 @@
import { app } from "electron";
import { systemLog } from "../logger";
import mainWindow from "../windows/main-window";
/**
* 初始化单实例锁
* @returns 如果当前实例获得了锁,返回 true否则返回 false
*/
export const initSingleLock = (): boolean => {
const gotTheLock = app.requestSingleInstanceLock();
// 如果未获得锁,退出当前实例
if (!gotTheLock) {
app.quit();
systemLog.warn("❌ 已有一个实例正在运行");
return false;
}
// 当第二个实例启动时触发
else {
app.on("second-instance", () => {
systemLog.warn("❌ 第二个实例将要启动");
mainWindow.getWin()?.show();
});
}
return true;
};

View File

@@ -0,0 +1,43 @@
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import { windowsLog } from "../logger";
import { appName } from "../utils/config";
import { join } from "path";
import icon from "../../../public/icons/favicon.png?asset";
export const createWindow = (
options: BrowserWindowConstructorOptions = {},
): BrowserWindow | null => {
try {
const defaultOptions: BrowserWindowConstructorOptions = {
title: appName,
width: 1280,
height: 720,
frame: false, // 是否显示窗口边框
center: true, // 窗口居中
icon, // 窗口图标
autoHideMenuBar: true, // 隐藏菜单栏
webPreferences: {
preload: join(__dirname, "../preload/index.mjs"),
// 禁用渲染器沙盒
sandbox: false,
// 禁用同源策略
webSecurity: false,
// 允许 HTTP
allowRunningInsecureContent: true,
// 禁用拼写检查
spellcheck: false,
// 启用 Node.js
nodeIntegration: true,
nodeIntegrationInWorker: true,
},
};
// 合并参数
options = Object.assign(defaultOptions, options);
// 创建窗口
const win = new BrowserWindow(options);
return win;
} catch (error) {
windowsLog.error(error);
return null;
}
};

View File

@@ -0,0 +1,60 @@
import { BrowserWindow } from "electron";
import { createWindow } from "./index";
import { loadWinUrl } from "../utils/config";
class LoadWindow {
private win: BrowserWindow | null = null;
private winURL: string;
constructor() {
this.winURL = loadWinUrl;
}
/**
* 主窗口事件
* @returns void
*/
private event(): void {
if (!this.win) return;
// 准备好显示
this.win.on("ready-to-show", () => {
this.win?.show();
});
}
/**
* 创建窗口
* @returns BrowserWindow | null
*/
create(): BrowserWindow | null {
this.win = createWindow({
width: 800,
height: 560,
maxWidth: 800,
maxHeight: 560,
resizable: false,
alwaysOnTop: true,
// 不在任务栏显示
skipTaskbar: true,
// 窗口不能最小化
minimizable: false,
// 窗口不能最大化
maximizable: false,
// 窗口不能进入全屏状态
fullscreenable: false,
show: false,
});
if (!this.win) return null;
// 加载地址
this.win.loadURL(this.winURL);
// 窗口事件
this.event();
return this.win;
}
/**
* 获取窗口
* @returns BrowserWindow | null
*/
getWin(): BrowserWindow | null {
return this.win;
}
}
export default new LoadWindow();

View File

@@ -0,0 +1,99 @@
import { app, BrowserWindow, session } from "electron";
import { createWindow } from "./index";
import { join } from "path";
class LoginWindow {
private win: BrowserWindow | null = null;
private loginTimer: NodeJS.Timeout | null = null;
private loginSession: Electron.Session | null = null;
constructor() {}
private getLoginSession(): Electron.Session {
if (!this.loginSession) {
this.loginSession = session.fromPartition("persist:login");
}
return this.loginSession;
}
// 事件绑定
private event(mainWin: BrowserWindow): void {
if (!this.win) return;
// 阻止新窗口创建
this.win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
// 加载完成后显示并开始轮询登录状态
this.win.webContents.once("did-finish-load", () => {
this.win?.show();
this.loginTimer = setInterval(() => this.checkLogin(mainWin), 1000);
this.win?.on("closed", () => {
if (this.loginTimer) clearInterval(this.loginTimer);
this.loginTimer = null;
});
});
}
// 检查是否已登录
private async checkLogin(mainWin: BrowserWindow) {
if (!this.win) return;
try {
this.win.webContents.executeJavaScript(
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 '",
);
// 判断 MUSIC_U
const MUSIC_U = await this.getLoginSession().cookies.get({ name: "MUSIC_U" });
if (MUSIC_U && MUSIC_U.length > 0) {
if (this.loginTimer) clearInterval(this.loginTimer);
this.loginTimer = null;
const value = `MUSIC_U=${MUSIC_U[0].value};`;
// 发送回主进程
mainWin?.webContents.send("send-cookies", value);
this.win.destroy();
this.win = null;
}
} catch (error) {
console.error(error);
}
}
// 创建登录窗口
async create(mainWin: BrowserWindow): Promise<BrowserWindow | null> {
await app.whenReady();
const loginSession = this.getLoginSession();
// 清理登录会话存储
await loginSession.clearStorageData({
storages: ["cookies", "localstorage"],
});
this.win = createWindow({
parent: mainWin,
title: "登录网易云音乐( 若遇到无响应请关闭后重试 ",
width: 1280,
height: 800,
center: true,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, "../preload/index.mjs"),
sandbox: false,
webSecurity: false,
allowRunningInsecureContent: true,
spellcheck: false,
nodeIntegration: true,
nodeIntegrationInWorker: true,
session: loginSession,
},
});
if (!this.win) return null;
// 加载登录地址
this.win.loadURL("https://music.163.com/#/login/");
// 绑定事件
this.event(mainWin);
return this.win;
}
// 获取窗口
getWin(): BrowserWindow | null {
return this.win;
}
}
export default new LoginWindow();

View File

@@ -0,0 +1,89 @@
import { BrowserWindow } from "electron";
import { createWindow } from "./index";
import { useStore } from "../store";
import { lyricWinUrl } from "../utils/config";
import mainWindow from "./main-window";
class LyricWindow {
private win: BrowserWindow | null = null;
constructor() {}
/**
* 主窗口事件
* @returns void
*/
private event(): void {
if (!this.win) return;
// 准备好显示
this.win.on("ready-to-show", () => {
this.win?.show();
});
// 歌词窗口缩放
this.win?.on("resized", () => {
const store = useStore();
const bounds = this.win?.getBounds();
if (bounds) {
const { width, height } = bounds;
store.set("lyric", { ...store.get("lyric"), width, height });
}
});
// 歌词窗口关闭
this.win?.on("close", () => {
this.win = null;
const mainWin = mainWindow?.getWin();
if (mainWin) {
mainWin?.webContents.send("closeDesktopLyric");
}
});
}
/**
* 创建主窗口
* @returns BrowserWindow | null
*/
create(): BrowserWindow | null {
const store = useStore();
const { width, height, x, y } = store.get("lyric");
this.win = createWindow({
width: width || 800,
height: height || 180,
minWidth: 640,
minHeight: 140,
maxWidth: 1400,
maxHeight: 360,
// 没有指定位置时居中显示
center: !(x && y),
// 窗口位置
x,
y,
transparent: true,
backgroundColor: "rgba(0, 0, 0, 0)",
alwaysOnTop: true,
resizable: true,
movable: true,
show: false,
// 不在任务栏显示
skipTaskbar: true,
// 窗口不能最小化
minimizable: false,
// 窗口不能最大化
maximizable: false,
// 窗口不能进入全屏状态
fullscreenable: false,
});
if (!this.win) return null;
// 加载地址
this.win.loadURL(lyricWinUrl);
// 窗口事件
this.event();
return this.win;
}
/**
* 获取窗口
* @returns BrowserWindow | null
*/
getWin(): BrowserWindow | null {
if (this.win && !this.win?.isDestroyed()) return this.win;
return null;
}
}
export default new LyricWindow();

View File

@@ -0,0 +1,134 @@
import { BrowserWindow, shell } from "electron";
import { createWindow } from "./index";
import { mainWinUrl } from "../utils/config";
import { useStore } from "../store";
import { isLinux } from "../utils/config";
class MainWindow {
private win: BrowserWindow | null = null;
private winURL: string;
constructor() {
this.winURL = mainWinUrl;
}
/**
* 保存窗口大小和状态
*/
private saveBounds() {
if (this.win?.isFullScreen()) return;
const store = useStore();
const bounds = this.win?.getBounds();
if (bounds) {
const maximized = this.win?.isMaximized();
store.set("window", { ...bounds, maximized });
}
}
/**
* 主窗口事件
* @returns void
*/
private event(): void {
if (!this.win) return;
const store = useStore();
// 配置网络代理
if (store.get("proxy")) {
this.win.webContents.session.setProxy({ proxyRules: store.get("proxy") });
}
// 窗口打开处理程序
this.win.webContents.setWindowOpenHandler((details) => {
const { url } = details;
if (url.startsWith("https://") || url.startsWith("http://")) {
shell.openExternal(url);
}
return { action: "deny" };
});
// 窗口显示时
this.win?.on("show", () => {
this.win?.webContents.send("lyricsScroll");
});
// 窗口获得焦点时
this.win?.on("focus", () => {
this.saveBounds();
});
// 窗口大小改变时
this.win?.on("resized", () => {
// 若处于全屏则不保存
if (this.win?.isFullScreen()) return;
this.saveBounds();
});
// 窗口位置改变时
this.win?.on("moved", () => {
this.saveBounds();
});
// 窗口最大化时
this.win?.on("maximize", () => {
this.saveBounds();
this.win?.webContents.send("win-state-change", true);
});
// 窗口取消最大化时
this.win?.on("unmaximize", () => {
this.saveBounds();
this.win?.webContents.send("win-state-change", false);
});
// Linux 无法使用 resized 和 moved
if (isLinux) {
this.win?.on("resize", () => {
// 若处于全屏则不保存
if (this.win?.isFullScreen()) return;
this.saveBounds();
});
this.win?.on("move", () => {
this.saveBounds();
});
}
// 窗口关闭
this.win?.on("close", (event) => {
event.preventDefault();
this.win?.hide();
});
}
/**
* 创建窗口
* @returns BrowserWindow | null
*/
create(): BrowserWindow | null {
const store = useStore();
const { width, height } = store.get("window");
this.win = createWindow({
// 菜单栏
titleBarStyle: "customButtonsOnHover",
width,
height,
minHeight: 600,
minWidth: 800,
show: false,
});
if (!this.win) return null;
// 加载地址
this.win.loadURL(this.winURL);
// 窗口事件
this.event();
return this.win;
}
/**
* 获取窗口
* @returns BrowserWindow | null
*/
getWin(): BrowserWindow | null {
if (this.win && !this.win.isDestroyed()) {
return this.win;
}
return null;
}
/**
* 显示主窗口
*/
showWindow() {
if (this.win) {
this.win.show();
if (this.win.isMinimized()) this.win.restore();
this.win.focus();
}
}
}
export default new MainWindow();

View File

@@ -1,8 +1,17 @@
import { ElectronAPI } from "@electron-toolkit/preload";
import type { StoreType } from "../main/store";
declare global {
interface Window {
electron: ElectronAPI;
api: unknown;
api: {
store: {
get<K extends keyof StoreType>(key: K): Promise<StoreType[K]>;
set<K extends keyof StoreType>(key: K, value: StoreType[K]): Promise<boolean>;
has(key: keyof StoreType): Promise<boolean>;
delete(key: keyof StoreType): Promise<boolean>;
reset(keys?: (keyof StoreType)[]): Promise<boolean>;
};
};
}
}

View File

@@ -1,4 +1,4 @@
import { contextBridge } from "electron";
import { contextBridge, ipcRenderer } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
// Use `contextBridge` APIs to expose Electron APIs to
@@ -7,10 +7,17 @@ import { electronAPI } from "@electron-toolkit/preload";
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld("electron", electronAPI);
// Expose store API via preload
contextBridge.exposeInMainWorld("api", {
store: {
get: (key: string) => ipcRenderer.invoke("store-get", key),
set: (key: string, value: unknown) => ipcRenderer.invoke("store-set", key, value),
has: (key: string) => ipcRenderer.invoke("store-has", key),
delete: (key: string) => ipcRenderer.invoke("store-delete", key),
reset: (keys?: string[]) => ipcRenderer.invoke("store-reset", keys),
},
});
} catch (error) {
console.error(error);
}
} else {
// @ts-expect-error (define in dts)
window.electron = electronAPI;
}

View File

@@ -0,0 +1,187 @@
import { FastifyInstance } from "fastify";
import mainWindow from "../../main/windows/main-window";
/**
* 播放控制接口
* @param fastify Fastify 实例
*/
export const initControlAPI = async (fastify: FastifyInstance) => {
// 播放控制路由前缀
await fastify.register(
async (fastify) => {
// 播放
fastify.get("/play", async (_request, reply) => {
try {
const mainWin = mainWindow.getWin();
if (!mainWin) {
return reply.code(500).send({
code: 500,
message: "主窗口未找到",
data: null,
});
}
mainWin.webContents.send("play");
return reply.send({
code: 200,
message: "播放命令已发送",
data: null,
});
} catch (error) {
return reply.code(500).send({
code: 500,
message: "播放失败",
data: error,
});
}
});
// 暂停
fastify.get("/pause", async (_request, reply) => {
try {
const mainWin = mainWindow.getWin();
if (!mainWin) {
return reply.code(500).send({
code: 500,
message: "主窗口未找到",
data: null,
});
}
mainWin.webContents.send("pause");
return reply.send({
code: 200,
message: "暂停命令已发送",
data: null,
});
} catch (error) {
return reply.code(500).send({
code: 500,
message: "暂停失败",
data: error,
});
}
});
// 播放/暂停切换
fastify.get("/toggle", async (_request, reply) => {
try {
const mainWin = mainWindow.getWin();
if (!mainWin) {
return reply.code(500).send({
code: 500,
message: "主窗口未找到",
data: null,
});
}
// 这里可以根据当前播放状态来决定发送 play 还是 pause
// 暂时先发送 toggle 事件,如果渲染进程支持的话
mainWin.webContents.send("toggle");
return reply.send({
code: 200,
message: "播放/暂停切换命令已发送",
data: null,
});
} catch (error) {
return reply.code(500).send({
code: 500,
message: "播放/暂停切换失败",
data: error,
});
}
});
// 下一曲
fastify.get("/next", async (_request, reply) => {
try {
const mainWin = mainWindow.getWin();
if (!mainWin) {
return reply.code(500).send({
code: 500,
message: "主窗口未找到",
data: null,
});
}
mainWin.webContents.send("playNext");
return reply.send({
code: 200,
message: "下一曲命令已发送",
data: null,
});
} catch (error) {
return reply.code(500).send({
code: 500,
message: "下一曲失败",
data: error,
});
}
});
// 上一曲
fastify.get("/prev", async (_request, reply) => {
try {
const mainWin = mainWindow.getWin();
if (!mainWin) {
return reply.code(500).send({
code: 500,
message: "主窗口未找到",
data: null,
});
}
mainWin.webContents.send("playPrev");
return reply.send({
code: 200,
message: "上一曲命令已发送",
data: null,
});
} catch (error) {
return reply.code(500).send({
code: 500,
message: "上一曲失败",
data: error,
});
}
});
// 获取播放状态(可选功能)
fastify.get("/status", async (_request, reply) => {
try {
const mainWin = mainWindow.getWin();
if (!mainWin) {
return reply.code(500).send({
code: 500,
message: "主窗口未找到",
data: null,
});
}
// 这里可以通过 IPC 获取当前播放状态
// 暂时返回基本信息
return reply.send({
code: 200,
message: "获取状态成功",
data: {
connected: true,
window: "available",
},
});
} catch (error) {
return reply.code(500).send({
code: 500,
message: "获取状态失败",
data: error,
});
}
});
},
{ prefix: "/control" },
);
};

View File

@@ -1,25 +1,28 @@
import { join } from "path";
import { isDev } from "../main/utils";
import initNcmAPI from "./netease";
import initUnblockAPI from "./unblock";
import { isDev } from "../main/utils/config";
import { serverLog } from "../main/logger";
import { initNcmAPI } from "./netease";
import { initUnblockAPI } from "./unblock";
import { initControlAPI } from "./control";
import fastifyCookie from "@fastify/cookie";
import fastifyMultipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static";
import fastify from "fastify";
import log from "../main/logger";
const initAppServer = async () => {
try {
const server = fastify({
// 忽略尾随斜杠
ignoreTrailingSlash: true,
routerOptions: {
// 忽略尾随斜杠
ignoreTrailingSlash: true,
},
});
// 注册插件
server.register(fastifyCookie);
server.register(fastifyMultipart);
// 生产环境启用静态文件
if (!isDev) {
log.info("📂 Serving static files from /renderer");
serverLog.info("📂 Serving static files from /renderer");
server.register(fastifyStatic, {
root: join(__dirname, "../renderer"),
});
@@ -45,13 +48,14 @@ const initAppServer = async () => {
// 注册接口
server.register(initNcmAPI, { prefix: "/api" });
server.register(initUnblockAPI, { prefix: "/api" });
server.register(initControlAPI, { prefix: "/api" });
// 启动端口
const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
await server.listen({ port });
log.info(`🌐 Starting AppServer on port ${port}`);
serverLog.info(`🌐 Starting AppServer on port ${port}`);
return server;
} catch (error) {
log.error("🚫 AppServer failed to start");
serverLog.error("🚫 AppServer failed to start");
throw error;
}
};

View File

@@ -1,7 +1,7 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { pathCase } from "change-case";
import NeteaseCloudMusicApi from "NeteaseCloudMusicApi";
import log from "../../main/logger";
import { serverLog } from "../../main/logger";
import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api";
// 获取数据
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
@@ -9,7 +9,7 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => {
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply,
) => {
log.info("🌐 Request NcmAPI:", name);
serverLog.log("🌐 Request NcmAPI:", name);
// 获取 NcmAPI 数据
try {
const result = await neteaseApi({
@@ -19,7 +19,7 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => {
});
return reply.send(result.body);
} catch (error: any) {
log.error("❌ NcmAPI Error:", error);
serverLog.error("❌ NcmAPI Error:", error);
if ([400, 301].includes(error.status)) {
return reply.status(error.status).send(error.body);
}
@@ -29,16 +29,16 @@ const getHandler = (name: string, neteaseApi: (params: any) => any) => {
};
// 初始化 NcmAPI
const initNcmAPI = async (fastify: FastifyInstance) => {
export const initNcmAPI = async (fastify: FastifyInstance) => {
// 主信息
fastify.get("/netease", (_, reply) => {
reply.send({
name: "NeteaseCloudMusicApi",
version: "4.20.0",
description: "网易云音乐 Node.js API service",
author: "@binaryify",
name: "@neteaseapireborn/api",
version: "4.29.2",
description: "网易云音乐 API Enhanced",
author: "@MoeFurina",
license: "MIT",
url: "https://gitlab.com/Binaryify/neteasecloudmusicapi",
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
});
});
@@ -60,7 +60,28 @@ const initNcmAPI = async (fastify: FastifyInstance) => {
}
});
log.info("🌐 Register NcmAPI successfully");
};
// 获取 TTML 歌词
fastify.get(
"/netease/lyric/ttml",
async (req: FastifyRequest<{ Querystring: { id: string } }>, reply: FastifyReply) => {
const { id } = req.query;
if (!id) {
return reply.status(400).send({ error: "id is required" });
}
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
try {
const response = await fetch(url);
if (response.status !== 200) {
return reply.send(null);
}
const data = await response.text();
return reply.send(data);
} catch (error) {
serverLog.error("❌ TTML Lyric Fetch Error:", error);
return reply.send(null);
}
},
);
export default initNcmAPI;
serverLog.info("🌐 Register NcmAPI successfully");
};

View File

@@ -0,0 +1,161 @@
import { SongUrlResult } from "./unblock";
import { serverLog } from "../../main/logger";
import { createHash } from "crypto";
import axios from "axios";
/**
* 生成随机设备 ID
* @returns 随机设备 ID
*/
const getRandomDeviceId = () => {
const min = 0;
const max = 100000000000;
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
return randomNum.toString();
};
/** 随机设备 ID */
const deviceId = getRandomDeviceId();
/**
* 格式化歌曲信息
* @param song 歌曲信息
* @returns 格式化后的歌曲信息
*/
const format = (song: any) => ({
id: song.MUSICRID.split("_").pop(),
name: song.SONGNAME,
duration: song.DURATION * 1000,
album: { id: song.ALBUMID, name: song.ALBUM },
artists: song.ARTIST.split("&").map((name: any, index: any) => ({
id: index ? null : song.ARTISTID,
name,
})),
});
/**
* 生成签名
* @param str 请求字符串
* @returns 包含签名的请求字符串
*/
const generateSign = (str: string) => {
const url = new URL(str);
const currentTime = Date.now();
str += `&timestamp=${currentTime}`;
const filteredChars = str
.substring(str.indexOf("?") + 1)
.replace(/[^a-zA-Z0-9]/g, "")
.split("")
.sort();
const dataToEncrypt = `kuwotest${filteredChars.join("")}${url.pathname}`;
const md5 = createHash("md5").update(dataToEncrypt).digest("hex");
return `${str}&sign=${md5}`;
};
/**
* 搜索歌曲
* @param keyword 搜索关键词
* @returns 歌曲 ID 或 null
*/
const search = async (info: string): Promise<string | null> => {
try {
const keyword = encodeURIComponent(info.replace(" - ", " "));
const url =
"http://search.kuwo.cn/r.s?&correct=1&vipver=1&stype=comprehensive&encoding=utf8" +
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
keyword;
const result = await axios.get(url);
if (
!result.data ||
result.data.content.length < 2 ||
!result.data.content[1].musicpage ||
result.data.content[1].musicpage.abslist.length < 1
) {
return null;
}
// 获取歌曲信息
const list = result.data.content[1].musicpage.abslist.map(format);
if (list[0] && !list[0]?.id) return null;
return list[0].id;
} catch (error) {
serverLog.error("❌ Get BodianSongId Error:", error);
return null;
}
};
/**
* 发送广告免费请求
* @returns 包含广告免费响应的 Promise
*/
const sendAdFreeRequest = () => {
try {
const adurl =
"http://bd-api.kuwo.cn/api/service/advert/watch?uid=-1&token=&timestamp=1724306124436&sign=15a676d66285117ad714e8c8371691da";
const headers = {
"user-agent": "Dart/2.19 (dart:io)",
plat: "ar",
channel: "aliopen",
devid: deviceId,
ver: "3.9.0",
host: "bd-api.kuwo.cn",
qimei36: "1e9970cbcdc20a031dee9f37100017e1840e",
"content-type": "application/json; charset=utf-8",
};
const data = JSON.stringify({
type: 5,
subType: 5,
musicId: 0,
adToken: "",
});
return axios.post(adurl, data, { headers });
} catch (error) {
serverLog.error("❌ Get Bodian Ad Free Error:", error);
return null;
}
};
/**
* 获取波点音乐歌曲 URL
* @param keyword 搜索关键词
* @returns 包含歌曲 URL 的结果对象
*/
const getBodianSongUrl = async (keyword: string): Promise<SongUrlResult> => {
try {
if (!keyword) return { code: 404, url: null };
const songId = await search(keyword);
if (!songId) return { code: 404, url: null };
// 请求地址
const headers = {
"user-agent": "Dart/2.19 (dart:io)",
plat: "ar",
channel: "aliopen",
devid: deviceId,
ver: "3.9.0",
host: "bd-api.kuwo.cn",
"X-Forwarded-For": "1.0.1.114",
};
let audioUrl = `http://bd-api.kuwo.cn/api/play/music/v2/audioUrl?&br=${"320kmp3"}&musicId=${songId}`;
// 生成签名
audioUrl = generateSign(audioUrl);
// 获取广告
await sendAdFreeRequest();
// 获取歌曲地址
const result = await axios.get(audioUrl, { headers });
if (typeof result.data === "object") {
const urlMatch = result.data.data.audioUrl;
serverLog.log("🔗 BodianSong URL:", urlMatch);
return { code: 200, url: urlMatch };
}
return { code: 404, url: null };
} catch (error) {
serverLog.error("❌ Get BodianSong URL Error:", error);
return { code: 404, url: null };
}
};
export default getBodianSongUrl;

View File

@@ -1,8 +1,9 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { SongUrlResult } from "./unblock";
import { serverLog } from "../../main/logger";
import getKuwoSongUrl from "./kuwo";
import log from "../../main/logger";
import axios from "axios";
import getBodianSongUrl from "./bodian";
/**
* 直接获取 网易云云盘 链接
@@ -17,16 +18,16 @@ const getNeteaseSongUrl = async (id: number | string): Promise<SongUrlResult> =>
params: { types: "url", id },
});
const songUrl = result.data.url;
log.info("🔗 NeteaseSongUrl URL:", songUrl);
serverLog.log("🔗 NeteaseSongUrl URL:", songUrl);
return { code: 200, url: songUrl };
} catch (error) {
log.error("❌ Get NeteaseSongUrl Error:", error);
serverLog.error("❌ Get NeteaseSongUrl Error:", error);
return { code: 404, url: null };
}
};
// 初始化 UnblockAPI
const UnblockAPI = async (fastify: FastifyInstance) => {
export const initUnblockAPI = async (fastify: FastifyInstance) => {
// 主信息
fastify.get("/unblock", (_, reply) => {
reply.send({
@@ -61,8 +62,18 @@ const UnblockAPI = async (fastify: FastifyInstance) => {
return reply.send(result);
},
);
// bodian
fastify.get(
"/unblock/bodian",
async (
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply,
) => {
const { keyword } = req.query;
const result = await getBodianSongUrl(keyword);
return reply.send(result);
},
);
log.info("🌐 Register UnblockAPI successfully");
serverLog.info("🌐 Register UnblockAPI successfully");
};
export default UnblockAPI;

View File

@@ -1,6 +1,6 @@
import { encryptQuery } from "./kwDES";
import { SongUrlResult } from "./unblock";
import log from "../../main/logger";
import { serverLog } from "../../main/logger";
import axios from "axios";
// 获取酷我音乐歌曲 ID
@@ -26,7 +26,7 @@ const getKuwoSongId = async (keyword: string): Promise<string | null> => {
if (songName && !songName?.includes(originalName[0])) return null;
return songId.slice("MUSIC_".length);
} catch (error) {
log.error("❌ Get KuwoSongId Error:", error);
serverLog.error("❌ Get KuwoSongId Error:", error);
return null;
}
};
@@ -53,12 +53,12 @@ const getKuwoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
});
if (result.data) {
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
log.info("🔗 KuwoSong URL:", urlMatch);
serverLog.log("🔗 KuwoSong URL:", urlMatch);
return { code: 200, url: urlMatch };
}
return { code: 404, url: null };
} catch (error) {
log.error("❌ Get KuwoSong URL Error:", error);
serverLog.error("❌ Get KuwoSong URL Error:", error);
return { code: 404, url: null };
}
};

View File

@@ -26,11 +26,7 @@ export default [
"**/components.d.ts",
],
},
...compat.extends(
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-essential",
),
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
{
plugins: {
"@typescript-eslint": typescriptEslint,

View File

@@ -15,6 +15,23 @@ server {
rewrite ^(.*)$ /index.html last;
}
location /api/netease/song/url/v1 {
proxy_buffers 16 64k;
proxy_buffer_size 128k;
proxy_busy_buffers_size 256k;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $remote_addr;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:3000/song/url/v1;
sub_filter '"url":"http://music.163.com' '"url":"/music/unblock';
sub_filter '"url":"https://music.163.com' '"url":"/music/unblock';
sub_filter_types application/json;
sub_filter_once off;
}
location /api/netease/ {
proxy_buffers 16 64k;
proxy_buffer_size 128k;
@@ -26,4 +43,10 @@ server {
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:3000/;
}
location /music/unblock/ {
proxy_pass https://music.163.com/;
proxy_buffering off;
proxy_request_buffering off;
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "splayer",
"productName": "SPlayer",
"version": "3.0.0-beta.1",
"version": "3.0.0-beta.5",
"description": "A minimalist music player",
"main": "./out/main/index.js",
"author": "imsyy",
@@ -23,40 +23,41 @@
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
"typecheck": "npm run typecheck:node && npm run typecheck:web",
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"dev": "node scripts/dev.mjs",
"build": "npx rimraf dist && npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:web": "npm run build",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
"build:win": "npm run build && electron-builder --win --config electron-builder.config.ts",
"build:mac": "npm run build && electron-builder --mac --config electron-builder.config.ts",
"build:linux": "npm run build && electron-builder --linux --config electron-builder.config.ts"
},
"dependencies": {
"@applemusic-like-lyrics/core": "^0.1.3",
"@applemusic-like-lyrics/lyric": "^0.2.4",
"@applemusic-like-lyrics/lyric": "^0.3.0",
"@applemusic-like-lyrics/vue": "^0.1.5",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@imsyy/color-utils": "^1.0.2",
"@material/material-color-utilities": "^0.3.0",
"@pixi/app": "^7.4.2",
"@pixi/core": "^7.4.2",
"@pixi/display": "^7.4.2",
"@pixi/filter-blur": "^7.4.2",
"@neteasecloudmusicapienhanced/api": "^4.29.16",
"@pixi/app": "^7.4.3",
"@pixi/core": "^7.4.3",
"@pixi/display": "^7.4.3",
"@pixi/filter-blur": "^7.4.3",
"@pixi/filter-bulge-pinch": "^5.1.1",
"@pixi/filter-color-matrix": "^7.4.2",
"@pixi/sprite": "^7.4.2",
"@vueuse/core": "^12.0.0",
"NeteaseCloudMusicApi": "^4.25.0",
"axios": "^1.7.9",
"@pixi/filter-color-matrix": "^7.4.3",
"@pixi/sprite": "^7.4.3",
"@vueuse/core": "^13.9.0",
"axios": "^1.13.2",
"axios-retry": "^4.5.0",
"change-case": "^5.4.4",
"dayjs": "^1.11.13",
"dayjs": "^1.11.18",
"electron-dl": "^4.0.0",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
"electron-store": "^11.0.2",
"electron-updater": "^6.6.2",
"file-saver": "^2.0.5",
"font-list": "^1.5.1",
"font-list": "^2.0.1",
"get-port": "^7.1.0",
"github-markdown-css": "^5.8.1",
"howler": "^2.2.4",
@@ -65,59 +66,70 @@
"jss-preset-default": "^10.10.0",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"marked": "^14.1.4",
"marked": "^16.4.0",
"md5": "^2.3.0",
"music-metadata": "7.14.0",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.1.3",
"plyr": "^3.7.8",
"vue-virt-list": "^1.5.5"
"music-metadata": "^11.9.0",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"plyr": "^3.8.3",
"vue-virt-list": "^1.6.1"
},
"devDependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@fastify/cookie": "^9.4.0",
"@fastify/http-proxy": "^9.5.0",
"@fastify/multipart": "^8.3.0",
"@fastify/static": "^7.0.4",
"@electron-toolkit/tsconfig": "^2.0.0",
"@electron-toolkit/utils": "^4",
"@fastify/cookie": "^11.0.2",
"@fastify/http-proxy": "^11.3.0",
"@fastify/multipart": "^9.2.1",
"@fastify/static": "^8.2.0",
"@types/file-saver": "^2.0.7",
"@types/howler": "^2.2.12",
"@types/js-cookie": "^3.0.6",
"@types/md5": "^2.3.5",
"@types/node": "^22.10.1",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-vue": "^5.2.1",
"@types/node": "^24.7.2",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"@vitejs/plugin-vue": "^6.0.1",
"ajv": "^8.17.1",
"crypto-js": "^4.2.0",
"electron": "^30.5.1",
"electron-builder": "^25.1.8",
"electron-log": "^5.2.4",
"electron-vite": "^2.3.0",
"eslint": "^9.16.0",
"eslint-plugin-vue": "^9.32.0",
"fast-glob": "^3.3.2",
"fastify": "^4.29.0",
"naive-ui": "^2.40.3",
"node-taglib-sharp": "^5.2.3",
"prettier": "^3.4.2",
"sass": "^1.82.0",
"terser": "^5.37.0",
"typescript": "5.6.2",
"unplugin-auto-import": "^0.18.6",
"unplugin-vue-components": "^0.27.5",
"vite": "^5.4.11",
"electron": "38.2.2",
"electron-builder": "^26.0.12",
"electron-log": "^5.4.3",
"electron-vite": "^4.0.1",
"eslint": "^9.37.0",
"eslint-plugin-vue": "^10.5.0",
"fast-glob": "^3.3.3",
"fastify": "^5.6.1",
"naive-ui": "^2.43.1",
"node-taglib-sharp": "^6.0.1",
"prettier": "^3.6.2",
"sass": "^1.93.2",
"terser": "^5.44.0",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^29.1.0",
"vite": "^7.2.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-wasm": "^3.3.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tsc": "2.0.29"
"vite-plugin-vue-devtools": "^8.0.3",
"vite-plugin-wasm": "^3.5.0",
"vue": "^3.5.24",
"vue-router": "^4.5.1",
"vue-tsc": "^3.1.3"
},
"pnpm": {
"overrides": {
"dmg-builder": "25.1.8",
"electron-builder-squirrel-windows": "25.1.8"
}
"dmg-builder": "26.0.12",
"electron-builder-squirrel-windows": "26.0.12"
},
"onlyBuiltDependencies": [
"@applemusic-like-lyrics/lyric",
"@parcel/watcher",
"core-js",
"electron",
"electron-winstaller",
"esbuild",
"sharp",
"vue-demi"
]
}
}

6210
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/icons/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/logo.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

90
scripts/dev.mjs Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* 跨平台开发启动脚本
* 自动检测操作系统并设置相应的字符编码
*/
import { spawn } from "child_process";
import os from "os";
// 检测操作系统平台
const platform = os.platform();
const isWindows = platform === "win32";
const isMacOS = platform === "darwin";
console.log(`🚀 检测到操作系统: ${platform}`);
// 设置环境变量
const env = { ...process.env };
if (isWindows) {
console.log("Windows 环境 - 正在设置代码页为 UTF-8");
// Windows 环境下先执行 chcp 65001
const chcp = spawn("chcp", ["65001"], {
stdio: "inherit",
shell: true,
env,
});
chcp.on("close", (code) => {
if (code === 0) {
console.log("✅ 代码页设置成功");
startElectronVite();
} else {
console.warn("⚠️ 代码页设置失败,继续启动...");
startElectronVite();
}
});
} else {
// macOS 和 Linux 环境
console.log(`🐧 ${isMacOS ? "macOS" : "Linux"} 环境 - 正在设置 UTF-8 编码`);
const langVar = env.LC_ALL || env.LANG;
if (langVar.endsWith("UTF-8")) {
console.log("✅ 当前环境已设置 UTF-8 编码");
} else {
if (langVar.startsWith("zh_CN")) {
env.LC_ALL = "zh_CN.UTF-8";
env.LANG = "zh_CN.UTF-8";
} else {
env.LC_ALL = "en_US.UTF-8";
env.LANG = "en_US.UTF-8";
}
}
setTimeout(() => startElectronVite(), 0);
}
const startElectronVite = () => {
console.log("🔧 正在启动 Electron Vite 开发服务器...");
// 设置 Node.js 选项
env.NODE_OPTIONS = "--max-old-space-size=4096";
const electronVite = spawn("electron-vite", ["dev"], {
stdio: "inherit",
shell: true,
env,
});
electronVite.on("close", (code) => {
console.log(`\n🏁 开发服务器已停止 (退出码: ${code})`);
process.exit(code);
});
electronVite.on("error", (err) => {
console.error("❌ 启动失败:", err.message);
process.exit(1);
});
// 优雅退出处理
process.on("SIGINT", () => {
console.log("\n🛑 正在停止开发服务器...");
electronVite.kill("SIGINT");
});
process.on("SIGTERM", () => {
console.log("\n🛑 正在停止开发服务器...");
electronVite.kill("SIGTERM");
});
};

View File

@@ -1,152 +1,5 @@
<template>
<Provider>
<!-- 主框架 -->
<n-layout
id="main"
:class="{
'show-player': musicStore.isHasPlayer && statusStore.showPlayBar,
'show-full-player': statusStore.showFullPlayer,
}"
has-sider
>
<!-- 侧边栏 -->
<n-layout-sider
id="main-sider"
:style="{
height:
musicStore.isHasPlayer && statusStore.showPlayBar ? 'calc(100vh - 80px)' : '100vh',
}"
:content-style="{
overflow: 'hidden',
height: '100%',
padding: '0',
}"
:native-scrollbar="false"
:collapsed="statusStore.menuCollapsed"
:collapsed-width="64"
:width="240"
collapse-mode="width"
show-trigger="bar"
bordered
@collapse="statusStore.menuCollapsed = true"
@expand="statusStore.menuCollapsed = false"
>
<Sider />
</n-layout-sider>
<n-layout id="main-layout">
<!-- 导航栏 -->
<Nav id="main-header" />
<n-layout
ref="contentRef"
id="main-content"
:native-scrollbar="false"
:style="{
'--layout-height': contentHeight,
}"
:content-style="{
display: 'grid',
gridTemplateRows: '1fr',
minHeight: '100%',
padding: '0 24px',
}"
position="absolute"
embedded
>
<!-- 路由页面 -->
<RouterView v-slot="{ Component }">
<Transition :name="`router-${settingStore.routeAnimation}`" mode="out-in">
<KeepAlive v-if="settingStore.useKeepAlive" :max="20" :exclude="['layout']">
<component :is="Component" class="router-view" />
</KeepAlive>
<component v-else :is="Component" class="router-view" />
</Transition>
</RouterView>
<!-- 回顶 -->
<n-back-top :right="40" :bottom="120">
<SvgIcon :size="22" name="Up" />
</n-back-top>
</n-layout>
</n-layout>
</n-layout>
<!-- 播放列表 -->
<MainPlayList />
<!-- 全局播放器 -->
<MainPlayer />
<!-- 全屏播放器 -->
<Teleport to="body">
<Transition name="up" mode="out-in">
<FullPlayer
v-if="
statusStore.showFullPlayer ||
(statusStore.fullPlayerActive && settingStore.fullPlayerCache)
"
/>
</Transition>
</Teleport>
<router-view />
</Provider>
</template>
<script setup lang="ts">
import { useMusicStore, useStatusStore, useSettingStore } from "@/stores";
import init from "@/utils/init";
const musicStore = useMusicStore();
const statusStore = useStatusStore();
const settingStore = useSettingStore();
// 主内容
const contentRef = ref<HTMLElement | null>(null);
// 主内容高度
const { height: contentHeight } = useElementSize(contentRef);
watchEffect(() => {
statusStore.mainContentHeight = contentHeight.value;
});
onMounted(async () => {
await init();
});
</script>
<style lang="scss" scoped>
#main {
flex: 1;
height: 100%;
transition:
transform 0.3s var(--n-bezier),
opacity 0.3s var(--n-bezier);
#main-layout {
background-color: rgba(var(--background), 0.58);
}
#main-content {
top: 70px;
background-color: transparent;
transition: bottom 0.3s;
.router-view {
position: relative;
height: 100%;
&.n-result {
display: flex;
flex-direction: column;
justify-content: center;
}
}
}
&.show-player {
// #main-sider {
// margin-bottom: 80px;
// }
#main-content {
bottom: 80px;
}
}
&.show-full-player {
opacity: 0;
transform: scale(0.9);
#main-header {
-webkit-app-region: no-drag;
}
}
}
</style>

View File

@@ -12,6 +12,16 @@ export const dailyRecommend = (type: "songs" | "resource" = "songs") => {
});
};
/**
* 每日推荐 - 不感兴趣
*/
export const dailyRecommendDislike = (id: number) => {
return request({
url: "/recommend/songs/dislike",
params: { id, timestamp: Date.now() },
});
};
/**
* 推荐内容
* @param {string} [type] - 推荐类型

View File

@@ -1,3 +1,4 @@
import { isElectron } from "@/utils/env";
import { songLevelData } from "@/utils/meta";
import request from "@/utils/request";
@@ -46,7 +47,11 @@ export const songUrl = (
};
// 获取解锁歌曲 URL
export const unlockSongUrl = (id: number, keyword: string, server: "netease" | "kuwo") => {
export const unlockSongUrl = (
id: number,
keyword: string,
server: "netease" | "kuwo" | "bodian",
) => {
const params = server === "netease" ? { id } : { keyword };
return request({
baseURL: "/api/unblock",
@@ -65,6 +70,29 @@ export const songLyric = (id: number) => {
});
};
/**
* 获取歌曲 TTML 歌词
* @param id 音乐 id
* @returns TTML 格式歌词
*/
export const songLyricTTML = async (id: number) => {
if (isElectron) {
return request({ url: "/lyric/ttml", params: { id, noCookie: true } });
} else {
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
try {
const response = await fetch(url);
if (response === null || response.status !== 200) {
return null;
}
const data = await response.text();
return data;
} catch {
return null;
}
}
};
/**
* 获取歌曲下载链接
* @param id 音乐 id

View File

@@ -66,12 +66,13 @@ export const keywords = [
"缩混",
"音乐总监",
"音乐制作",
"OP",
"SP",
"op",
"sp",
"Talkbox",
"Producers",
"Producer",
"Produced",
];
export const regexes = [
/^[Oo][Pp]\s*[:]/,
/^[Ss][Pp]\s*[:]/,
].map((regex) => regex.source);

View File

@@ -4,6 +4,10 @@
"id": 3136952023,
"name": "私人雷达"
},
{
"id": 8402996200,
"name": "会员雷达"
},
{
"id": 5320167908,
"name": "时光雷达"

View File

@@ -0,0 +1,18 @@
import type { LyricConfig } from "../../types/desktop-lyric";
const config: LyricConfig = {
isLock: false,
playedColor: "#fe7971",
unplayedColor: "#ccc",
shadowColor: "rgba(0, 0, 0, 0.5)",
fontFamily: "system-ui",
fontSize: 24,
fontIsBold: false,
showTran: true,
showYrc: true,
isDoubleLine: true,
position: "both",
limitBounds: false,
};
export default config;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="m12 13.4l-4.9 4.9q-.275.275-.7.275t-.7-.275t-.275-.7t.275-.7l4.9-4.9l-4.9-4.9q-.275-.275-.275-.7t.275-.7t.7-.275t.7.275l4.9 4.9l4.9-4.9q.275-.275.7-.275t.7.275t.275.7t-.275.7L13.4 12l4.9 4.9q.275.275.275.7t-.275.7t-.7.275t-.7-.275z"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M9 15H6q-.425 0-.712-.288T5 14t.288-.712T6 13h4q.425 0 .713.288T11 14v4q0 .425-.288.713T10 19t-.712-.288T9 18zm6-6h3q.425 0 .713.288T19 10t-.288.713T18 11h-4q-.425 0-.712-.288T13 10V6q0-.425.288-.712T14 5t.713.288T15 6z"/></svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from MingCute Icon by MingCute Design - https://github.com/Richard9394/MingCute/blob/main/LICENSE --><g fill="none" fill-rule="evenodd"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M18 4a1 1 0 1 0-2 0v1H4a1 1 0 0 0 0 2h12v1a1 1 0 1 0 2 0V7h2a1 1 0 1 0 0-2h-2zM4 11a1 1 0 1 0 0 2h2v1a1 1 0 1 0 2 0v-1h12a1 1 0 1 0 0-2H8v-1a1 1 0 0 0-2 0v1zm-1 7a1 1 0 0 1 1-1h12v-1a1 1 0 1 1 2 0v1h2a1 1 0 1 1 0 2h-2v1a1 1 0 1 1-2 0v-1H4a1 1 0 0 1-1-1"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,6 @@
<svg t="1760890100888" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1549"
xmlns:xlink="http://www.w3.org/1999/xlink">
<path
d="M192 240a112 112 0 0 1 111.616 102.784l0.384 9.216V832a16 16 0 0 0 12.352 15.552L320 848h66.816a48 48 0 0 1 6.528 95.552l-6.528 0.448H320a112 112 0 0 1-111.616-102.784L208 832V352a16 16 0 0 0-12.352-15.552L192 336H128a48 48 0 0 1-6.528-95.552L128 240h64z m640-157.568a112 112 0 0 1 111.616 102.848l0.384 9.152V832a112 112 0 0 1-102.784 111.616L832 944h-67.84a48 48 0 0 1-6.464-95.552l6.464-0.448H832a16 16 0 0 0 15.552-12.352L848 832V194.432a16 16 0 0 0-12.352-15.552L832 178.432H480a48 48 0 0 1-6.528-95.552l6.528-0.448H832z m-160 315.136c61.824 0 112 50.112 112 112v147.648a112 112 0 0 1-112 112h-128a112 112 0 0 1-112-112V509.568c0-61.888 50.176-112 112-112z m0 96h-128a16 16 0 0 0-16 16v147.648c0 8.832 7.168 16 16 16h128a16 16 0 0 0 16-16V509.568a16 16 0 0 0-16-16z m64-253.568a48 48 0 0 1 6.528 95.552l-6.528 0.448h-256a48 48 0 0 1-6.528-95.552L480 240h256zM256 82.432a48 48 0 0 1 6.528 95.616L256 178.432H128a48 48 0 0 1-6.528-95.552L128 82.432h128z"
fill="currentColor" p-id="1550"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

1
src/assets/icons/Eq.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 17V7q0-.425.288-.712T8 6t.713.288T9 7v10q0 .425-.288.713T8 18t-.712-.288T7 17m4 4V3q0-.425.288-.712T12 2t.713.288T13 3v18q0 .425-.288.713T12 22t-.712-.288T11 21m-8-8v-2q0-.425.288-.712T4 10t.713.288T5 11v2q0 .425-.288.713T4 14t-.712-.288T3 13m12 4V7q0-.425.288-.712T16 6t.713.288T17 7v10q0 .425-.288.713T16 18t-.712-.288T15 17m4-4v-2q0-.425.288-.712T20 10t.713.288T21 11v2q0 .425-.288.713T20 14t-.712-.288T19 13"/></svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M7 17h3q.425 0 .713.288T11 18t-.288.713T10 19H6q-.425 0-.712-.288T5 18v-4q0-.425.288-.712T6 13t.713.288T7 14zM17 7h-3q-.425 0-.712-.288T13 6t.288-.712T14 5h4q.425 0 .713.288T19 6v4q0 .425-.288.713T18 11t-.712-.288T17 10z"/></svg>

After

Width:  |  Height:  |  Size: 456 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M12.025 20.35q-.35 0-.687-.125t-.613-.375Q8 17.45 6.3 15.812t-2.662-2.874t-1.3-2.263T2 8.5q0-2.3 1.6-3.9T7.5 3q1.65 0 2.9.637t.9 1.838l-.925 3.25q-.125.5.163.888t.787.387H13l-.65 6.35q-.025.2.163.225t.237-.15L14.6 10.3q.15-.5-.15-.9t-.8-.4H12l1.525-4.525Q13.8 3.6 14.675 3.3T16.5 3q2.3 0 3.9 1.6T22 8.5q0 1.1-.4 2.175t-1.388 2.375t-2.65 2.938t-4.212 3.862q-.275.25-.625.375t-.7.125"/></svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="currentColor"
d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2M9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9zm9 14H6V10h12zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2s-2 .9-2 2s.9 2 2 2" />
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M6 20h12V10H6zm6-3q.825 0 1.413-.587T14 15t-.587-1.412T12 13t-1.412.588T10 15t.588 1.413T12 17m-6 3V10zm0 2q-.825 0-1.412-.587T4 20V10q0-.825.588-1.412T6 8h7V6q0-2.075 1.463-3.537T18 1q1.775 0 3.1 1.075t1.75 2.7q.125.425-.162.825T22 6q-.425 0-.7-.175t-.4-.575q-.275-.95-1.062-1.6T18 3q-1.25 0-2.125.875T15 6v2h3q.825 0 1.413.588T20 10v10q0 .825-.587 1.413T18 22z"/></svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@@ -0,0 +1 @@
<svg class="prefix__icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M511.764 131.708A446.146 446.146 0 10957.91 577.854a446.146 446.146 0 00-446.146-446.146zm0 519.76a71.83 71.83 0 1171.83-70.937 72.276 72.276 0 01-71.83 70.937z" fill="#F55E55"/><path d="M802.205.541l-168.197 37.03a67.814 67.814 0 00-53.091 66.03v120.013l3.569 349.779h114.213V223.614h108.86a26.323 26.323 0 0026.769-26.322V26.864A26.769 26.769 0 00802.205.54z" fill="#F9BBB8"/><path d="M511.764 386.457A186.935 186.935 0 10698.7 572.947a186.935 186.935 0 00-186.935-186.49zm0 264.565a71.383 71.383 0 1171.383-71.383 71.383 71.383 0 01-71.383 71.383z" fill="#F9BBB8"/></svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.45 15.5q.625.625 1.575.588T13.4 15.4L19 7l-8.4 5.6q-.65.45-.712 1.362t.562 1.538M5.1 20q-.55 0-1.012-.238t-.738-.712q-.65-1.175-1-2.437T2 14q0-2.075.788-3.9t2.137-3.175T8.1 4.788T12 4q2.05 0 3.85.775T19 6.888t2.15 3.125t.825 3.837q.025 1.375-.312 2.688t-1.038 2.512q-.275.475-.737.713T18.874 20z"/></svg>

After

Width:  |  Height:  |  Size: 535 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="currentColor"
d="M8.5 7h2L16 21h-2.4l-1.1-3H6.3l-1.1 3H3zm-1.4 9h4.8L9.5 9.7zM22 5v2h-3v3h-2V7h-3V5h3V2h2v3z" />
</svg>

After

Width:  |  Height:  |  Size: 227 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
<path fill="currentColor" d="M10.5 7h-2L3 21h2.2l1.1-3h6.2l1.1 3H16zm-3.4 9l2.4-6.3l2.4 6.3zM22 7h-8V5h8z" />
</svg>

After

Width:  |  Height:  |  Size: 204 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="currentColor" d="M10.425 15.25h3.1l.375 1.15q.1.275.325.438t.5.162q.475 0 .738-.387t.112-.813l-2.35-6.625q-.1-.275-.35-.45t-.55-.175h-.65q-.3 0-.55.175t-.35.45L8.4 15.85q-.15.425.113.788t.712.362q.275 0 .5-.162t.325-.438zm.475-1.45l1.1-3.3l1.075 3.3zM10 3q-.425 0-.712-.288T9 2t.288-.712T10 1h4q.425 0 .713.288T15 2t-.288.713T14 3zm2 19q-1.85 0-3.488-.712T5.65 19.35t-1.937-2.863T3 13t.713-3.488T5.65 6.65t2.863-1.937T12 4q1.55 0 2.975.5t2.675 1.45l.7-.7q.275-.275.7-.275t.7.275t.275.7t-.275.7l-.7.7Q20 8.6 20.5 10.025T21 13q0 1.85-.713 3.488T18.35 19.35t-2.863 1.938T12 22"/></svg>

After

Width:  |  Height:  |  Size: 792 B

View File

@@ -55,6 +55,9 @@
<n-tag v-if="song.originCoverType === 1" :bordered="false" type="primary" round>
</n-tag>
<n-tag v-if="song.originCoverType === 2" :bordered="false" type="info" round>
翻唱
</n-tag>
<n-tag v-if="song.free === 1" :bordered="false" type="error" round> VIP </n-tag>
<n-tag v-if="song.free === 4" :bordered="false" type="error" round> EP </n-tag>
<!-- 云盘 -->
@@ -150,13 +153,14 @@
<script setup lang="ts">
import type { SongType } from "@/types/main";
import { useStatusStore, useMusicStore, useDataStore } from "@/stores";
import { formatNumber, isElectron } from "@/utils/helper";
import { formatNumber } from "@/utils/helper";
import { openJumpArtist } from "@/utils/modal";
import { toLikeSong } from "@/utils/auth";
import { isObject } from "lodash-es";
import { formatTimestamp, msToTime } from "@/utils/time";
import player from "@/utils/player";
import blob from "@/utils/blob";
import { isElectron } from "@/utils/env";
const props = defineProps<{
// 歌曲

View File

@@ -49,8 +49,12 @@ const settingStore = useSettingStore();
// 操作系统主题
const osTheme = useOsTheme();
// 全局主题
const themeOverrides = ref<GlobalThemeOverrides>({});
// 全局主题(使用 shallowRef 避免深层追踪开销)
const themeOverrides = shallowRef<GlobalThemeOverrides>({});
// 轻量的 rgba 构造器
const toRGBA = (rgb: string, alpha: number) => `rgba(${rgb}, ${alpha})`;
// 主题缓存键
let lastThemeCacheKey: string | null = null;
// 获取明暗模式
const theme = computed(() => {
@@ -88,121 +92,133 @@ const changeGlobalTheme = () => {
themeOverrides.value = {};
return;
}
// 修改主题
themeOverrides.value = settingStore.themeGlobalColor
? {
common: {
fontFamily: `${settingStore.globalFont === "default" ? "v-sans" : settingStore.globalFont}, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
primaryColor: `rgb(${colorSchemes.primary})`,
primaryColorHover: `rgba(${colorSchemes.primary}, 0.78)`,
primaryColorPressed: `rgba(${colorSchemes.primary}, 0.26)`,
primaryColorSuppl: `rgba(${colorSchemes.primary}, 0.12)`,
textColorBase: colorSchemes.primary,
textColor1: `rgb(${colorSchemes.primary})`,
textColor2: `rgba(${colorSchemes.primary}, 0.82)`,
textColor3: `rgba(${colorSchemes.primary}, 0.52)`,
bodyColor: `rgb(${colorSchemes.background})`,
cardColor: `rgb(${colorSchemes["surface-container"]})`,
tagColor: `rgb(${colorSchemes["surface-container"]})`,
modalColor: `rgb(${colorSchemes["surface-container"]})`,
popoverColor: `rgb(${colorSchemes["surface-container"]})`,
buttonColor2: `rgba(${colorSchemes.primary}, 0.08)`,
buttonColor2Hover: `rgba(${colorSchemes.primary}, 0.12)`,
buttonColor2Pressed: `rgba(${colorSchemes.primary}, 0.08)`,
iconColor: `rgb(${colorSchemes.primary})`,
iconColorHover: `rgba(${colorSchemes.primary}, 0.475)`,
closeIconColor: `rgba(${colorSchemes.primary}, 0.58)`,
hoverColor: `rgba(${colorSchemes.primary}, 0.09)`,
borderColor: `rgba(${colorSchemes.primary}, 0.09)`,
textColorDisabled: `rgba(${colorSchemes.primary}, 0.3)`,
placeholderColorDisabled: `rgba(${colorSchemes.primary}, 0.3)`,
iconColorDisabled: `rgba(${colorSchemes.primary}, 0.3)`,
},
Card: {
borderColor: `rgba(${colorSchemes.primary}, 0.09)`,
},
Button: {
textColorHover: `rgba(${colorSchemes.primary}, 0.78)`,
textColorFocus: `rgba(${colorSchemes.primary}, 0.58)`,
colorPrimary: `rgba(${colorSchemes.primary}, 0.9)`,
colorHoverPrimary: `rgb(${colorSchemes.primary})`,
colorPressedPrimary: `rgba(${colorSchemes.primary}, 0.8)`,
colorFocusPrimary: `rgb(${colorSchemes.primary})`,
},
Slider: {
handleColor: `rgb(${colorSchemes.primary})`,
fillColor: `rgb(${colorSchemes.primary})`,
fillColorHover: `rgb(${colorSchemes.primary})`,
railColor: `rgba(${colorSchemes.primary}, 0.2)`,
railColorHover: `rgba(${colorSchemes.primary}, 0.3)`,
},
Switch: {
railColorActive: `rgba(${colorSchemes.primary}, 0.8)`,
},
Input: {
color: `rgba(${colorSchemes.primary}, 0.1)`,
colorFocus: `rgb(${colorSchemes["surface-container"]})`,
placeholderColor: `rgba(${colorSchemes.primary}, 0.58)`,
border: `1px solid rgba(${colorSchemes.primary}, 0.1)`,
clearColor: `rgba(${colorSchemes.primary}, 0.38)`,
clearColorHover: `rgba(${colorSchemes.primary}, 0.48)`,
clearColorPressed: `rgba(${colorSchemes.primary}, 0.3)`,
},
Icon: {
color: `rgb(${colorSchemes.primary})`,
},
Empty: {
textColor: `rgba(${colorSchemes.primary}, 0.38)`,
},
Divider: {
color: `rgba(${colorSchemes.primary}, 0.09)`,
},
Dropdown: {
dividerColor: `rgba(${colorSchemes.primary}, 0.09)`,
},
Layout: {
siderBorderColor: `rgba(${colorSchemes.primary}, 0.09)`,
},
Tabs: {
colorSegment: `rgba(${colorSchemes.primary}, 0.08)`,
tabColorSegment: `rgba(${colorSchemes.primary}, 0.12)`,
},
Drawer: {
headerBorderBottom: `1px solid rgba(${colorSchemes.primary}, 0.09)`,
footerBorderTop: `1px solid rgba(${colorSchemes.primary}, 0.09)`,
},
Menu: {
dividerColor: `rgba(${colorSchemes.primary}, 0.09)`,
},
Progress: {
railColor: `rgba(${colorSchemes.primary}, 0.16)`,
},
Popover: {
color: `rgb(${colorSchemes["surface-container"]})`,
},
}
: {
common: {
fontFamily: `${settingStore.globalFont === "default" ? "v-sans" : settingStore.globalFont}, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`,
primaryColor: `rgb(${colorSchemes.primary})`,
primaryColorHover: `rgba(${colorSchemes.primary}, 0.78)`,
primaryColorPressed: `rgba(${colorSchemes.primary}, 0.26)`,
primaryColorSuppl: `rgba(${colorSchemes.primary}, 0.12)`,
},
Icon: {
color: `rgb(${colorSchemes.primary})`,
},
Slider: {
handleColor: `rgb(${colorSchemes.primary})`,
fillColor: `rgb(${colorSchemes.primary})`,
fillColorHover: `rgb(${colorSchemes.primary})`,
railColor: `rgba(${colorSchemes.primary}, 0.2)`,
railColorHover: `rgba(${colorSchemes.primary}, 0.3)`,
},
Popover: {
color: `rgb(${colorSchemes["surface-container"]})`,
},
};
// 构造主题缓存 Key
const themeModeLabel = theme.value ? "dark" : "light";
const themeCacheKey = `${themeModeLabel}|${settingStore.themeGlobalColor ? 1 : 0}|${settingStore.globalFont}|${colorSchemes.primary}|${colorSchemes.background}|${colorSchemes["surface-container"]}`;
if (lastThemeCacheKey === themeCacheKey) return;
lastThemeCacheKey = themeCacheKey;
// 关键颜色
const primaryRGB = colorSchemes.primary as string;
const surfaceContainerRGB = colorSchemes["surface-container"] as string;
// 全局字体
const fontFamily = `${settingStore.globalFont === "default" ? "v-sans" : settingStore.globalFont}, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// 通用样式基座
const commonBase = {
fontFamily,
primaryColor: `rgb(${primaryRGB})`,
primaryColorHover: toRGBA(primaryRGB, 0.78),
primaryColorPressed: toRGBA(primaryRGB, 0.26),
primaryColorSuppl: toRGBA(primaryRGB, 0.12),
} as GlobalThemeOverrides["common"];
if (settingStore.themeGlobalColor) {
themeOverrides.value = {
common: {
...commonBase,
textColorBase: primaryRGB,
textColor1: `rgb(${primaryRGB})`,
textColor2: toRGBA(primaryRGB, 0.82),
textColor3: toRGBA(primaryRGB, 0.52),
bodyColor: `rgb(${colorSchemes.background})`,
cardColor: `rgb(${surfaceContainerRGB})`,
tagColor: `rgb(${surfaceContainerRGB})`,
modalColor: `rgb(${surfaceContainerRGB})`,
popoverColor: `rgb(${surfaceContainerRGB})`,
buttonColor2: toRGBA(primaryRGB, 0.08),
buttonColor2Hover: toRGBA(primaryRGB, 0.12),
buttonColor2Pressed: toRGBA(primaryRGB, 0.08),
iconColor: `rgb(${primaryRGB})`,
iconColorHover: toRGBA(primaryRGB, 0.475),
closeIconColor: toRGBA(primaryRGB, 0.58),
hoverColor: toRGBA(primaryRGB, 0.09),
borderColor: toRGBA(primaryRGB, 0.09),
textColorDisabled: toRGBA(primaryRGB, 0.3),
placeholderColorDisabled: toRGBA(primaryRGB, 0.3),
iconColorDisabled: toRGBA(primaryRGB, 0.3),
},
Card: {
borderColor: toRGBA(primaryRGB, 0.09),
},
Button: {
textColorHover: toRGBA(primaryRGB, 0.78),
textColorFocus: toRGBA(primaryRGB, 0.58),
colorPrimary: toRGBA(primaryRGB, 0.9),
colorHoverPrimary: `rgb(${primaryRGB})`,
colorPressedPrimary: toRGBA(primaryRGB, 0.8),
colorFocusPrimary: `rgb(${primaryRGB})`,
},
Slider: {
handleColor: `rgb(${primaryRGB})`,
fillColor: `rgb(${primaryRGB})`,
fillColorHover: `rgb(${primaryRGB})`,
railColor: toRGBA(primaryRGB, 0.2),
railColorHover: toRGBA(primaryRGB, 0.3),
},
Switch: {
railColorActive: toRGBA(primaryRGB, 0.8),
},
Input: {
color: toRGBA(primaryRGB, 0.1),
colorFocus: `rgb(${surfaceContainerRGB})`,
placeholderColor: toRGBA(primaryRGB, 0.58),
border: `1px solid ${toRGBA(primaryRGB, 0.1)}`,
clearColor: toRGBA(primaryRGB, 0.38),
clearColorHover: toRGBA(primaryRGB, 0.48),
clearColorPressed: toRGBA(primaryRGB, 0.3),
},
Icon: {
color: `rgb(${primaryRGB})`,
},
Empty: {
textColor: toRGBA(primaryRGB, 0.38),
},
Divider: {
color: toRGBA(primaryRGB, 0.09),
},
Dropdown: {
dividerColor: toRGBA(primaryRGB, 0.09),
},
Layout: {
siderBorderColor: toRGBA(primaryRGB, 0.09),
},
Tabs: {
colorSegment: toRGBA(primaryRGB, 0.08),
tabColorSegment: toRGBA(primaryRGB, 0.12),
},
Drawer: {
headerBorderBottom: `1px solid ${toRGBA(primaryRGB, 0.09)}`,
footerBorderTop: `1px solid ${toRGBA(primaryRGB, 0.09)}`,
},
Menu: {
dividerColor: toRGBA(primaryRGB, 0.09),
},
Progress: {
railColor: toRGBA(primaryRGB, 0.16),
},
Popover: {
color: `rgb(${surfaceContainerRGB})`,
},
};
} else {
themeOverrides.value = {
common: {
...commonBase,
},
Icon: {
color: `rgb(${primaryRGB})`,
},
Slider: {
handleColor: `rgb(${primaryRGB})`,
fillColor: `rgb(${primaryRGB})`,
fillColorHover: `rgb(${primaryRGB})`,
railColor: toRGBA(primaryRGB, 0.2),
railColorHover: toRGBA(primaryRGB, 0.3),
},
};
}
} catch (error) {
themeOverrides.value = {};
console.error("切换主题色出现错误:", error);

View File

@@ -1,7 +1,11 @@
<!-- 全局图标 -->
<template>
<n-icon v-if="name" :size="size" :color="color" :depth="depth">
<div ref="svgContainer" class="svg-container" />
<div
ref="svgContainer"
:style="{ transform: offset ? `translateY(${offset}px)` : undefined }"
class="svg-container"
/>
</n-icon>
</template>
@@ -10,6 +14,7 @@ const props = defineProps<{
name: string;
size?: string | number;
color?: string;
offset?: number;
depth?: 1 | 2 | 3 | 4 | 5;
}>();

View File

@@ -29,10 +29,11 @@ import {
import type { CoverType } from "@/types/main";
import { useStatusStore, useSettingStore, useDataStore, useMusicStore } from "@/stores";
import { useRouter, RouterLink } from "vue-router";
import { isElectron, renderIcon } from "@/utils/helper";
import { renderIcon } from "@/utils/helper";
import { openCreatePlaylist } from "@/utils/modal";
import { debounce } from "lodash-es";
import { isLogin } from "@/utils/auth";
import { isElectron } from "@/utils/env";
import player from "@/utils/player";
const router = useRouter();
@@ -176,11 +177,20 @@ const menuOptions = computed<MenuOption[] | MenuGroupOption[]>(() => {
children: [...likedPlaylist.value],
},
]
: [];
: [
{
key: "local",
link: "local",
label: "本地歌曲",
show: isElectron,
icon: renderIcon("FolderMusic"),
},
];
});
// 生成歌单列表
const renderPlaylist = (playlist: CoverType[], showCover: boolean) => {
if (!isLogin()) return [];
return playlist.map((playlist) => ({
key: playlist.id,
label: () =>

View File

@@ -91,8 +91,9 @@
<script setup lang="ts">
import type { DropdownOption } from "naive-ui";
import { useSettingStore } from "@/stores";
import { isElectron, isDev, renderIcon } from "@/utils/helper";
import { renderIcon } from "@/utils/helper";
import { openSetting } from "@/utils/modal";
import { isDev, isElectron } from "@/utils/env";
const router = useRouter();
const settingStore = useSettingStore();
@@ -110,10 +111,8 @@ const min = () => window.electron.ipcRenderer.send("win-min");
// 最大化或还原
const maxOrRes = () => {
if (window.electron.ipcRenderer.sendSync("win-state")) {
isMax.value = false;
window.electron.ipcRenderer.send("win-restore");
} else {
isMax.value = true;
window.electron.ipcRenderer.send("win-max");
}
};
@@ -125,7 +124,7 @@ const hideOrClose = (action: "hide" | "exit") => {
settingStore.closeAppMethod = action;
}
showCloseModal.value = false;
window.electron.ipcRenderer.send(action === "hide" ? "win-hide" : "win-close");
window.electron.ipcRenderer.send(action === "hide" ? "win-hide" : "quit-app");
};
// 尝试关闭软件
@@ -198,9 +197,12 @@ const setSelect = (key: string) => {
};
onMounted(() => {
// 获取窗口状态
// 获取窗口状态并监听主进程的状态变更
if (isElectron) {
isMax.value = window.electron.ipcRenderer.sendSync("win-state");
window.electron.ipcRenderer.on("win-state-change", (_event, value: boolean) => {
isMax.value = value;
});
}
});
</script>

View File

@@ -363,7 +363,6 @@ const getListData = async (id: number): Promise<SongType[]> => {
&:hover {
background-color: rgba(var(--primary), 0.12);
.cover {
border-radius: 16px 16px 0 0;
.cover-img {
transform: scale(1.1);
filter: brightness(0.8);

View File

@@ -15,7 +15,7 @@
<Transition name="fade" mode="out-in">
<VirtList
ref="listRef"
:key="listData?.[0]?.id"
:key="listKey"
:list="listData"
:minSize="94"
:buffer="2"
@@ -60,9 +60,21 @@
:hiddenCover="hiddenCover"
:hiddenAlbum="hiddenAlbum"
:hiddenSize="hiddenSize"
@dblclick.stop="player.updatePlayList(listData, itemData, playListId)"
@dblclick.stop="
doubleClickAction === 'add'
? player.addNextSong(itemData, true)
: player.updatePlayList(listData, itemData, playListId)
"
@contextmenu.stop="
songListMenuRef?.openDropdown($event, listData, itemData, index, type, playListId)
songListMenuRef?.openDropdown(
$event,
listData,
itemData,
index,
type,
playListId,
isDailyRecommend,
)
"
/>
</template>
@@ -114,7 +126,7 @@ import type { DropdownOption } from "naive-ui";
import type { SongType, SortType } from "@/types/main";
import { useMusicStore, useStatusStore } from "@/stores";
import { VirtList } from "vue-virt-list";
import { cloneDeep, entries, isEmpty } from "lodash-es";
import { entries, isEmpty } from "lodash-es";
import { sortOptions } from "@/utils/meta";
import { renderIcon } from "@/utils/helper";
import SongListMenu from "@/components/Menu/SongListMenu.vue";
@@ -143,11 +155,16 @@ const props = withDefaults(
disabledSort?: boolean;
// 播放歌单 ID
playListId?: number;
// 是否为每日推荐
isDailyRecommend?: boolean;
// 双击播放操作
doubleClickAction?: "all" | "add";
}>(),
{
type: "song",
loadingText: "努力加载中...",
playListId: 0,
isDailyRecommend: false,
},
);
@@ -179,8 +196,9 @@ const songListMenuRef = ref<InstanceType<typeof SongListMenu> | null>(null);
// 列表数据
const listData = computed<SongType[]>(() => {
const data = cloneDeep(props.data);
if (props.disabledSort) return data;
if (props.disabledSort) return props.data;
// 创建副本用于排序(避免修改原数组)
const data = [...props.data];
// 排序
switch (statusStore.listSort) {
case "titleAZ":
@@ -212,6 +230,16 @@ const listData = computed<SongType[]>(() => {
}
});
// 虚拟列表 key
const listKey = computed(() => {
// 每日推荐
if (props.isDailyRecommend) {
return musicStore.dailySongsData.timestamp || 0;
}
// 其他列表长度(检测增删操作)
return listData.value?.length || 0;
});
// 列表是否具有播放歌曲
const hasPlaySong = computed(() => {
return listData.value.findIndex((item) => item.id === musicStore.playSong.id);
@@ -360,7 +388,6 @@ onBeforeUnmount(() => {
}
.meta {
width: 50px;
font-size: 13px;
text-align: center;
&.size {
width: 60px;

View File

@@ -18,7 +18,7 @@
<script setup lang="ts">
import type { SongType } from "@/types/main";
import { NAlert, type DropdownOption } from "naive-ui";
import { useStatusStore, useLocalStore, useDataStore } from "@/stores";
import { useStatusStore, useLocalStore, useDataStore, useMusicStore } from "@/stores";
import { renderIcon, copyData } from "@/utils/helper";
import { deleteCloudSong, importCloudSong } from "@/api/cloud";
import {
@@ -29,6 +29,8 @@ import {
} from "@/utils/modal";
import { deleteSongs, isLogin } from "@/utils/auth";
import { songUrl } from "@/api/song";
import { dailyRecommendDislike } from "@/api/rec";
import { formatSongsList } from "@/utils/format";
import player from "@/utils/player";
const emit = defineEmits<{ removeSong: [index: number[]] }>();
@@ -37,6 +39,7 @@ const router = useRouter();
const dataStore = useDataStore();
const localStore = useLocalStore();
const statusStore = useStatusStore();
const musicStore = useMusicStore();
// 右键菜单数据
const dropdownX = ref<number>(0);
@@ -52,6 +55,7 @@ const openDropdown = (
index: number,
type: "song" | "radio",
playListId?: number,
isDailyRecommend: boolean = false,
) => {
try {
e.preventDefault();
@@ -110,6 +114,15 @@ const openDropdown = (
key: "line-1",
type: "divider",
},
{
key: "dislike",
label: "不感兴趣",
show: isDailyRecommend && isLoginNormal,
props: {
onClick: () => dislikeSong(song, index),
},
icon: renderIcon("HeartBroken"),
},
{
key: "more",
label: "更多操作",
@@ -322,6 +335,41 @@ const importSongToCloud = async (song: SongType) => {
}
};
// 每日推荐 - 不感兴趣
const dislikeSong = async (song: SongType, index: number) => {
if (!song?.id) return;
const loadingMessage = window.$message.loading("正在不感兴趣...", { duration: 0 });
try {
const result = await dailyRecommendDislike(song.id);
// 关闭 loading
loadingMessage.destroy();
if (result.code === 200) {
// 创建新数组以触发响应式更新
const currentList = [...musicStore.dailySongsData.list];
// 从列表中移除当前歌曲
currentList.splice(index, 1);
// 替换原歌曲
if (result.data) {
const formattedSong = formatSongsList([result.data])[0];
currentList.splice(index, 0, formattedSong);
}
// 更新列表(同时更新 timestamp 触发完整响应式更新)
musicStore.dailySongsData = {
list: currentList,
timestamp: Date.now(),
};
window.$message.success("已标记为不感兴趣");
} else {
window.$message.error("操作失败,请重试");
}
} catch (error) {
// 关闭 loading
loadingMessage.destroy();
window.$message.error("操作失败,请重试");
console.error("不感兴趣操作失败:", error);
}
};
defineExpose({ openDropdown });
</script>

View File

@@ -0,0 +1,98 @@
<template>
<n-flex :size="20" class="auto-close" align="center" vertical>
<n-card class="open">
<n-flex align="center" justify="space-between">
<n-flex size="small" align="center">
<SvgIcon name="TimeAuto" size="22" />
<Transition name="fade" mode="out-in">
<n-text v-if="!statusStore.autoClose.enable"> 未开启 </n-text>
<n-text v-else strong>
{{ convertSecondsToTime(statusStore.autoClose.remainTime) }}
</n-text>
</Transition>
</n-flex>
<n-switch
v-model:value="statusStore.autoClose.enable"
:round="false"
@update:value="handleUpdate"
/>
</n-flex>
</n-card>
<!-- 时间选择 -->
<n-flex size="large" align="center" justify="center">
<n-tag
v-for="(item, index) in [10, 20, 30, 45, 60, 90, 120]"
:bordered="false"
:key="index"
type="primary"
size="large"
round
@click="player.startAutoCloseTimer(item, item * 60)"
>
{{ item }}min
</n-tag>
<!-- 自定义 -->
<n-popconfirm
:negative-text="null"
:positive-button-props="{
strong: true,
secondary: true,
type: 'primary',
}"
:show-icon="false"
@positive-click="player.startAutoCloseTimer(customTime, customTime * 60)"
>
<template #trigger>
<n-tag :bordered="false" type="primary" size="large" round> 自定义时长 </n-tag>
</template>
<n-flex vertical>
<n-text>自定义时长分钟</n-text>
<n-input-number
v-model:value="customTime"
:min="1"
:max="120"
placeholder="请输入自定义时长"
/>
</n-flex>
</n-popconfirm>
</n-flex>
<!-- 是否播放完 -->
<n-checkbox v-model:checked="statusStore.autoClose.waitSongEnd">
等待整首歌曲播放完成再停止播放
</n-checkbox>
</n-flex>
</template>
<script setup lang="ts">
import { useStatusStore } from "@/stores";
import { convertSecondsToTime } from "@/utils/time";
import player from "@/utils/player";
const statusStore = useStatusStore();
// 自定义时长
const customTime = ref(1);
// 是否开启
const handleUpdate = (value: boolean) => {
if (value) {
player.startAutoCloseTimer(statusStore.autoClose.time, statusStore.autoClose.remainTime);
} else {
statusStore.autoClose.enable = false;
statusStore.autoClose.remainTime = statusStore.autoClose.time * 60;
}
};
</script>
<style scoped lang="scss">
.auto-close {
width: 100%;
.open {
width: 100%;
border-radius: 8px;
.n-text {
font-size: 18px;
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<n-flex align="center" size="large" vertical>
<n-flex align="center" justify="center">
<n-tag
v-for="(item, index) in [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2]"
:type="statusStore.playRate === item ? 'primary' : 'default'"
:bordered="statusStore.playRate === item"
:key="index"
size="large"
round
@click="player.setRate(item)"
>
{{ item }}x
</n-tag>
</n-flex>
<n-text :depth="3"> 当前播放速度 {{ statusStore.playRate }}x </n-text>
<n-slider
v-model:value="statusStore.playRate"
:step="0.1"
:min="0.2"
:max="2"
:tooltip="false"
:marks="{
0.2: '0.2x',
1: '1x',
2: '2x',
}"
@update:value="(value) => player.setRate(value)"
/>
</n-flex>
</template>
<script setup lang="ts">
import { useStatusStore } from "@/stores";
import player from "@/utils/player";
const statusStore = useStatusStore();
</script>

View File

@@ -67,10 +67,11 @@ import { useSettingStore } from "@/stores";
import { songLevelData, getLevelsUpTo } from "@/utils/meta";
import { formatSongsList } from "@/utils/format";
import { cloneDeep, reduce } from "lodash-es";
import { formatFileSize, isElectron } from "@/utils/helper";
import { formatFileSize } from "@/utils/helper";
import { getPlayerInfo } from "@/utils/player-utils/song";
import { openSetting } from "@/utils/modal";
import { saveAs } from "file-saver";
import player from "@/utils/player";
import { isElectron } from "@/utils/env";
const props = defineProps<{ id: number }>();
const emit = defineEmits<{ close: [] }>();
@@ -156,7 +157,7 @@ const download = async () => {
}
// 下载相关数据
const { url, type = "mp3" } = result.data;
const songName = player.getPlayerInfo(songData.value) || "未知曲目";
const songName = getPlayerInfo(songData.value) || "未知曲目";
// 区分设备下载
if (isElectron) {
await electronDownload(url, songName, type.toLowerCase());

View File

@@ -0,0 +1,158 @@
<template>
<n-flex class="equalizer" size="large" vertical>
<n-alert :show-icon="false"> 实验性功能请谨慎使用 </n-alert>
<n-flex align="center" justify="space-between" :size="8">
<n-flex wrap :size="8" class="eq-presets">
<n-tag
v-for="(preset, key) in presetList"
:key="key"
:type="currentPreset === key ? 'primary' : 'default'"
:bordered="currentPreset === key"
:disabled="!enabled"
round
@click="applyPreset(key as PresetKey)"
>
{{ preset.label }}
</n-tag>
</n-flex>
<n-switch v-model:value="enabled" :round="false" :disabled="!isElectron" />
</n-flex>
<div class="eq-sliders">
<div v-for="(freq, i) in freqLabels" :key="freq" class="eq-col">
<div class="eq-freq">{{ freq }}</div>
<n-slider
v-model:value="bands[i]"
:min="-12"
:max="12"
:step="0.1"
:disabled="!enabled || !isElectron"
vertical
@update:value="onBandChange(i, $event)"
/>
<div class="eq-value">{{ formatDb(bands[i]) }}</div>
</div>
</div>
</n-flex>
</template>
<script setup lang="ts">
import { isElectron } from "@/utils/env";
import { useStatusStore } from "@/stores";
import player from "@/utils/player";
const statusStore = useStatusStore();
type PresetKey = keyof typeof presetList;
// 10 段中心频率
const frequencies = [31, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];
// 频率文本
const freqLabels = frequencies.map((f) => (f >= 1000 ? `${f / 1000}kHz` : `${f}Hz`));
// 预设(单位 dB范围建议在 [-12, 12]
const presetList = {
acoustic: { label: "原声", bands: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] },
pop: { label: "流行", bands: [-1, -1, 0, 2, 4, 4, 2, 1, -1, 1] },
dance: { label: "舞曲", bands: [4, 6, 7, 0, 2, 3, 5, 4, 3, 0] },
rock: { label: "摇滚", bands: [5, 3, 3, 1, 0, -1, 0, 2, 3, 5] },
classical: { label: "古典", bands: [5, 4, 3, 2, -1, -1, 0, 2, 3, 5] },
jazz: { label: "爵士", bands: [3, 3, 2, 2, -1, -1, 0, 2, 2, 5] },
vocal: { label: "人声", bands: [-2, -1, 0, 2, 4, 4, 2, 0, -1, -2] },
bass: { label: "重低音", bands: [6, 6, 8, 2, 0, 0, 0, 0, 0, 0] },
custom: { label: "自定义", bands: [] as number[] },
} as const;
const enabled = ref<boolean>(statusStore.eqEnabled);
// 当前预设
const currentPreset = ref<PresetKey>((statusStore.eqPreset as PresetKey) || "custom");
// 当前频段
const bands = ref<number[]>(
statusStore.eqBands?.length === 10 ? [...statusStore.eqBands] : Array(10).fill(0),
);
/** 格式化 dB 文本 */
const formatDb = (v: number) => `${v >= 0 ? "+" : ""}${v}dB`;
/**
* 应用预设
*/
const applyPreset = (key: PresetKey) => {
if (!enabled.value) return;
currentPreset.value = key;
statusStore.setEqPreset(key);
// 自定义不覆盖当前频段
if (key !== "custom") {
const arr = presetList[key].bands;
bands.value = [...arr];
statusStore.setEqBands(bands.value);
if (enabled.value) player.updateEq({ bands: bands.value });
}
};
/**
* 根据当前开关状态应用/移除 EQ
*/
const applyEq = () => {
if (!isElectron) return;
statusStore.setEqEnabled(enabled.value);
statusStore.setEqBands(bands.value);
if (enabled.value) {
player.enableEq({ bands: bands.value, frequencies });
} else {
player.disableEq();
}
};
/**
* 单段变更处理:实时更新 EQ
*/
const onBandChange = (index: number, value: number) => {
bands.value[index] = value;
statusStore.setEqBands(bands.value);
// 任何手动拖动都切换为自定义
if (currentPreset.value !== "custom") {
currentPreset.value = "custom";
statusStore.setEqPreset("custom");
}
if (enabled.value) player.updateEq({ bands: bands.value });
};
watch(enabled, () => applyEq());
</script>
<style scoped lang="scss">
.equalizer {
.eq-sliders {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 12px;
margin-top: 20px;
.eq-col {
display: flex;
flex-direction: column;
align-items: center;
.eq-freq {
height: 20px;
font-size: 12px;
opacity: 0.75;
margin-bottom: 6px;
}
:deep(.n-slider) {
height: 160px;
}
.eq-value {
width: 46px;
text-align: center;
margin-top: 6px;
font-size: 12px;
opacity: 0.8;
white-space: nowrap;
}
}
}
}
</style>

View File

@@ -1,20 +0,0 @@
<template>
<div class="exclude">
<n-alert :show-icon="false">请勿添加过多以免影响歌词的正常显示</n-alert>
<n-dynamic-tags v-model:value="settingStore.excludeKeywords" />
</div>
</template>
<script setup lang="ts">
import { useSettingStore } from "@/stores";
const settingStore = useSettingStore();
</script>
<style lang="scss" scoped>
.exclude {
.n-alert {
margin-bottom: 20px;
}
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<div class="exclude">
<n-alert :show-icon="false">请勿添加过多以免影响歌词的正常显示</n-alert>
<n-tabs type="line" v-model:value="page" animated>
<n-tab-pane name="keywords" tab="关键词">
<n-dynamic-tags v-model:value="settingStore.excludeKeywords" />
</n-tab-pane>
<n-tab-pane name="regexes" tab="正则表达式">
<n-dynamic-tags v-model:value="settingStore.excludeRegexes" />
</n-tab-pane>
<template #suffix>
<n-button type="primary" strong secondary @click="reset">重置此页</n-button>
</template>
</n-tabs>
</div>
</template>
<script setup lang="ts">
import { useSettingStore } from "@/stores";
import { keywords, regexes } from "@/assets/data/exclude";
const settingStore = useSettingStore();
const page = ref("keywords");
const reset = () => {
switch (page.value) {
case "keywords":
settingStore.excludeKeywords = keywords;
break;
case "regexes":
settingStore.excludeRegexes = regexes;
break;
}
};
</script>
<style lang="scss" scoped>
.exclude {
.n-alert {
margin-bottom: 20px;
}
}
</style>

View File

@@ -24,7 +24,7 @@
<script setup lang="ts">
import type { LoginType } from "@/types/main";
import { isElectron } from "@/utils/helper";
import { isElectron } from "@/utils/env";
const emit = defineEmits<{
close: [];
@@ -38,7 +38,7 @@ const openWeb = () => {
window.$dialog.info({
title: "使用前告知",
content:
"请知悉,该功能仍旧无法确保账号的安全性!请自行决定是否使用!如遇打开窗口后页面出现白屏或者无法点击等情况,请关闭后再试。在登录完成后,请点击菜单栏中的 “登录完成” 按钮以完成登录( 通常位于窗口的左上角macOS 位于顶部的全局菜单栏中 ",
"请知悉,该功能仍旧无法确保账号的安全性!请自行决定是否使用!如遇打开窗口后页面出现白屏或者无法点击等情况,请关闭后重试",
positiveText: "我已了解",
negativeText: "取消",
onPositiveClick: () => window.electron.ipcRenderer.send("open-login-web"),

View File

@@ -190,29 +190,29 @@ const getSongInfo = async () => {
common: ICommonTagsResult;
format: IFormat;
md5: string;
lyric?: string;
} = await window.electron.ipcRenderer.invoke("get-music-metadata", path);
// console.log(infoData);
// 解构数据
const { fileName, fileSize, common, format, md5 } = infoData;
const { fileName, fileSize, common, format, md5, lyric } = infoData;
// 更新数据
infoFormData.value = {
fileName,
name: common.title || "",
artist: common.artist || "",
album: common.album || "",
alia: common.comment?.[0] || "",
lyric: common.lyrics?.[0] || "",
type: format.codec,
fileName: String(fileName),
name: String(common.title ?? ""),
artist: String(common.artist ?? ""),
album: String(common.album ?? ""),
alia: String(common.comment?.[0]?.text ?? ""),
lyric: String(lyric ?? ""),
type: String(format.codec ?? ""),
duration: format.duration ? Number(format.duration.toFixed(2)) : 0,
size: fileSize,
br: format.bitrate ? Math.floor(format.bitrate / 1000 || 0) : 0,
frequency: format.sampleRate,
md5,
md5: String(md5),
};
// 获取封面
const coverBuff = common.picture?.[0]?.data || "";
const coverType = common.picture?.[0]?.format || "";
if (coverBuff) coverData.value = blob.createBlobURL(coverBuff, coverType, path);
if (coverBuff) coverData.value = blob.createBlobURL(coverBuff as Buffer, coverType, path);
};
// 在线匹配
@@ -262,7 +262,7 @@ const onlineMatch = debounce(
const changeCover = async () => {
const newPath = await window.electron.ipcRenderer.invoke("choose-image");
if (!newPath) return;
coverData.value = newPath;
coverData.value = `file://${newPath}`;
};
// 实时修改列表
@@ -300,7 +300,9 @@ const saveSongInfo = debounce(async (song: SongType) => {
cover:
coverData.value.startsWith("blob:") || coverData.value === "/images/song.jpg?assest"
? null
: coverData.value,
: coverData.value.startsWith("file://")
? coverData.value.replace(/^file:\/\//, "")
: coverData.value,
};
console.log(song.path, metadata);
await window.electron.ipcRenderer.invoke("set-music-metadata", song.path, metadata);

View File

@@ -117,7 +117,7 @@
</template>
<script setup lang="ts">
import { isElectron } from "@/utils/helper";
import { isElectron } from "@/utils/env";
const emit = defineEmits<{
close: [];
@@ -130,7 +130,7 @@ const isReadOver = useElementVisibility(readOverRef);
// 关闭软件
const closeApp = () => {
window.electron.ipcRenderer.send("win-close");
window.electron.ipcRenderer.send("quit-app");
};
</script>

View File

@@ -18,6 +18,10 @@
</template>
<script setup lang="ts">
import { useSettingStore } from "@/stores";
const settingStore = useSettingStore();
const props = defineProps<{
// 开始时间
start: number;
@@ -31,6 +35,7 @@ const props = defineProps<{
// 是否显示
const isShow = computed(() => {
if (!settingStore.countDownShow) return false;
// 计算实时时间 - 0.5是否小于开始 + 持续时间,小于则显示,否则不显示
return props.seek + 0.5 < props.start + props.duration;
});

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