Compare commits

...

291 Commits

Author SHA1 Message Date
imsyy
ea822f91e8 🐞 fix: 修复失败后无法尝试下一曲 2025-11-18 16:12:23 +08:00
imsyy
5e260ffc0d 🐞 fix: 修复进度调节单位错误 2025-11-18 15:29:37 +08:00
imsyy
e3dcde71e8 Merge branch 'dev-lyric' into dev 2025-11-18 15:22:25 +08:00
imsyy
39c35e8a31 🦄 refactor: 重构为毫秒单位 2025-11-18 15:08:15 +08:00
imsyy
772f6552e7 🦄 refactor: 基础适配新格式 2025-11-18 00:16:59 +08:00
imsyy
c9f3553806 🐞 fix: 修复歌词管理引用错误 2025-11-17 18:15:28 +08:00
底层用户
7de1355f18 Merge pull request #573 from MoYingJi/docs-it
docs: 小改 Issue Template
2025-11-17 14:19:59 +08:00
imsyy
392c64f06b feat: 完善歌词模块 2025-11-16 23:35:52 +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
imsyy
6d367f1fd5 🧪 test: lyric 重构准备 2025-11-14 00:39:19 +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
imsyy
15b500806a feat: 完善自动登录 2024-12-12 11:37:29 +08:00
imsyy
9accf5d27d 🐞 fix: 修复音量调节 2024-12-11 17:57:56 +08:00
imsyy
191ab29a44 feat: 完善部分配置及页面 2024-12-11 17:12:59 +08:00
imsyy
a4d4cd5f70 feat: 支持软件内登录 2024-12-11 10:38:15 +08:00
imsyy
3b07f7346f feat: 新增副歌时间展示 2024-12-10 17:45:12 +08:00
Naihe
1edceeebdd Update idMeta.json
增加会员雷达,里面的每日推荐歌曲质量还不错
https://music.163.com/#/playlist?id=8402996200
2024-12-09 13:23:10 +08:00
imsyy
fbf261f80b Merge branch 'dev' of github.com:imsyy/SPlayer into dev 2024-12-02 16:02:13 +08:00
imsyy
432fa18299 🌈 style: 修改部分播放样式 #290 2024-12-02 16:02:02 +08:00
底层用户
b2dcec840b Merge pull request #301 from serious-snow/dev
refactor(music): 优化音乐播放逻辑和数据处理
2024-12-02 16:00:11 +08:00
imsyy
6f93d65f02 🔧 build: change version 2024-12-02 11:48:18 +08:00
imsyy
852b901353 🐞 fix: 修复歌单歌曲数量异常 #303 2024-12-02 11:38:21 +08:00
imsyy
71a282157d 🐞 fix: 修复下载路径分隔符 #302 2024-12-02 10:04:06 +08:00
wangjian
49f462cdf5 refactor(music): 优化音乐播放逻辑和数据处理
-改进 setNextPlaySong 方法,处理空播放列表的情况
-优化重复歌曲移除逻辑,确保插入位置正确
- 调整音乐数据重置方式,使用对象展开运算符
-增加播放器加载状态检查,避免未准备好时操作
-改进歌曲播放逻辑,处理添加到播放列表后立即播放的情况- 优化播放索引切换,考虑当前播放状态
-调整播放列表移除歌曲逻辑,优化用户体验
- 将初始 playIndex 设置为 -1,表示未播放状态
2024-11-29 14:28:22 +08:00
imsyy
94c0ca70e1 🐞 fix: 修复 snap 无法正常启动 #299 2024-11-27 14:30:24 +08:00
imsyy
813637762c 🔧 build: update dependency 2024-11-05 10:33:09 +08:00
imsyy
eb39b85a8b 🐞 fix: 修复启动失败 2024-11-05 09:49:39 +08:00
底层用户
201fd8d687 Merge pull request #288 from jcfun/dev
fix🐛: 修复了歌词翻译和音译开关不生效的bug
2024-11-04 18:17:53 +08:00
底层用户
02116d8f0f Merge branch 'dev' into dev 2024-11-04 18:16:49 +08:00
jcfun
551a190edf fix🐛: 修复了歌词翻译和音译开关不生效的bug 2024-11-04 12:29:27 +08:00
底层用户
f5015a4028 Merge pull request #279 from FrzMtrsprt/lyric-pos
🦄 refactor: 初始化桌面歌词至屏幕下方
2024-10-21 09:09:33 +08:00
FrzMtrsprt
ea55616bd6 🦄 refactor: 初始化桌面歌词至屏幕下方 2024-10-20 17:17:31 -04:00
imsyy
2b8eb93404 🐞 fix: fix some bugs #275 2024-10-18 15:50:24 +08:00
imsyy
18113d94e9 🐞 fix: 修复 UID 登录模式部分功能异常 2024-10-16 16:19:33 +08:00
imsyy
a8f01d5728 🐞 fix: UID 模式歌单歌曲获取数量过少 2024-10-15 10:51:07 +08:00
imsyy
3a6f8e7462 🐞 fix: 修复 Cookie 登录状态过期过快 2024-10-15 09:53:17 +08:00
imsyy
57325bfc46 feat: 支持 Cookie 登录 2024-10-14 16:18:22 +08:00
imsyy
a3e8931460 feat: 新增 UID 登录模式
- 修复未登录状态下动态封面报错 #274
- 修复海外手机号无法登录 #273
2024-10-14 15:05:14 +08:00
imsyy
2e0f448f66 🐞 fix: 修复音频播放问题 #240 2024-10-11 18:02:16 +08:00
imsyy
44cfbbf4e0 feat: 支持动态封面 2024-10-11 14:39:31 +08:00
imsyy
963343c39e 🔧 build: 修复构建 2024-10-10 17:55:28 +08:00
imsyy
a697799f48 Merge branch 'dev' of github.com:imsyy/SPlayer into dev 2024-10-09 17:54:10 +08:00
imsyy
7c59cc2ccb feat: 切换虚拟列表组件 2024-10-09 17:53:44 +08:00
底层用户
11acbcf7eb Merge pull request #269 from jcfun/dev
fix🐛: 修复了linux6.1内核下硬件加速被错误关闭的bug
2024-10-06 23:48:35 +08:00
imsyy
c35ede2158 🔧 build: 更新依赖 2024-10-05 11:29:33 +08:00
imsyy
dc480459eb 🐞 fix: 同步上游接口以解决登录风控问题 #270 2024-10-05 11:13:25 +08:00
jcfun
c100bfed8d fix🐛: 修复了linux6.1内核下硬件加速被错误关闭的bug 2024-10-03 12:28:23 +08:00
imsyy
2b48713565 feat: 搜索栏跳转完善 #262 2024-09-29 11:52:24 +08:00
imsyy
528f9b0aa6 🐞 fix: 修复快捷键错误 #264 2024-09-29 10:55:21 +08:00
imsyy
0e0bde89bb feat: 支持批量删除歌曲 2024-09-27 18:24:48 +08:00
imsyy
acf60b8b75 🐳 chore: 支持 Docker 多平台 2024-09-27 16:53:09 +08:00
imsyy
3b5338d582 🌈 style: 更新样式优化 2024-09-27 16:14:38 +08:00
imsyy
0c82d6a096 feat: 完善本地目录管理 2024-09-27 11:34:53 +08:00
imsyy
50374e173e 🐞 fix: 托盘图标无法显示 #260 2024-09-27 09:13:02 +08:00
imsyy
62c9dc33db feat: 托盘菜单完善 2024-09-26 15:24:25 +08:00
imsyy
2b6d68ecbd 🦄 refactor: convert to TypeScript #174 2024-09-26 11:57:23 +08:00
imsyy
8a842aa6d6 🐞 fix: 修复海外地区播放问题 #238 2024-08-23 11:32:54 +08:00
底层用户
b0a08ce1b8 Merge pull request #214 from skykeyjoker/dev
更新Readme,增加一点该有的致谢🙏
2024-07-22 17:46:42 +08:00
底层用户
77ab94a59a 📃 docs: Update README.md 2024-07-22 17:45:47 +08:00
Skykey
2b65269ba9 Update README.md 2024-07-18 13:17:00 +08:00
imsyy
7ed33919ab 🔧 build: 支持 Mac Inter 部署 #203 2024-07-08 14:02:29 +08:00
imsyy
d20512e662 🔧 build: 支持 Mac Inter 部署 #203 2024-07-08 13:44:50 +08:00
底层用户
9423d2bf9e Merge pull request #186 from jcfun/dev
- 修复linux下无法记录窗口大小和位置的问题
- 修复主进程端口占用时无法自动切换端口的问题
- 统一程序关闭按钮的效果
2024-06-21 09:10:58 +08:00
jcfun
54fb8e74a8 fix: 修复linux下无法记录窗口大小和位置的问题; 修复主进程端口占用时无法自动切换端口的问题; feat: 统一程序关闭按钮的效果 2024-06-21 00:11:47 +08:00
imsyy
7d3d7696da 🐞 fix: 播放器展开时可拖拽 #168 2024-06-18 09:33:21 +08:00
imsyy
e66ba52889 🐞 fix: 修复酷我音源失效 2024-06-06 09:34:52 +08:00
imsyy
b146dc011e feat: 自动切换可用端口 2024-06-04 15:03:02 +08:00
imsyy
ad2422d826 🐞 fix: 修复酷我解灰失效 2024-05-28 09:47:33 +08:00
imsyy
6b9ba74c35 🦄 refactor: 图片大小优化 2024-05-24 11:36:18 +08:00
imsyy
22e2653e20 🐞 fix: 修复图片 2024-05-24 11:19:20 +08:00
imsyy
4cac54c84a feat: 电台模式完善 2024-05-24 11:01:42 +08:00
imsyy
e5f9ecd7b5 ↩ revert: 回退部分修改 2024-05-24 10:47:14 +08:00
imsyy
d64cfb40ec Revert "🐎 ci: 修复工作流"
This reverts commit f2935f3c1c.
2024-05-24 10:40:36 +08:00
imsyy
53c30caf00 Merge branch 'dev' of github.com:imsyy/SPlayer into dev 2024-05-24 10:27:04 +08:00
imsyy
6fcce91b2b ↩ revert: 回退 2024-05-24 10:26:53 +08:00
imsyy
5a53dfcdf7 🐞 fix: 修正图片 2024-05-24 10:09:43 +08:00
imsyy
bbe71bd62d feat: 支持查看电台节目评论 2024-05-23 18:01:16 +08:00
imsyy
ecedf0b7b9 🦄 refactor: add .gitattributes 2024-05-23 15:21:08 +08:00
imsyy
f2935f3c1c 🐎 ci: 修复工作流 2024-05-10 16:55:15 +08:00
imsyy
d3e677d494 🐎 ci: 修复构建工作流 2024-05-10 16:24:51 +08:00
imsyy
e49f90b0da feat: 电台模式下可展开播放器 2024-05-10 16:11:31 +08:00
imsyy
0359b0e470 🔧 build: 更新依赖 2024-05-06 11:24:15 +08:00
imsyy
aa2373695b 🔧 build: 去除无关文件 2024-04-29 17:47:29 +08:00
imsyy
cf940ff405 🌈 style: 优化部分样式 2024-04-29 17:43:56 +08:00
imsyy
e7c17cf531 🐞 fix: 修复偶发性客户端状态栏异常 2024-04-26 10:02:25 +08:00
imsyy
c6a1ca4f42 feat: 新增自定义字体 #156 & 网络代理 #159 2024-04-25 18:17:50 +08:00
imsyy
8f5008df69 🐞 fix: 修复大部分历史遗留错误 #144
- 修复客户端导航栏显示异常的问题
- 优化频谱动画的显示效果
- 修复Tab栏移动效果 #144
- 优化字体粗体时的显示效果 #156
2024-04-16 17:51:15 +08:00
imsyy
428ce4be86 feat: 支持配置自动更新开关 #148 2024-03-21 14:11:49 +08:00
imsyy
d46c4c4285 🐞 fix: 修复标题栏异常消失 #142 2024-03-14 13:41:31 +08:00
imsyy
0b871175b2 🐞 fix: 修复标题栏异常消失 #142 2024-03-14 11:57:29 +08:00
imsyy
c34c4fd880 📃 docs: update Docs 2024-03-05 18:00:26 +08:00
imsyy
ff00f0c283 🐞 fix: 修复标题栏无法正常显示 #142 2024-02-22 15:18:26 +08:00
imsyy
847c2e5810 📃 docs: 更新说明 2024-01-25 09:22:11 +08:00
imsyy
e62c81bb33 🐞 fix: 修复电台未加载全部节目 2024-01-18 16:25:03 +08:00
imsyy
984fdb3459 📃 docs: 更新说明 2024-01-18 11:45:46 +08:00
imsyy
019b78bf38 🐞 fix: 修复歌单无法删除歌曲 2024-01-13 10:23:40 +08:00
imsyy
cf88c7669f 📃 docs: 更新说明 2024-01-12 11:14:09 +08:00
imsyy
f4383ba848 feat: 播放页面支持调节音量 #124 2024-01-12 11:08:35 +08:00
imsyy
adbda459ba 🐞 fix: 修复下载歌曲元信息不正确导致无法正常播放 #113 2024-01-11 16:09:09 +08:00
imsyy
984d747179 🐞 fix: 修复本地歌词翻译显示异常 #121 2024-01-11 11:39:29 +08:00
653 changed files with 42169 additions and 31562 deletions

View File

@@ -1,38 +1,6 @@
# 程序配置
## 程序名称
MAIN_VITE_TITLE = "SPlayer"
## 程序主端口
MAIN_VITE_MAIN_PORT = 7899
## 程序开发环境运行端口
MAIN_VITE_DEV_PORT = 6944
# 全局 API 配置
## API 运行地址
MAIN_VITE_SERVER_HOST = 127.0.0.1
## API 运行端口
MAIN_VITE_SERVER_PORT = 11451
## API 在线地址( 网址结尾不要加 /
### 用于非客户端( 浏览器环境
RENDERER_VITE_SERVER_URL = /api
# 程序信息
RENDERER_VITE_SITE_TITLE = "SPlayer"
RENDERER_VITE_SITE_ANTHOR = "無名"
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
RENDERER_VITE_SITE_URL = "imsyy.top"
# Cookie
## 咪咕音乐 Cookie
MAIN_VITE_MIGU_COOKIE = ""
# 公告配置
## 若无需公告,请将标题或内容任意一项设为空即可
## 公告类型
RENDERER_VITE_ANN_TYPE = "info"
## 公告标题
RENDERER_VITE_ANN_TITLE = ""
## 公告内容
RENDERER_VITE_ANN_CONTENT = ""
## 公告时长(毫秒)不可超过 999999
RENDERER_VITE_ANN_DURATION = 8000
## WEB 端口
VITE_WEB_PORT = 14558
## API 端口
VITE_SERVER_PORT = 25884
## API 地址 - 结尾不要加 /
VITE_API_URL = /api/netease

View File

@@ -1,6 +0,0 @@
node_modules
dist
out
.gitignore
auto-imports.d.ts
components.d.ts

304
.eslintrc-auto-import.json Normal file
View File

@@ -0,0 +1,304 @@
{
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"VNode": true,
"WritableComputedRef": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"injectLocal": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"pausableWatch": true,
"provide": true,
"provideLocal": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useClipboardItems": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDialog": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useId": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLoadingBar": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMessage": true,
"useModel": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNotification": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"usePrevious": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useScreenOrientation": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

View File

@@ -1,56 +0,0 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
extends: [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"@electron-toolkit",
"@vue/eslint-config-prettier",
],
rules: {
"vue/v-on-event-hyphenation": "off",
"vue/require-default-prop": "off",
"vue/multi-word-component-names": "off",
"vue/attribute-hyphenation": "off",
},
ignorePatterns: [
"node_modules/",
"build/",
"dist/",
"out/",
"components.d.ts",
"auto-imports.d.ts",
],
globals: {
defineProps: true,
defineEmits: true,
withDefaults: true,
h: true,
vue: true,
ref: true,
reactive: true,
computed: true,
watch: true,
provide: true,
inject: true,
defineComponent: true,
onBeforeMount: true,
onBeforeUnmount: true,
onUnmounted: true,
onMounted: true,
nextTick: true,
watchEffect: true,
electron: true,
$message: true,
$dialog: true,
$loadingBar: true,
$changeLogin: true,
$notification: true,
$changeThemeColor: true,
$canNotConnect: true,
$refreshCloudCatch: true,
$cleanAll: true,
$player: true,
},
};

View File

@@ -1,17 +0,0 @@
name: 添加功能
description: 请填写希望添加的功能的具体信息
title: 【添加功能】请填写标题
labels: [add]
body:
- type: input
id: name
validations:
required: true
attributes:
label: "希望添加什么功能?"
placeholder: "请填写功能名称"
- type: textarea
id: other
attributes:
label: "具体信息"
description: "请详细描述希望添加的功能的具体信息"

View File

@@ -1,34 +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,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 添加功能
url: https://github.com/imsyy/SPlayer/discussions/new?category=%E6%83%B3%E6%B3%95-ideas
about: 新的功能建议和提问答疑请到讨论区发起
- name: 转到讨论区
url: https://github.com/imsyy/SPlayer/discussions
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起

View File

@@ -1,49 +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.1.1
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4.0.0
with:
node-version: "18.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 rimraf "dist/!(*.exe)"
# 上传构建产物
- name: Upload artifacts
uses: actions/upload-artifact@v3.1.3
with:
name: SPlayer-dev
path: dist

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

@@ -3,6 +3,7 @@ name: Publish Docker image
on:
release:
types: [published]
workflow_dispatch:
jobs:
push_to_registry:
@@ -15,6 +16,12 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
@@ -41,6 +48,9 @@ jobs:
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@@ -1,154 +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
timeout-minutes: 30
# ===================================================================
# 并行构建所有平台和架构
# ===================================================================
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.1.1
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4.0.0
uses: actions/checkout@v4
# 设置 pnpm 版本
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
node-version: "18.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
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# 上传构建产物
- name: Upload Windows artifact
uses: actions/upload-artifact@v3.1.3
with:
name: SPlarer-Win
if-no-files-found: ignore
path: dist/*.*
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v0.1.15
if: startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: dist/*.*
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# Mac
build-macos:
name: Build for macOS
runs-on: macos-latest
timeout-minutes: 30
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4.1.1
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4.0.0
with:
node-version: "18.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@v3.1.3
with:
name: SPlarer-Macos
if-no-files-found: ignore
path: dist/*.*
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v0.1.15
if: startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: dist/*.*
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
# Linux
build-linux:
name: Build for Linux
runs-on: ubuntu-22.04
timeout-minutes: 30
steps:
# 检出 Git 仓库
- name: Check out git repository
uses: actions/checkout@v4.1.1
# 安装 Node.js
- name: Install Node.js
uses: actions/setup-node@v4.0.0
with:
node-version: "18.x"
# 更新 Ubuntu 软件源
- name: Ubuntu Update with sudo
if: runner.os == 'Linux'
run: sudo apt-get update
# 复制环境变量文件
- name: Copy .env.example
# 安装依赖
- name: Install RPM & Pacman
if: 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
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
- name: Install Snapcraft
if: runner.os == 'Linux'
uses: samuelmeuli/action-snapcraft@v2
# 构建 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 }}
# 上传构建产物
- name: Upload Linux artifact
uses: actions/upload-artifact@v3.1.3
with:
name: SPlarer-Linux
if-no-files-found: ignore
path: dist/*.*
# 创建 GitHub Release
- name: Release
uses: softprops/action-gh-release@v0.1.15
if: startsWith(github.ref, 'refs/tags/v')
with:
draft: true
files: dist/*.*
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Linux x64 & ARM64 App
if: runner.os == 'Linux'
run: pnpm build:linux || true
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# 上传 Snap 包到 Snapcraft 商店
- name: Publish Snap to Snap Store
if: runner.os == 'Linux'
run: snapcraft upload dist/*.snap --release stable
continue-on-error: true
# 合并所有构建
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: SPlayer-${{ runner.os }}
path: dist/*.*
# ===================================================================
# 收集并发布 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:
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

7
.gitignore vendored
View File

@@ -15,9 +15,8 @@ coverage
*.local
out
.env
/cypress/videos/
/cypress/screenshots/
auto-imports.d.ts
components.d.ts
# Editor directories and files
.vscode/*
@@ -29,3 +28,5 @@ out
*.njsproj
*.sln
*.sw?
.env.development
.env.production

4
.npmrc
View File

@@ -1,5 +1,5 @@
registry=https://registry.npmmirror.com
disturl=https://registry.npmmirror.com/-/binary/node
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
shamefully-hoist=true

View File

@@ -2,7 +2,7 @@ out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json
auto-imports.d.ts
components.d.ts
# tsconfig.json
# tsconfig.*.json

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"printWidth": 100
}

View File

@@ -1,8 +0,0 @@
# 是否使用单引号而不是双引号
singleQuote: false
# 是否在语句末尾使用分号
semi: true
# 每行的最大打印宽度
printWidth: 100
# 是否在对象和数组的末尾加上逗号
trailingComma: all

View File

@@ -1,5 +1,5 @@
# build
FROM node:18-alpine as builder
FROM node:20-alpine AS builder
RUN apk update && apk add --no-cache git
@@ -11,19 +11,28 @@ RUN npm install
COPY . .
# add .env.example to .env
RUN [ ! -e ".env" ] && cp .env.example .env || true
RUN npm run build
# nginx
FROM nginx:1.25.3-alpine-slim as app
FROM nginx:1.27-alpine-slim AS app
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"]

248
README.md
View File

@@ -1,10 +1,14 @@
<div align="center">
<img alt="logo" height="80" src="./public/images/icons/favicon.png" />
<h2>SPlayer</h2>
<p>一个简约的音乐播放器</p>
<img alt="main" src="./screenshots/main.png" />
</div>
<br />
# 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)
## 说明
@@ -14,16 +18,16 @@
>
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
> - **禁止用于售卖或其他盈利用途**,如若发现,作者保留追究法律责任的权利
> - 禁止在二开项目中修改程序原版权信息( 您可以添加二开作者信息
> - 感谢您的尊重与理解
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
- 本项目采用 [Vue 3](https://cn.vuejs.org/) + [TypeScript](https://www.typescriptlang.org/) + [Naïve UI](https://www.naiveui.com/) + [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行解决兼容性后进行构建
- 仅对移动端做了基础适配,**不保证功能全部可用**
> 请注意,本程序不打算开发移动端,也不会对移动端进行完美适配,仅保证基础可用性
- 欢迎各位大佬 `Star` 😍
## 👀 Demo
@@ -35,10 +39,14 @@
- ✨ 支持扫码登录
- 📱 支持手机号登录
- 📅 自动进行每日签到及云贝签到
- 🎨 封面主题色自适应
- 💻 支持桌面歌词
- 💻 支持切换为本地播放器,此模式将不会连接网络
- 🎨 封面主题色自适应,支持全站着色
- 🌚 Light / Dark / Auto 模式自动切换
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
- 📁 简易的本地音乐标签编辑及封面修改
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
- ⬇️ 下载歌曲(最高支持 Hi-Res
- ⬇️ 下载歌曲( 最高支持 Hi-Res,需具有相应会员账号
- 新建歌单及歌单编辑
- ❤️ 收藏 / 取消收藏歌单或歌手
- 🎶 每日推荐歌曲
@@ -53,12 +61,11 @@
- 🎶 音乐频谱显示
- ⏭️ 音乐渐入渐出
- 🔄 支持 PWA
- 💬 支持评论区及评论点赞
- 🌓 明暗模式自动 / 手动切换
- 💬 支持评论区
- 📱 移动端基础适配
- ~~🌐 `i18n` 支持~~
## 🖼️ Screenshots
## 🖼️ screenshots
> 开发中,仅供参考
@@ -116,6 +123,10 @@
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
## Snap Store
[![Get it from the Snap Store](https://snapcraft.io/en/dark/install.svg)](https://snapcraft.io/splayer)
## ⚙️ Docker 部署
> 安装及配置 `Docker` 将不在此处说明,请自行解决
@@ -129,7 +140,7 @@
docker build -t splayer .
# 运行
docker run -d --name SPlayer -p 7899:7899 splayer
docker run -d --name SPlayer -p 25884:25884 splayer
# 或使用 Docker Compose
docker-compose up -d
```
@@ -137,13 +148,16 @@ docker-compose up -d
### 在线部署
```bash
# 拉取
# 从 Docker Hub 拉取
docker pull imsyy/splayer:latest
# 从 GitHub ghcr 拉取
docker pull ghcr.io/imsyy/splayer:latest
# 运行
docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
docker run -d --name SPlayer -p 25884:25884 imsyy/splayer:latest
```
以上步骤成功后,将会在本地 [localhost:7899](http://localhost:7899/) 启动,如需更换端口,请自行修改命令行中的端口号
以上步骤成功后,将会在本地 [localhost:25884](http://localhost:25884/) 启动,如需更换端口,请自行修改命令行中的端口号
## ⚙️ Vercel 部署
@@ -152,15 +166,15 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
3. 复制 `/.env.example` 文件并重命名为 `/.env`
4.`.env` 文件中的 `RENDERER_VITE_SERVER_URL` 改为第一步得到的 API 地址
4.`.env` 文件中的 `VITE_API_URL` 改为第一步得到的 API 地址
```js
RENDERER_VITE_SERVER_URL = "https://example.com";
VITE_API_URL = "https://example.com";
```
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
![build](/screenshots/build.png)
![build](/screenshots/build.jpg)
6. 点击 `Deploy`,即可成功部署
@@ -169,19 +183,17 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
2. 克隆仓库
> 将链接中的 example/repository.git 替换为你要克隆的实际仓库的地址
```bash
git clone https://github.com/example/repository.git
git clone https://github.com/imsyy/SPlayer.git
```
3. 安装依赖
```bash
pnpm install
# 或
# 或
yarn install
# 或
# 或
npm install
```
@@ -189,9 +201,9 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
```bash
pnpm build
# 或
# 或
yarn build
# 或
# 或
npm build
```
@@ -224,7 +236,10 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
- [applemusic-like-lyrics](https://github.com/Steve-xmh/applemusic-like-lyrics)
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
- [material-color-utilities](https://github.com/material-foundation/material-color-utilities)
## 📢 免责声明
@@ -247,179 +262,6 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
## 📂 目录结构
<details>
<summary>查看目录结构详情</summary>
> ChatGPT 写的,如有错误,请见谅
```dir
├── auto-imports.d.ts # 自动导入
├── components.d.ts # 自动导入
├── docker-compose.yml # Docker Compose
├── Dockerfile # Docker
├── electron # Electron
│   ├── main # Electron 主进程
│   │   ├── index.js # 主进程入口
│   │   ├── mainIpcMain.js # 主进程与渲染进程通信
│   │   ├── startMainServer.js # 启动主进程服务器
│   │   ├── startNcmServer.js # 启动网易云音乐服务
│   │   └── utils # 主进程工具函数
│   │   ├── checkUpdates.js # 检查更新
│   │   ├── createGlobalShortcut.js # 创建全局快捷键
│   │   ├── createSystemTray.js # 创建系统托盘
│   │   ├── getNeteaseMusicUrl.js # 解灰
│   │   ├── kwDES.js # DES加密算法
│   │   └── readDirAsync.js # 异步读取目录
│   └── preload # Electron 预加载脚本
│   └── index.js # 预加载脚本入口文件
├── electron-builder.yml # Electron Builder
├── electron.vite.config.js # Electron Vite
├── index.html # 主页面 HTML
├── LICENSE # 项目许可证
├── nginx.conf # Nginx 配置
├── src # 项目源代码
│   ├── api # API 相关
│   │   ├── ./..
│   ├── App.vue # 根组件
│   ├── assets # 静态资源
│   │   ├── emoji.json # 表情数据
│   │   ├── icon.json # 图标数据
│   │   └── themeColor.json # 主题颜色数据
│   ├── components # 组件目录
│   │   ├── Cover # 封面相关组件目录
│   │   │   ├── CoverDropdown.vue # 封面下拉组件
│   │   │   ├── MainCover.vue # 主封面组件
│   │   │   ├── SpecialCoverCard.vue # 特殊封面卡片组件
│   │   │   └── SpecialCover.vue # 特殊封面组件
│   │   ├── Global # 全局组件目录
│   │   │   ├── MainLayout.vue # 主布局组件
│   │   │   ├── Menu.vue # 菜单组件
│   │   │   ├── Pagination.vue # 分页组件
│   │   │   ├── Playlist.vue # 歌单组件
│   │   │   ├── Provider.vue # 全局化配置组件
│   │   │   └── SvgIcon.vue # SVG 图标组件
│   │   ├── List # 列表组件目录
│   │   │   ├── CommentList.vue # 评论列表组件
│   │   │   ├── SongListDropdown.vue # 歌曲下拉组件
│   │   │   └── SongList.vue # 歌曲列表组件
│   │   ├── Modal # 弹窗相关组件目录
│   │   │   ├── AddPlaylist.vue # 添加歌单组件
│   │   │   ├── CloudSongMatch.vue # 云盘歌曲匹配组件
│   │   │   ├── CreatePlaylist.vue # 创建歌单组件
│   │   │   ├── DownloadSong.vue # 下载歌曲组件
│   │   │   ├── LoginPhone.vue # 手机登录组件
│   │   │   ├── LoginQRCode.vue # 二维码登录组件
│   │   │   ├── Login.vue # 登录组件
│   │   │   ├── PlaylistUpdate.vue # 歌单编辑组件
│   │   │   └── UpCloudSong.vue # 上传云盘歌曲组件
│   │   ├── Nav # 导航相关组件目录
│   │   │   ├── MainNav.vue # 主导航组件
│   │   │   └── UserData.vue # 用户数据组件
│   │   ├── Player # 播放器相关组件目录
│   │   │   ├── CountDown.vue # 倒计时组件
│   │   │   ├── FullPlayer.vue # 全屏播放器组件
│   │   │   ├── Lyric.vue # 歌词组件
│   │   │   ├── MainControl.vue # 主控制组件
│   │   │   ├── PlayerControl.vue # 播放器控制组件
│   │   │   ├── PlayerCover.vue # 播放器封面组件
│ │ │ └── PrivateFm.vue # 私人 FM 组件
│ │ ├── Search # 搜索相关组件
│ │ │ ├── SearchHot.vue # 热门搜索组件
│ │ │ ├── SearchInp.vue # 搜索输入组件
│ │ │ └── SearchSuggestions.vue # 搜索建议组件
│ │ └── WinDom # 窗口 DOM 相关组件
│ │ └── TitleBar.vue # 标题栏组件
│ ├── main.js # Vue 应用的入口文件
│ ├── router # Vue Router 相关文件夹
│ │ ├── index.js # Vue Router 入口文件
│ │ └── routes.js # 路由配置文件
│ ├── stores # Vuex Store 相关文件夹
│ │ ├── indexedDB.js # IndexedDB 数据库相关文件
│ │ ├── index.js # Vuex Store 入口文件
│ │ ├── musicData.js # 音乐数据相关文件
│ │ ├── siteData.js # 网站数据相关文件
│ │ ├── siteSettings.js # 网站设置相关文件
│ │ └── siteStatus.js # 网站状态相关文件
│ ├── style # 样式相关文件夹
│ │ ├── animate.scss # 动画样式文件
│ │ └── main.scss # 主样式文件
│ ├── utils # 工具函数文件夹
│ │ ├── auth.js # 认证相关函数
│ │ ├── base64.js # Base64编码解码相关函数
│ │ ├── color-utils.js # 颜色工具函数
│ │ ├── cover-color.js # 封面颜色相关函数
│ │ ├── debounce.js # 防抖函数
│ │ ├── formatData.js # 数据格式化函数
│ │ ├── formRules.js # 表单验证规则
│ │ ├── globalEvents.js # 全局事件处理函数
│ │ ├── globalShortcut.js # 全局快捷键相关函数
│ │ ├── helper.js # 辅助函数
│ │ ├── parseLyric.js # 解析歌词函数
│ │ ├── Player.js # 播放器控制相关函数
│ │ ├── request.js # 网络请求相关函数
│ │ ├── throttle.js # 节流函数
│ │ ├── timeTools.js # 时间工具函数
│ │ └── userSignIn.js # 用户登录相关函数
│ └── views # Vue组件文件夹
│ ├── Artist # 艺术家相关组件
│ │ ├── albums.vue # 艺术家专辑组件
│ │ ├── hot.vue # 艺术家热门组件
│ │ ├── index.vue # 艺术家主组件
│ │ ├── songs.vue # 艺术家歌曲组件
│ │ └── videos.vue # 艺术家视频组件
│ ├── Cloud.vue # 云盘组件
│ ├── Comment.vue # 评论组件
│ ├── DailySongs.vue # 每日推荐组件
│ ├── Discover # 发现音乐相关组件
│ │ ├── artists.vue # 发现音乐艺术家组件
│ │ ├── index.vue # 发现音乐主组件
│ │ ├── new.vue # 发现音乐新歌组件
│ │ ├── playlists.vue # 发现音乐歌单组件
│ │ └── toplists.vue # 发现音乐排行榜组件
│ ├── History.vue # 历史记录组件
│ ├── Home.vue # 主页组件
│ ├── Like # 我喜欢的相关组件
│ │ ├── albums.vue # 我喜欢的专辑组件
│ │ ├── artists.vue # 我喜欢的艺术家组件
│ │ ├── index.vue # 我喜欢的主组件
│ │ ├── playlists.vue # 我喜欢的歌单组件
│ │ └── videos.vue # 我喜欢的视频组件
│ ├── List # 列表相关组件
│ │ ├── album.vue # 专辑组件
│ │ └── playlist.vue # 歌单组件
│ │ └── dj.vue # 电台组件
│ ├── Local # 本地音乐相关组件
│ │ ├── albums.vue # 本地音乐专辑组件
│ │ ├── artists.vue # 本地音乐艺术家组件
│ │ ├── index.vue # 本地音乐主组件
│ │ └── songs.vue # 本地音乐歌曲组件
│ ├── Player.vue # 视频播放器组件
│ ├── Dj # 电台相关组件
│ │ └── index.vue # 电台主组件
│ │ └── type.vue # 电台分类组件
│ ├── Search # 搜索相关组件
│ │ ├── albums.vue # 搜索专辑组件
│ │ ├── artists.vue # 搜索艺术家组件
│   │   ├── index.vue # 搜索主组件
│   │   ├── playlists.vue # 搜索歌单组件
│   │   ├── songs.vue # 搜索歌曲组件
│   │   └── videos.vue # 搜索视频组件
│   │   └── djs.vue # 搜索电台组件
│   ├── Setting # 设置相关组件
│   │   └── index.vue # 设置主组件
│   ├── Song.vue
│   ├── State
│   │   ├── 403.vue
│   │   ├── 404.vue
│   │   └── 500.vue
│   └── Test.vue
└── vercel.json # Vercel 部署配置
```
</details>
## ⭐ Star History
[![Star History Chart](https://api.star-history.com/svg?repos=imsyy/SPlayer&type=Date)](https://star-history.com/#imsyy/SPlayer&Date)

315
auto-eslint.mjs Normal file
View File

@@ -0,0 +1,315 @@
export default {
"globals": {
"Component": true,
"ComponentPublicInstance": true,
"ComputedRef": true,
"DirectiveBinding": true,
"EffectScope": true,
"ExtractDefaultPropTypes": true,
"ExtractPropTypes": true,
"ExtractPublicPropTypes": true,
"InjectionKey": true,
"MaybeRef": true,
"MaybeRefOrGetter": true,
"PropType": true,
"Ref": true,
"ShallowRef": true,
"Slot": true,
"Slots": true,
"VNode": true,
"WritableComputedRef": true,
"asyncComputed": true,
"autoResetRef": true,
"computed": true,
"computedAsync": true,
"computedEager": true,
"computedInject": true,
"computedWithControl": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createInjectionState": true,
"createReactiveFn": true,
"createRef": true,
"createReusableTemplate": true,
"createSharedComposable": true,
"createTemplatePromise": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"eagerComputed": true,
"effectScope": true,
"extendRef": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"getCurrentWatcher": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"injectLocal": true,
"isDefined": true,
"isProxy": true,
"isReactive": true,
"isReadonly": true,
"isRef": true,
"isShallow": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeRouteLeave": true,
"onBeforeRouteUpdate": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onElementRemoval": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onLongPress": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"onWatcherCleanup": true,
"pausableWatch": true,
"provide": true,
"provideLocal": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactiveComputed": true,
"reactiveOmit": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refAutoReset": true,
"refDebounced": true,
"refDefault": true,
"refThrottled": true,
"refWithControl": true,
"resolveComponent": true,
"resolveRef": true,
"resolveUnref": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"syncRef": true,
"syncRefs": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"toValue": true,
"triggerRef": true,
"tryOnBeforeMount": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAnimate": true,
"useArrayDifference": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayFindLast": true,
"useArrayIncludes": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useArrayUnique": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBluetooth": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClipboard": true,
"useClipboardItems": true,
"useCloned": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCountdown": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCurrentElement": true,
"useCycleList": true,
"useDark": true,
"useDateFormat": true,
"useDebounce": true,
"useDebounceFn": true,
"useDebouncedRefHistory": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDialog": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useDropZone": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFileDialog": true,
"useFileSystemAccess": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGamepad": true,
"useGeolocation": true,
"useId": true,
"useIdle": true,
"useImage": true,
"useInfiniteScroll": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLink": true,
"useLoadingBar": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMessage": true,
"useModel": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNotification": true,
"useNow": true,
"useObjectUrl": true,
"useOffsetPagination": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"useParentElement": true,
"usePerformanceObserver": true,
"usePermission": true,
"usePointer": true,
"usePointerLock": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredContrast": true,
"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,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSorted": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStepper": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRef": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
"useThrottleFn": true,
"useThrottledRefHistory": true,
"useTimeAgo": true,
"useTimeAgoIntl": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVModel": true,
"useVModels": true,
"useVibrate": true,
"useVirtualList": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchArray": true,
"watchAtMost": true,
"watchDebounced": true,
"watchDeep": true,
"watchEffect": true,
"watchIgnorable": true,
"watchImmediate": true,
"watchOnce": true,
"watchPausable": true,
"watchPostEffect": true,
"watchSyncEffect": true,
"watchThrottled": true,
"watchTriggerable": true,
"watchWithFilter": true,
"whenever": true
}
}

353
auto-imports.d.ts vendored
View File

@@ -3,68 +3,307 @@
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useNotification: typeof import('naive-ui')['useNotification']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
const EffectScope: typeof import('vue').EffectScope
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
const computedInject: typeof import('@vueuse/core').computedInject
const computedWithControl: typeof import('@vueuse/core').computedWithControl
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createEventHook: typeof import('@vueuse/core').createEventHook
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
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const debouncedRef: typeof import('@vueuse/core').debouncedRef
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const eagerComputed: typeof import('@vueuse/core').eagerComputed
const effectScope: typeof import('vue').effectScope
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
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDefined: typeof import('@vueuse/core').isDefined
const isProxy: typeof import('vue').isProxy
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
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
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
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onStartTyping: typeof import('@vueuse/core').onStartTyping
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const reactify: typeof import('@vueuse/core').reactify
const reactifyObject: typeof import('@vueuse/core').reactifyObject
const reactive: typeof import('vue').reactive
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
const reactivePick: typeof import('@vueuse/core').reactivePick
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refAutoReset: typeof import('@vueuse/core').refAutoReset
const refDebounced: typeof import('@vueuse/core').refDebounced
const refDefault: typeof import('@vueuse/core').refDefault
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const resolveUnref: typeof import('@vueuse/core').resolveUnref
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs
const templateRef: typeof import('@vueuse/core').templateRef
const throttledRef: typeof import('@vueuse/core').throttledRef
const throttledWatch: typeof import('@vueuse/core').throttledWatch
const toRaw: typeof import('vue').toRaw
const toReactive: typeof import('@vueuse/core').toReactive
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
const unref: typeof import('vue').unref
const unrefElement: typeof import('@vueuse/core').unrefElement
const until: typeof import('@vueuse/core').until
const useActiveElement: typeof import('@vueuse/core').useActiveElement
const useAnimate: typeof import('@vueuse/core').useAnimate
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
const useArrayFind: typeof import('@vueuse/core').useArrayFind
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
const useArrayMap: typeof import('@vueuse/core').useArrayMap
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
const useArraySome: typeof import('@vueuse/core').useArraySome
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
const useAsyncState: typeof import('@vueuse/core').useAsyncState
const useAttrs: typeof import('vue').useAttrs
const useBase64: typeof import('@vueuse/core').useBase64
const useBattery: typeof import('@vueuse/core').useBattery
const useBluetooth: typeof import('@vueuse/core').useBluetooth
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
const useCached: typeof import('@vueuse/core').useCached
const useClipboard: typeof import('@vueuse/core').useClipboard
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
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
const useCssVars: typeof import('vue').useCssVars
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
const useCycleList: typeof import('@vueuse/core').useCycleList
const useDark: typeof import('@vueuse/core').useDark
const useDateFormat: typeof import('@vueuse/core').useDateFormat
const useDebounce: typeof import('@vueuse/core').useDebounce
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
const useDevicesList: typeof import('@vueuse/core').useDevicesList
const useDialog: typeof import('naive-ui').useDialog
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDropZone: typeof import('@vueuse/core').useDropZone
const useElementBounding: typeof import('@vueuse/core').useElementBounding
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
const useElementHover: typeof import('@vueuse/core').useElementHover
const useElementSize: typeof import('@vueuse/core').useElementSize
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
const useEventBus: typeof import('@vueuse/core').useEventBus
const useEventListener: typeof import('@vueuse/core').useEventListener
const useEventSource: typeof import('@vueuse/core').useEventSource
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
const useFavicon: typeof import('@vueuse/core').useFavicon
const useFetch: typeof import('@vueuse/core').useFetch
const useFileDialog: typeof import('@vueuse/core').useFileDialog
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
const useFocus: typeof import('@vueuse/core').useFocus
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
const useFps: typeof import('@vueuse/core').useFps
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLink: typeof import('vue-router').useLink
const useLoadingBar: typeof import('naive-ui').useLoadingBar
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
const useMediaControls: typeof import('@vueuse/core').useMediaControls
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
const useMemoize: typeof import('@vueuse/core').useMemoize
const useMemory: typeof import('@vueuse/core').useMemory
const useMessage: typeof import('naive-ui').useMessage
const useModel: typeof import('vue').useModel
const useMounted: typeof import('@vueuse/core').useMounted
const useMouse: typeof import('@vueuse/core').useMouse
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork
const useNotification: typeof import('naive-ui').useNotification
const useNow: typeof import('@vueuse/core').useNow
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
const useOnline: typeof import('@vueuse/core').useOnline
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
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
const useScroll: typeof import('@vueuse/core').useScroll
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSlots: typeof import('vue').useSlots
const useSorted: typeof import('@vueuse/core').useSorted
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSupported: typeof import('@vueuse/core').useSupported
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
const useTextDirection: typeof import('@vueuse/core').useTextDirection
const useTextSelection: typeof import('@vueuse/core').useTextSelection
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
const useThrottle: typeof import('@vueuse/core').useThrottle
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
const useTimestamp: typeof import('@vueuse/core').useTimestamp
const useTitle: typeof import('@vueuse/core').useTitle
const useToNumber: typeof import('@vueuse/core').useToNumber
const useToString: typeof import('@vueuse/core').useToString
const useToggle: typeof import('@vueuse/core').useToggle
const useTransition: typeof import('@vueuse/core').useTransition
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
const useUserMedia: typeof import('@vueuse/core').useUserMedia
const useVModel: typeof import('@vueuse/core').useVModel
const useVModels: typeof import('@vueuse/core').useVModels
const useVibrate: typeof import('@vueuse/core').useVibrate
const useVirtualList: typeof import('@vueuse/core').useVirtualList
const useWakeLock: typeof import('@vueuse/core').useWakeLock
const useWebNotification: typeof import('@vueuse/core').useWebNotification
const useWebSocket: typeof import('@vueuse/core').useWebSocket
const useWebWorker: typeof import('@vueuse/core').useWebWorker
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
const useWindowSize: typeof import('@vueuse/core').useWindowSize
const watch: typeof import('vue').watch
const watchArray: typeof import('@vueuse/core').watchArray
const watchAtMost: typeof import('@vueuse/core').watchAtMost
const watchDebounced: typeof import('@vueuse/core').watchDebounced
const watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
const watchOnce: typeof import('@vueuse/core').watchOnce
const watchPausable: typeof import('@vueuse/core').watchPausable
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const watchThrottled: typeof import('@vueuse/core').watchThrottled
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, 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')
}

108
components.d.ts vendored
View File

@@ -1,72 +1,104 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AddPlaylist: typeof import('./src/components/Modal/AddPlaylist.vue')['default']
CloudSongMatch: typeof import('./src/components/Modal/CloudSongMatch.vue')['default']
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
ArtistList: typeof import('./src/components/List/ArtistList.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']
CoverDropdown: typeof import('./src/components/Cover/CoverDropdown.vue')['default']
CoverPlayBtn: typeof import('./src/components/Cover/CoverPlayBtn.vue')['default']
CoverList: typeof import('./src/components/List/CoverList.vue')['default']
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']
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']
Login: typeof import('./src/components/Modal/Login.vue')['default']
LoginPhone: typeof import('./src/components/Modal/LoginPhone.vue')['default']
LoginQRCode: typeof import('./src/components/Modal/LoginQRCode.vue')['default']
Lyric: typeof import('./src/components/Player/Lyric.vue')['default']
MainControl: typeof import('./src/components/Player/MainControl.vue')['default']
MainCover: typeof import('./src/components/Cover/MainCover.vue')['default']
MainLayout: typeof import('./src/components/Global/MainLayout.vue')['default']
MainNav: typeof import('./src/components/Nav/MainNav.vue')['default']
Menu: typeof import('./src/components/Global/Menu.vue')['default']
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
KeyboardSetting: typeof import('./src/components/Setting/KeyboardSetting.vue')['default']
LocalSetting: typeof import('./src/components/Setting/LocalSetting.vue')['default']
Login: typeof import('./src/components/Modal/Login/Login.vue')['default']
LoginCookie: typeof import('./src/components/Modal/Login/LoginCookie.vue')['default']
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']
MainPlayer: typeof import('./src/components/Player/MainPlayer.vue')['default']
MainPlayList: typeof import('./src/components/Player/MainPlayList.vue')['default']
MainSetting: typeof import('./src/components/Setting/MainSetting.vue')['default']
Menu: typeof import('./src/components/Layout/Menu.vue')['default']
NA: typeof import('naive-ui')['NA']
NAlert: typeof import('naive-ui')['NAlert']
Nav: typeof import('./src/components/Layout/Nav.vue')['default']
NAvatar: typeof import('naive-ui')['NAvatar']
NBackTop: typeof import('naive-ui')['NBackTop']
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCollapse: typeof import('naive-ui')['NCollapse']
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
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']
NGi: typeof import('naive-ui')['NGi']
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']
NH4: typeof import('naive-ui')['NH4']
NH6: typeof import('naive-ui')['NH6']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NLi: typeof import('naive-ui')['NLi']
NList: typeof import('naive-ui')['NList']
NListItem: typeof import('naive-ui')['NListItem']
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
NMenu: typeof import('naive-ui')['NMenu']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NModalProvider: typeof import('naive-ui')['NModalProvider']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
NPagination: typeof import('naive-ui')['NPagination']
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']
@@ -86,27 +118,39 @@ declare module 'vue' {
NText: typeof import('naive-ui')['NText']
NThing: typeof import('naive-ui')['NThing']
NVirtualList: typeof import('naive-ui')['NVirtualList']
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
OtherSetting: typeof import('./src/components/Setting/OtherSetting.vue')['default']
PersonalFM: typeof import('./src/components/Player/PersonalFM.vue')['default']
PlayerBackground: typeof import('./src/components/Player/PlayerBackground.vue')['default']
PlayerComment: typeof import('./src/components/Player/PlayerComment.vue')['default']
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
Playlist: typeof import('./src/components/Global/Playlist.vue')['default']
PlaylistUpdate: typeof import('./src/components/Modal/PlaylistUpdate.vue')['default']
PrivateFm: typeof import('./src/components/Player/PrivateFm.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']
Provider: typeof import('./src/components/Global/Provider.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SearchHot: typeof import('./src/components/Search/SearchHot.vue')['default']
SearchDefault: typeof import('./src/components/Search/SearchDefault.vue')['default']
SearchInp: typeof import('./src/components/Search/SearchInp.vue')['default']
SearchSuggestions: typeof import('./src/components/Search/SearchSuggestions.vue')['default']
SearchInpMenu: typeof import('./src/components/Menu/SearchInpMenu.vue')['default']
SearchSuggest: typeof import('./src/components/Search/SearchSuggest.vue')['default']
Sider: typeof import('./src/components/Layout/Sider.vue')['default']
SImage: typeof import('./src/components/UI/s-image.vue')['default']
SongCard: typeof import('./src/components/Card/SongCard.vue')['default']
SongDataCard: typeof import('./src/components/Card/SongDataCard.vue')['default']
SongInfoEditor: typeof import('./src/components/Modal/SongInfoEditor.vue')['default']
SongList: typeof import('./src/components/List/SongList.vue')['default']
SongListDrawer: typeof import('./src/components/List/SongListDrawer.vue')['default']
SongListDropdown: typeof import('./src/components/List/SongListDropdown.vue')['default']
SpecialCover: typeof import('./src/components/Cover/SpecialCover.vue')['default']
SpecialCoverCard: typeof import('./src/components/Cover/SpecialCoverCard.vue')['default']
Spectrum: typeof import('./src/components/Player/Spectrum.vue')['default']
SongListCard: typeof import('./src/components/Card/SongListCard.vue')['default']
SongListMenu: typeof import('./src/components/Menu/SongListMenu.vue')['default']
SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default']
TitleBar: typeof import('./src/components/WinDom/TitleBar.vue')['default']
UpCloudSong: typeof import('./src/components/Modal/UpCloudSong.vue')['default']
UserData: typeof import('./src/components/Nav/UserData.vue')['default']
TextContainer: typeof import('./src/components/Global/TextContainer.vue')['default']
UpdateApp: typeof import('./src/components/Modal/UpdateApp.vue')['default']
UpdatePlaylist: typeof import('./src/components/Modal/UpdatePlaylist.vue')['default']
User: typeof import('./src/components/Layout/User.vue')['default']
UserAgreement: typeof import('./src/components/Modal/UserAgreement.vue')['default']
}
}

3
dev-app-update.yml Normal file
View File

@@ -0,0 +1,3 @@
provider: github
owner: "imsyy"
repo: "SPlayer"

View File

@@ -8,5 +8,23 @@ services:
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
ports:
- 7899:7899
- 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,98 +0,0 @@
# 应用程序的唯一标识符
appId: com.imsyy.splayer
# 应用程序的产品名称
productName: SPlayer
copyright: Copyright © imsyy 2023
# 构建资源所在的目录
directories:
buildResources: build
# 包含在最终应用程序构建中的文件列表,这里使用通配符 ! 表示排除不需要的文件
files:
- "!**/.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/**
# Windows 平台配置
win:
# 可执行文件名
executableName: SPlayer
# 应用程序的图标文件路径
icon: public/images/icons/favicon-512x512.png
# 构建类型
target: nsis
# NSIS 安装器配置
nsis:
# 一键式安装程序还是辅助安装程序
oneClick: false
# 安装程序的生成名称
artifactName: ${productName}-${version}-setup.${ext}
# 创建的桌面快捷方式名称
shortcutName: ${productName}
# 卸载时显示的名称
uninstallDisplayName: ${productName}
# 创建桌面图标
createDesktopShortcut: always
# 是否允许 UAC 提升权限
allowElevation: true
# 是否允许用户更改安装目录
allowToChangeInstallationDirectory: true
# 安装包图标
installerIcon: public/images/icons/favicon.ico
# 卸载命令图标
uninstallerIcon: public/images/icons/favicon.ico
# macOS 平台配置
mac:
# 可执行文件名
executableName: SPlayer
# 应用程序的图标文件路径
icon: public/images/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
# macOS 平台的 DMG 配置
dmg:
# DMG 文件的生成名称
artifactName: ${productName}-${version}.${ext}
# Linux 平台配置
linux:
# 可执行文件名
executableName: SPlayer
# 应用程序的图标文件路径
icon: public/images/icons/favicon-512x512.png
# 构建类型
target:
- AppImage
- deb
- rpm
- tar.gz
# 维护者信息
maintainer: imsyy.top
# 应用程序类别
category: Audio;Music
# AppImage 配置
appImage:
# AppImage 文件的生成名称
artifactName: ${productName}-${version}.${ext}
# 是否在构建之前重新编译原生模块
npmRebuild: false
# 自动更新的配置
publish:
# 更新提供商
provider: github
# 自动更新检查的 URL
# url: https://example.com/auto-updates
owner: "imsyy"
repo: "SPlayer"

View File

@@ -1,163 +0,0 @@
import { resolve } from "path";
import {
defineConfig,
externalizeDepsPlugin,
loadEnv,
splitVendorChunkPlugin,
} from "electron-vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
import { VitePWA } from "vite-plugin-pwa";
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";
export default defineConfig(({ mode }) => {
// 读取环境变量
const getEnv = (name) => {
return loadEnv(mode, process.cwd())[name];
};
// 返回配置
return {
// 主进程
main: {
resolve: {
alias: {
"@main": resolve(__dirname, "electron/main"),
},
},
plugins: [externalizeDepsPlugin()],
build: {
publicDir: resolve(__dirname, "public"),
rollupOptions: {
input: {
index: resolve(__dirname, "electron/main/index.js"),
},
},
},
},
// 预渲染
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: resolve(__dirname, "electron/preload/index.js"),
},
},
},
},
// 渲染进程
renderer: {
resolve: {
extensions: [".js", ".vue", ".json"],
alias: {
"@": resolve(__dirname, "src"),
},
},
plugins: [
vue(),
AutoImport({
imports: [
"vue",
{
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
},
],
}),
Components({
resolvers: [NaiveUiResolver()],
}),
// viteCompression
viteCompression(),
// splitVendorChunkPlugin
splitVendorChunkPlugin(),
// PWA
VitePWA({
registerType: "autoUpdate",
workbox: {
clientsClaim: true,
skipWaiting: true,
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /(.*?)\.(woff2|woff|ttf)/,
handler: "CacheFirst",
options: {
cacheName: "file-cache",
},
},
{
urlPattern: /(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,
handler: "CacheFirst",
options: {
cacheName: "image-cache",
},
},
],
},
manifest: {
name: getEnv("RENDERER_VITE_SITE_TITLE"),
short_name: getEnv("RENDERER_VITE_SITE_TITLE"),
description: getEnv("RENDERER_VITE_SITE_DES"),
display: "standalone",
start_url: "/",
theme_color: "#fff",
background_color: "#efefef",
icons: [
{
src: "/images/icons/favicon-32x32.png",
sizes: "32x32",
type: "image/png",
},
{
src: "/images/icons/favicon-96x96.png",
sizes: "96x96",
type: "image/png",
},
{
src: "/images/icons/favicon-256x256.png",
sizes: "256x256",
type: "image/png",
},
{
src: "/images/icons/favicon-512x512.png",
sizes: "512x512",
type: "image/png",
},
],
},
}),
],
// 服务器配置
server: {
port: getEnv("MAIN_VITE_DEV_PORT"),
// 代理
proxy: {
"/api": {
target: `http://${getEnv("MAIN_VITE_SERVER_HOST")}:${getEnv("MAIN_VITE_SERVER_PORT")}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
// 构建
root: ".",
build: {
minify: "terser",
publicDir: resolve(__dirname, "public"),
rollupOptions: {
input: {
index: resolve(__dirname, "index.html"),
},
},
terserOptions: {
compress: {
pure_funcs: ["console.log"],
},
},
sourcemap: false,
},
},
};
});

122
electron.vite.config.ts Normal file
View File

@@ -0,0 +1,122 @@
import { resolve } from "path";
import { MainEnv } from "./env";
import { defineConfig, externalizeDepsPlugin, loadEnv } from "electron-vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
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 }) => {
// 读取环境变量
const getEnv = (name: keyof MainEnv): string => {
return loadEnv(mode, process.cwd())[name];
};
console.log(command);
// 获取端口
const webPort: number = Number(getEnv("VITE_WEB_PORT") || 14558);
const servePort: number = Number(getEnv("VITE_SERVER_PORT") || 25884);
// 返回配置
return {
// 主进程
main: {
plugins: [externalizeDepsPlugin()],
build: {
publicDir: resolve(__dirname, "public"),
rollupOptions: {
input: {
index: resolve(__dirname, "electron/main/index.ts"),
lyric: resolve(__dirname, "web/lyric.html"),
},
},
},
},
// 预加载
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
input: {
index: resolve(__dirname, "electron/preload/index.ts"),
},
},
},
},
// 渲染进程
renderer: {
root: ".",
plugins: [
vue(),
// mode === "development" && VueDevTools(),
AutoImport({
imports: [
"vue",
"vue-router",
"@vueuse/core",
{
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
},
],
eslintrc: {
enabled: true,
filepath: "./auto-eslint.mjs",
},
}),
Components({
resolvers: [NaiveUiResolver()],
}),
viteCompression(),
wasm(),
],
resolve: {
alias: {
"@": resolve(__dirname, "src/"),
},
},
css: {
preprocessorOptions: {
scss: {
silenceDeprecations: ["legacy-js-api"],
},
},
},
server: {
port: webPort,
// 代理
proxy: {
"/api": {
target: `http://127.0.0.1:${servePort}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, "/api"),
},
},
},
preview: {
port: webPort,
},
build: {
minify: "terser",
publicDir: resolve(__dirname, "public"),
rollupOptions: {
input: {
index: resolve(__dirname, "index.html"),
loading: resolve(__dirname, "web/loading/index.html"),
},
output: {
manualChunks: {
stores: ["src/stores/data.ts", "src/stores/index.ts"],
},
},
},
terserOptions: {
compress: {
pure_funcs: ["console.log"],
},
},
sourcemap: false,
},
},
};
});

1
electron/main/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="electron-vite/node" />

View File

@@ -1,252 +0,0 @@
import { join } from "path";
import { app, protocol, shell, BrowserWindow, globalShortcut, nativeImage } from "electron";
import { platform, optimizer, is } from "@electron-toolkit/utils";
import { startNcmServer } from "@main/startNcmServer";
import { startMainServer } from "@main/startMainServer";
import { configureAutoUpdater } from "@main/utils/checkUpdates";
import createSystemTray from "@main/utils/createSystemTray";
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
import mainIpcMain from "@main/mainIpcMain";
import Store from "electron-store";
import log from "electron-log";
// 屏蔽报错
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
// 配置 log
log.transports.file.resolvePathFn = () =>
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
// 设置日志文件的最大大小为 2 MB
log.transports.file.maxSize = 2 * 1024 * 1024;
// 绑定 console 事件
console.error = log.error.bind(log);
console.warn = log.warn.bind(log);
console.info = log.info.bind(log);
console.debug = log.debug.bind(log);
// 主进程
class MainProcess {
constructor() {
// 主窗口
this.mainWindow = null;
// 主代理
this.mainServer = null;
// 网易云 API
this.ncmServer = null;
// Store
this.store = new Store({
// 窗口大小
windowSize: {
width: { type: "number", default: 1280 },
height: { type: "number", default: 740 },
},
});
// 设置应用程序名称
if (process.platform === "win32") app.setAppUserModelId(app.getName());
// 初始化
this.checkApp().then(async (lockObtained) => {
if (lockObtained) {
await this.init();
}
});
}
// 单例锁
async checkApp() {
if (!app.requestSingleInstanceLock()) {
log.error("已有一个程序正在运行,本次启动阻止");
app.quit();
// 未获得锁
return false;
}
// 聚焦到当前程序
else {
app.on("second-instance", () => {
if (this.mainWindow) {
this.mainWindow.show();
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
this.mainWindow.focus();
}
});
// 获得锁
return true;
}
}
// 初始化程序
async init() {
log.info("主进程初始化");
// 启动网易云 API
try {
this.ncmServer = await startNcmServer({
port: import.meta.env.MAIN_VITE_SERVER_PORT,
host: import.meta.env.MAIN_VITE_SERVER_HOST,
});
} catch (error) {
console.error("启动网易云 API 失败:", error);
}
// 非开发环境启动代理
if (!is.dev) {
this.mainServer = await startMainServer();
}
// 注册应用协议
app.setAsDefaultProtocolClient("SPlayer");
// 应用程序准备好之前注册
protocol.registerSchemesAsPrivileged([
{ scheme: "app", privileges: { secure: true, standard: true } },
]);
// 主应用程序事件
this.mainAppEvents();
}
// 创建主窗口
createWindow() {
// 创建浏览器窗口
this.mainWindow = new BrowserWindow({
title: app.getName() || "SPlayer",
width: this.store.get("windowSize.width") || 1280, // 窗口宽度
height: this.store.get("windowSize.height") || 740, // 窗口高度
minHeight: 700, // 最小高度
minWidth: 1200, // 最小宽度
center: true, // 是否出现在屏幕居中的位置
show: false, // 初始时不显示窗口
frame: false, // 无边框
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
// 图标配置
icon: nativeImage.createFromPath(join(__dirname, "../../public/images/icons/favicon.png")),
// 预加载
webPreferences: {
devTools: is.dev,
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
webSecurity: false,
hardwareAcceleration: true,
},
});
// 窗口准备就绪时显示窗口
this.mainWindow.once("ready-to-show", () => {
this.mainWindow.show();
// mainWindow.maximize();
this.store.set("windowSize", this.mainWindow.getBounds());
});
// 主窗口事件
this.mainWindowEvents();
// 设置窗口打开处理程序
this.mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: "deny" };
});
// 渲染路径
// 在开发模式
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
this.mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
}
// 生产模式
else {
this.mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
}
}
// 主应用程序事件
mainAppEvents() {
app.whenReady().then(async () => {
// 创建主窗口
this.createWindow();
// 检测更新
configureAutoUpdater();
// 引入主 Ipc
mainIpcMain(this.mainWindow);
// 系统托盘
createSystemTray(this.mainWindow);
// 注册快捷键
createGlobalShortcut(this.mainWindow);
});
// 开发环境下 F12 打开控制台
app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window);
});
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) this.createWindow();
});
// 自定义协议
app.on("open-url", (_, url) => {
console.log("Received custom protocol URL:", url);
});
// 将要退出
app.on("will-quit", () => {
// 注销全部快捷键
globalShortcut.unregisterAll();
});
// 当所有窗口都关闭时退出应用macOS 除外
app.on("window-all-closed", () => {
if (!platform.isMacOS) {
app.quit();
}
});
}
// 主窗口事件
mainWindowEvents() {
this.mainWindow.on("show", () => {
this.mainWindow.webContents.send("lyricsScroll");
});
// this.mainWindow.on("hide", () => {
// console.info("窗口隐藏");
// });
this.mainWindow.on("focus", () => {
this.mainWindow.webContents.send("lyricsScroll");
});
// this.mainWindow.on("blur", () => {
// console.info("窗口失去焦点");
// });
this.mainWindow.on("maximize", () => {
this.mainWindow.webContents.send("windowState", true);
});
this.mainWindow.on("unmaximize", () => {
this.mainWindow.webContents.send("windowState", false);
});
this.mainWindow.on("resized", () => {
this.store.set("windowSize", this.mainWindow.getBounds());
});
this.mainWindow.on("moved", () => {
this.store.set("windowSize", this.mainWindow.getBounds());
});
// 窗口关闭
this.mainWindow.on("close", (event) => {
if (platform.isLinux) {
app.quit();
} else {
if (!app.isQuiting) {
event.preventDefault();
this.mainWindow.hide();
}
return false;
}
});
}
}
new MainProcess();

86
electron/main/index.ts Normal file
View File

@@ -0,0 +1,86 @@
import { app, BrowserWindow } from "electron";
import { electronApp } from "@electron-toolkit/utils";
import { release, type } from "os";
import { isMac } from "./utils/config";
import { unregisterShortcuts } from "./shortcut";
import { initTray, MainTray } from "./tray";
import { processLog } from "./logger";
import initAppServer from "../server";
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";
// 主进程
class MainProcess {
// 窗口
mainWindow: BrowserWindow | null = null;
loadWindow: BrowserWindow | null = null;
// 托盘
mainTray: MainTray | null = null;
// 是否退出
isQuit: boolean = false;
constructor() {
processLog.info("🚀 Main process startup");
// 程序单例锁
initSingleLock();
// 禁用 Windows 7 的 GPU 加速功能
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
// 监听应用事件
this.handleAppEvents();
// Electron 初始化完成后
// 某些API只有在此事件发生后才能使用
app.whenReady().then(async () => {
processLog.info("🚀 Application Process Startup");
// 设置应用程序名称
electronApp.setAppUserModelId("com.imsyy.splayer");
// 启动主服务进程
await initAppServer();
// 启动窗口
this.loadWindow = loadWindow.create();
this.mainWindow = mainWindow.create();
// 注册其他服务
this.mainTray = initTray(this.mainWindow!);
// 注册 IPC 通信
initIpc();
});
}
// 应用程序事件
handleAppEvents() {
// 窗口被关闭时
app.on("window-all-closed", () => {
if (!isMac) app.quit();
this.mainWindow = null;
this.loadWindow = null;
});
// 应用被激活
app.on("activate", () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
}
});
// 自定义协议
app.on("open-url", (_, url) => {
processLog.log("Received custom protocol URL:", url);
});
// 将要退出
app.on("will-quit", () => {
// 注销全部快捷键
unregisterShortcuts();
});
// 退出前
app.on("before-quit", () => {
this.isQuit = true;
});
}
}
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,420 @@
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 (_, lyricDirs: string[], id: number): Promise<{ lrc: string; ttml: string }> => {
const result = { lrc: "", ttml: "" };
try {
// 定义需要查找的模式
const patterns = {
ttml: `**/${id}.ttml`,
lrc: `**/${id}.lrc`,
};
// 遍历每一个目录
for (const dir of lyricDirs) {
try {
// 查找 ttml
if (!result.ttml) {
const ttmlFiles = await FastGlob(patterns.ttml, { cwd: dir });
if (ttmlFiles.length > 0) {
const filePath = join(dir, ttmlFiles[0]);
await access(filePath);
result.ttml = await readFile(filePath, "utf-8");
}
}
// 查找 lrc
if (!result.lrc) {
const lrcFiles = await FastGlob(patterns.lrc, { cwd: dir });
if (lrcFiles.length > 0) {
const filePath = join(dir, lrcFiles[0]);
await access(filePath);
result.lrc = await readFile(filePath, "utf-8");
}
}
// 如果两种文件都找到了就提前结束搜索
if (result.ttml && result.lrc) break;
} catch {
// 某个路径异常,跳过
}
}
} catch {
/* 忽略错误 */
}
return result;
},
);
// 删除文件
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

@@ -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,280 +0,0 @@
import { ipcMain, dialog, app, clipboard, shell } from "electron";
import { readDirAsync } from "@main/utils/readDirAsync";
import { parseFile } from "music-metadata";
import { download } from "electron-dl";
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
import NodeID3 from "node-id3";
import axios from "axios";
import fs from "fs/promises";
/**
* 监听主进程的 IPC 事件
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
*/
const mainIpcMain = (win) => {
// 窗口操作部分
ipcMain.on("window-min", (ev) => {
// 阻止最小化
ev.preventDefault();
// 最小化
win.minimize();
});
ipcMain.on("window-maxOrRestore", (ev) => {
const winSizeState = win.isMaximized();
winSizeState ? win.restore() : win.maximize();
ev.reply("windowState", win.isMaximized());
});
ipcMain.on("window-restore", () => {
win.restore();
});
ipcMain.on("window-hide", () => {
win.hide();
});
ipcMain.on("window-close", () => {
win.close();
app.isQuiting = true;
app.quit();
});
ipcMain.on("window-relaunch", () => {
app.isQuiting = true;
app.relaunch();
app.quit();
});
// 显示进度
ipcMain.on("setProgressBar", (_, val) => {
if (val === "close") {
win.setProgressBar(-1);
return false;
}
win.setProgressBar(val / 100);
});
// 解灰
ipcMain.handle("getMusicNumUrl", async (_, data) => {
// 解析传入数据
const songData = JSON.parse(data);
const songName = `${songData?.name}-${songData?.artists?.[0].name}`;
console.log("开始解灰:", songName);
const url = await getNeteaseMusicUrl(songName);
console.log("解灰地址:", url);
return url;
});
// bili 链接解析
ipcMain.handle("getBiliUrlData", async (_, url) => {
const data = await getBiliUrlBase64(url);
return data;
});
// 默认音乐文件夹
ipcMain.handle("getdefaultMusicPath", async () => {
const path = app.getPath("music");
return path;
});
// 选择文件夹
ipcMain.handle("selectDir", async (_, isChooseDl = false) => {
try {
const { canceled, filePaths } = await dialog.showOpenDialog({
title: isChooseDl ? "选择下载目录" : "选择添加目录",
defaultPath: isChooseDl ? app.getPath("downloads") : app.getPath("music"),
properties: ["openDirectory", "createDirectory"],
buttonLabel: "选择文件夹",
});
if (!canceled) {
const selectedDirectory = filePaths[0];
return selectedDirectory;
}
} catch (err) {
console.error("选择文件夹时发生错误:", err);
throw err;
}
});
// 读取文件夹内容
ipcMain.handle("getDirContents", async (_, selectedDir) => {
try {
// 使用 readDirAsync 函数递归地读取文件夹内容
const directoryContents = await readDirAsync(selectedDir);
return directoryContents;
} catch (err) {
console.error("读取文件夹内容时发生错误:", err);
throw err;
}
});
// 读取音乐歌词
ipcMain.handle("getMusicLyric", async (_, path) => {
try {
const data = await parseFile(path);
const lyric = data.common.lyrics;
if (lyric && lyric.length > 0) {
return lyric[0];
}
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
else {
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
// 返回读取的 lrc 数据,如果没有则返回 null
return lrcData || null;
}
} catch (error) {
console.error("读取音乐歌词出错:", error);
return null;
}
});
// 读取音乐封面
ipcMain.handle("getMusicCover", async (_, path) => {
try {
const data = await parseFile(path);
const picture = data.common.picture;
if (picture && picture.length > 0) {
const coverData = picture[0].data;
const coverFormat = picture[0].format;
return { coverData, coverFormat };
}
// 如果封面数据不存在,尝试读取同名的封面图片文件
else {
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
const coverData = await fs.readFile(coverFilePath);
// 返回读取的封面图片数据,如果没有则返回 null
return coverData ? { coverData, coverFormat: "jpg" } : null;
}
} catch (error) {
console.error("读取音乐封面出错:", error);
return null;
}
});
// 执行复制操作
ipcMain.handle("copyData", async (_, data) => {
try {
clipboard.writeText(data);
return true;
} catch (error) {
console.error("复制操作出错:", error);
return false;
}
});
// 本地磁盘文件删除
ipcMain.handle("deleteFile", async (_, path) => {
try {
// 检查文件是否存在
if (fs.access(path)) {
// 尝试删除文件
fs.unlink(path);
console.log(`文件已删除:${path}`);
return true;
} else {
console.log(`文件不存在:${path}`);
return false;
}
} catch (err) {
console.error(`文件删除操作出错:${path}`, err);
return false;
}
});
// 打开歌曲目录
ipcMain.on("openSongLocal", (_, path) => {
try {
if (fs.access(path)) {
shell.showItemInFolder(path);
} else {
console.log(`文件不存在:${path}`);
}
} catch (error) {
console.error("打开歌曲目录时出错:", error);
}
});
// 下载文件至指定目录
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
try {
if (fs.access(path)) {
const songData = JSON.parse(song);
console.info("开始下载:", songData, data);
// 下载歌曲
const songDownload = await download(win, data.url, {
directory: path,
filename: `${songName}.${songType}`,
});
// 若不为 mp3则不进行元信息写入
if (songType !== "mp3") return true;
// 下载封面
const coverDownload = await download(win, songData.cover, {
directory: path,
filename: `${songName}.jpg`,
});
// 生成歌曲文件的元数据
const songTag = {
title: songData.name,
artist: Array.isArray(songData.artists)
? songData.artists.map((ar) => ar.name).join(" / ")
: songData.artists || "未知歌手",
album: songData.album?.name || songData.album,
image: coverDownload.getSavePath(),
};
// 保存修改后的元数据
const isSuccess = NodeID3.write(songTag, songDownload.getSavePath());
// 删除封面
await fs.unlink(coverDownload.getSavePath());
return isSuccess;
} else {
console.log(`目录不存在:${path}`);
return false;
}
} catch (error) {
console.error("下载文件时出错:", error);
return false;
}
});
};
/**
* 从 Bilibili 视频中获取文件的 Base64 数据
*
* @param {string} url - 要获取的文件的 URL
* @returns {Promise<string>} - 文件的 Base64 数据
*/
const getBiliUrlBase64 = async (url) => {
try {
const response = await axios.get(url, {
headers: {
Referer: "https://www.bilibili.com/",
"User-Agent": "okhttp/3.4.1",
},
responseType: "arraybuffer",
withCredentials: false,
});
// 将二进制数据转换为缓冲区
const buffer = toBuffer(response.data);
// 将缓冲区中的数据转换为 Base64 编码的字符串
const encodedData = buffer.toString("base64");
// 返回 Base64 编码的文件数据
return encodedData;
} catch (error) {
console.error("获取文件数据时发生错误:" + error);
return null;
}
};
/**
* 将数据转换为缓冲区( Buffer
*
* @param {ArrayBuffer|Buffer|Uint8Array} data - 要转换的数据
* @returns {Buffer} - 转换后的缓冲区
*/
const toBuffer = (data) => {
if (data instanceof Buffer) {
return data;
} else {
return Buffer.from(data);
}
};
export default mainIpcMain;

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,19 +0,0 @@
import { join } from "path";
import express from "express";
import expressProxy from "express-http-proxy";
/**
* 启动主服务器
* @returns {import('http').Server} HTTP 服务器实例
*/
export const startMainServer = async () => {
const { MAIN_VITE_MAIN_PORT, MAIN_VITE_SERVER_HOST, MAIN_VITE_SERVER_PORT } = import.meta.env;
const port = MAIN_VITE_MAIN_PORT ?? 7899;
const apiHost = `http://${MAIN_VITE_SERVER_HOST}:${MAIN_VITE_SERVER_PORT}`;
const expressApp = express();
// 代理
expressApp.use("/", express.static(join(__dirname, "../renderer/")));
expressApp.use("/api", expressProxy(apiHost));
// 启动 Express 应用服务器,并监听指定端口
return expressApp.listen(port, "127.0.0.1");
};

View File

@@ -1,20 +0,0 @@
const netEaseApi = require("NeteaseCloudMusicApi");
/**
* 启动网易云音乐 API 服务器
*
* @async
* @param {Object} options - 服务器配置
* @param {number} [options.port=11451] - 服务器端口
* @param {string} [options.host="127.0.0.1"] - 服务器主机地址
* @returns {Promise<void>} 返回一个 Promise在 API 服务器成功启动后 resolve
*/
export const startNcmServer = async (
options = {
port: 11451,
host: "127.0.0.1",
},
) => {
console.log(options);
return await netEaseApi.serveNcmApi(options);
};

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

@@ -0,0 +1,141 @@
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
import { join } from "path";
import { isWin } from "../utils/config";
import { thumbarLog } from "../logger";
enum ThumbarKeys {
Play = "play",
Pause = "pause",
Prev = "prev",
Next = "next",
}
type ThumbarMap = Map<ThumbarKeys, ThumbarButton>;
export interface Thumbar {
clearThumbar(): void;
updateThumbar(playing: boolean, clean?: boolean): void;
}
// 缩略图单例
let thumbar: Thumbar | null = null;
// 工具栏图标
const thumbarIcon = (filename: string) => {
// 是否为暗色
const isDark = nativeTheme.shouldUseDarkColors;
// 返回图标
return nativeImage.createFromPath(
join(__dirname, `../../public/icons/thumbar/${filename}-${isDark ? "dark" : "light"}.png`),
);
};
// 缩略图工具栏
const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
return new Map<ThumbarKeys, ThumbarButton>()
.set(ThumbarKeys.Prev, {
tooltip: "上一曲",
icon: thumbarIcon("prev"),
click: () => win.webContents.send("playPrev"),
})
.set(ThumbarKeys.Next, {
tooltip: "下一曲",
icon: thumbarIcon("next"),
click: () => win.webContents.send("playNext"),
})
.set(ThumbarKeys.Play, {
tooltip: "播放",
icon: thumbarIcon("play"),
click: () => win.webContents.send("play"),
})
.set(ThumbarKeys.Pause, {
tooltip: "暂停",
icon: thumbarIcon("pause"),
click: () => win.webContents.send("pause"),
});
};
// 创建缩略图工具栏
class createThumbar implements Thumbar {
// 窗口
private _win: BrowserWindow;
// 工具栏
private _thumbar: ThumbarMap;
// 工具栏按钮
private _prev: ThumbarButton;
private _next: ThumbarButton;
private _play: ThumbarButton;
private _pause: ThumbarButton;
// 当前播放状态
private _isPlaying: boolean = false;
constructor(win: BrowserWindow) {
// 初始化数据
this._win = win;
this._thumbar = createThumbarButtons(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.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;
thumbarLog.info("🚀 ThumbarButtons Startup");
thumbar = new createThumbar(win);
return thumbar;
} catch (error) {
thumbarLog.error("❌ ThumbarButtons Error", error);
throw error;
}
};
/**
* 获取缩略图工具栏
* @returns 缩略图工具栏
*/
export const getThumbar = () => thumbar;

327
electron/main/tray/index.ts Normal file
View File

@@ -0,0 +1,327 @@
import {
app,
Tray,
Menu,
MenuItemConstructorOptions,
BrowserWindow,
nativeImage,
nativeTheme,
} from "electron";
import { isWin, appName } from "../utils/config";
import { join } from "path";
import { trayLog } from "../logger";
import { useStore } from "../store";
import lyricWindow from "../windows/lyric-window";
// 播放模式
type PlayMode = "repeat" | "repeat-once" | "shuffle";
type PlayState = "play" | "pause" | "loading";
// 全局数据
let playMode: PlayMode = "repeat";
let playState: PlayState = "pause";
let playName: string = "未播放歌曲";
let likeSong: boolean = false;
let desktopLyricShow: boolean = false;
let desktopLyricLock: boolean = false;
export interface MainTray {
setTitle(title: string): void;
setPlayMode(mode: PlayMode): void;
setLikeState(like: boolean): void;
setPlayState(state: PlayState): void;
setPlayName(name: string): void;
setDesktopLyricShow(show: boolean): void;
setDesktopLyricLock(lock: boolean): void;
destroyTray(): void;
}
// 托盘单例
let mainTrayInstance: MainTray | null = null;
// 托盘图标
const trayIcon = (filename: string) => {
// const rootPath = isDev
// ? join(__dirname, "../../public/icons/tray")
// : join(app.getAppPath(), "../../public/icons/tray");
// return nativeImage.createFromPath(join(rootPath, filename));
return nativeImage.createFromPath(join(__dirname, `../../public/icons/tray/${filename}`));
};
// 托盘菜单
const createTrayMenu = (win: BrowserWindow): MenuItemConstructorOptions[] => {
// 区分明暗图标
const showIcon = (iconName: string) => {
const isDark = nativeTheme.shouldUseDarkColors;
return trayIcon(`${iconName}${isDark ? "-dark" : "-light"}.png`).resize({
width: 16,
height: 16,
});
};
// 菜单
const menu: MenuItemConstructorOptions[] = [
{
id: "name",
label: playName,
icon: showIcon("music"),
click: () => {
win.show();
win.focus();
},
},
{
type: "separator",
},
{
id: "toogleLikeSong",
label: likeSong ? "从我喜欢中移除" : "添加到我喜欢",
icon: showIcon(likeSong ? "like" : "unlike"),
click: () => win.webContents.send("toogleLikeSong"),
},
{
id: "changeMode",
label:
playMode === "repeat" ? "列表循环" : playMode === "repeat-once" ? "单曲循环" : "随机播放",
icon: showIcon(playMode),
submenu: [
{
id: "repeat",
label: "列表循环",
icon: showIcon("repeat"),
checked: playMode === "repeat",
type: "radio",
click: () => win.webContents.send("changeMode", "repeat"),
},
{
id: "repeat-once",
label: "单曲循环",
icon: showIcon("repeat-once"),
checked: playMode === "repeat-once",
type: "radio",
click: () => win.webContents.send("changeMode", "repeat-once"),
},
{
id: "shuffle",
label: "随机播放",
icon: showIcon("shuffle"),
checked: playMode === "shuffle",
type: "radio",
click: () => win.webContents.send("changeMode", "shuffle"),
},
],
},
{
type: "separator",
},
{
id: "playNext",
label: "上一曲",
icon: showIcon("prev"),
click: () => win.webContents.send("playPrev"),
},
{
id: "playOrPause",
label: playState === "pause" ? "播放" : "暂停",
icon: showIcon(playState === "pause" ? "play" : "pause"),
click: () => win.webContents.send(playState === "pause" ? "play" : "pause"),
},
{
id: "playNext",
label: "下一曲",
icon: showIcon("next"),
click: () => win.webContents.send("playNext"),
},
{
type: "separator",
},
{
id: "toogleDesktopLyric",
label: `${desktopLyricShow ? "关闭" : "开启"}桌面歌词`,
icon: showIcon("lyric"),
click: () => win.webContents.send("toogleDesktopLyric"),
},
{
id: "toogleDesktopLyricLock",
label: `${desktopLyricLock ? "解锁" : "锁定"}桌面歌词`,
icon: showIcon(desktopLyricLock ? "lock" : "unlock"),
visible: desktopLyricShow,
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",
},
{
id: "setting",
label: "全局设置",
icon: showIcon("setting"),
click: () => {
win.show();
win.focus();
win.webContents.send("openSetting");
},
},
{
type: "separator",
},
{
id: "exit",
label: "退出",
icon: showIcon("power"),
click: () => {
// win.close();
app.exit(0);
app.quit();
},
},
];
return menu;
};
// 创建托盘
class CreateTray implements MainTray {
// 窗口
private _win: BrowserWindow;
// 托盘
private _tray: Tray;
// 菜单
private _menu: MenuItemConstructorOptions[];
private _contextMenu: Menu;
constructor(win: BrowserWindow) {
// 托盘图标
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
height: 32,
width: 32,
});
// 初始化数据
this._win = win;
this._tray = new Tray(icon);
this._menu = createTrayMenu(this._win);
this._contextMenu = Menu.buildFromTemplate(this._menu);
// 初始化事件
this.initTrayMenu();
this.initEvents();
this.setTitle(appName);
}
// 托盘菜单
private initTrayMenu() {
this._menu = createTrayMenu(this._win);
this._contextMenu = Menu.buildFromTemplate(this._menu);
this._tray.setContextMenu(this._contextMenu);
}
// 托盘事件
private initEvents() {
// 点击
this._tray.on("click", () => this._win.show());
// 明暗变化
nativeTheme.on("updated", () => {
this.initTrayMenu();
});
}
// 设置标题
/**
* 设置标题
* @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) + "...";
playName = name;
// 更新菜单
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();
}
}
/**
* 初始化托盘
* @param win 主窗口
* @param lyricWin 歌词窗口
* @returns 托盘实例
*/
export const initTray = (win: BrowserWindow) => {
try {
trayLog.info("🚀 Tray Process Startup");
const tray = new CreateTray(win);
// 保存单例实例
mainTrayInstance = tray;
return tray;
} catch (error) {
trayLog.error("❌ Tray Process Error", error);
return null;
}
};
/**
* 获取托盘实例
* @returns 托盘实例
*/
export const getMainTray = (): MainTray | null => mainTrayInstance;

View File

@@ -0,0 +1,84 @@
import { app, type BrowserWindow } from "electron";
import { updateLog } from "../logger";
import electronUpdater from "electron-updater";
import { isDev } from "../utils/config";
// import
const { autoUpdater } = electronUpdater;
// 开发环境启用
if (isDev) {
Object.defineProperty(app, "isPackaged", {
get: () => true,
});
}
// 更新源
autoUpdater.setFeedURL({
provider: "github",
owner: "imsyy",
repo: "SPlayer",
});
// 禁用自动下载
autoUpdater.autoDownload = false;
// 是否初始化
let isInit: boolean = false;
// 是否提示
let isShowTip: boolean = false;
// 事件监听
const initUpdaterListeners = (win: BrowserWindow) => {
if (isInit) return;
// 当有新版本可用时
autoUpdater.on("update-available", (info) => {
win.webContents.send("update-available", info);
updateLog.info(`🚀 New version available: ${info.version}`);
});
// 更新下载进度
autoUpdater.on("download-progress", (progress) => {
win.webContents.send("download-progress", progress);
updateLog.info(`🚀 Downloading: ${progress.percent}%`);
});
// 当下载完成时
autoUpdater.on("update-downloaded", (info) => {
win.webContents.send("update-downloaded", info);
updateLog.info(`🚀 Update downloaded: ${info.version}`);
// 安装更新
autoUpdater.quitAndInstall();
});
// 当没有新版本时
autoUpdater.on("update-not-available", (info) => {
if (isShowTip) win.webContents.send("update-not-available", info);
updateLog.info(`✅ No new version available: ${info.version}`);
});
// 更新错误
autoUpdater.on("error", (err) => {
win.webContents.send("update-error", err);
updateLog.error(`❌ Update error: ${err.message}`);
});
isInit = true;
};
// 检查更新
export const checkUpdate = (win: BrowserWindow, showTip: boolean = false) => {
// 初始化事件监听器
initUpdaterListeners(win);
// 更改提示
isShowTip = showTip;
// 检查更新
autoUpdater.checkForUpdates();
};
// 开始下载
export const startDownloadUpdate = () => {
autoUpdater.downloadUpdate();
};

View File

@@ -1,32 +0,0 @@
import { dialog, shell } from "electron";
import { is } from "@electron-toolkit/utils";
import pkg from "electron-updater";
const { autoUpdater } = pkg;
// 更新弹窗
const hasNewVersion = (info) => {
dialog
.showMessageBox({
title: "发现新版本 v" + info.version,
message: "发现新版本 v" + info.version,
detail: "是否前往 GitHub 下载新版本安装包?",
buttons: ["前往", "取消"],
type: "question",
noLink: true,
})
.then((result) => {
if (result.response === 0) {
shell.openExternal("https://github.com/imsyy/SPlayer/releases");
}
});
};
export const configureAutoUpdater = () => {
if (is.dev) return false;
autoUpdater.checkForUpdatesAndNotify();
// 若有更新
autoUpdater.on("update-available", (info) => {
hasNewVersion(info);
});
};

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

@@ -1,24 +0,0 @@
import { globalShortcut } from "electron";
/**
* 注册全局快捷键
* @param {BrowserWindow} win - 程序窗口
*/
const createGlobalShortcut = (win) => {
// 刷新程序
globalShortcut.register("CmdOrCtrl+Shift+R", () => {
if (win && win.isFocused()) win?.reload();
});
// 打开开发者工具
globalShortcut.register("CmdOrCtrl+Shift+I", () => {
if (win && win.isFocused()) {
win?.webContents.openDevTools({
mode: "right",
activate: true,
});
}
});
};
export default createGlobalShortcut;

View File

@@ -1,139 +0,0 @@
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
import { join } from "path";
// 当前歌曲数据
let playSongName = "当前暂无播放歌曲";
let playSongState = false;
/**
* 创建系统托盘
* @param {BrowserWindow} win - 程序窗口
*/
const createSystemTray = (win) => {
// 系统托盘
const mainTray = new Tray(
nativeImage
.createFromPath(
join(
__dirname,
process.platform === "win32"
? "../../public/images/icons/favicon.ico"
: "../../public/images/icons/favicon-32x32.png",
),
)
.resize({
height: 32,
width: 32,
}),
);
// 应用内菜单
Menu.setApplicationMenu(createTrayMenu(win));
// 默认名称
win.setTitle(app.getName());
mainTray.setTitle(app.getName());
mainTray.setToolTip(app.getName());
// 左键事件
mainTray.on("click", () => win.show());
// 托盘菜单
mainTray.setContextMenu(createTrayMenu(win));
// 系统主题改变
nativeTheme.on("updated", () => {
mainTray.setContextMenu(createTrayMenu(win));
});
// 播放歌曲改变
ipcMain.on("songNameChange", (_, val) => {
playSongName = val;
win.setTitle(val);
mainTray.setTitle(val);
mainTray.setToolTip(val);
mainTray.setContextMenu(createTrayMenu(win));
});
// 播放状态改变
ipcMain.on("songStateChange", (_, val) => {
playSongState = val;
mainTray.setContextMenu(createTrayMenu(win));
});
};
// 生成图标
const createIcon = (name) => {
// 系统是否为暗色
const isDarkMode = nativeTheme.shouldUseDarkColors;
// 返回图标
return nativeImage
.createFromPath(
isDarkMode
? join(__dirname, `../../public/images/icons/${name}-dark.png`)
: join(__dirname, `../../public/images/icons/${name}-light.png`),
)
.resize({ width: 16, height: 16 });
};
// 生成右键菜单
const createTrayMenu = (win) => {
// 返回菜单
return Menu.buildFromTemplate([
{
label: playSongName,
icon: createIcon("open"),
click() {
win.show();
win.focus();
win.webContents.send("showPlayer");
},
},
{
type: "separator",
},
{
label: "上一曲",
icon: createIcon("prev"),
accelerator: "CmdOrCtrl+Left",
click: () => {
win.webContents.send("playNextOrPrev", "prev");
},
},
{
label: playSongState ? "暂停" : "播放",
icon: createIcon(playSongState ? "pause" : "play"),
accelerator: "CmdOrCtrl+Space",
click: () => {
win.webContents.send("playOrPause");
},
},
{
label: "下一曲",
icon: createIcon("next"),
accelerator: "CmdOrCtrl+Right",
click: () => {
win.webContents.send("playNextOrPrev", "next");
},
},
{
type: "separator",
},
{
label: "全局设置",
icon: createIcon("setting"),
click: () => {
win.show();
win.focus();
win.webContents.send("open-setting");
},
},
{
type: "separator",
},
{
label: "退出",
icon: createIcon("power"),
click: () => {
win.close();
app.isQuiting = true;
app.quit();
},
},
]);
};
export default createSystemTray;

View File

@@ -1,167 +0,0 @@
import { encryptQuery } from "@main/utils/kwDES";
import axios from "axios";
/**
* 网易云音乐解灰
*/
// 咪咕音乐请求头
const requestHeader = {
Origin: "http://music.migu.cn/",
Referer: "http://m.music.migu.cn/v3/",
aversionid: import.meta.env.MAIN_VITE_MIGU_COOKIE || null,
channel: "0146921",
};
/**
* 获取咪咕音乐歌曲 ID
* @param {string} keyword - 搜索关键字
* @returns {Promise<any>}
*/
const getMiguSongId = async (keyword) => {
try {
const url =
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
keyword.toString() +
"&type=2&rows=20&pgc=1";
const result = await axios.get(url, {
headers: requestHeader,
});
if (result.data?.musics?.length) {
// 是否与原曲吻合
const originalName = keyword.split("-");
const songName = result.data.musics[0]?.songName;
if (songName && !songName?.includes(originalName[0])) {
return null;
}
return result.data.musics[0].id;
}
return null;
} catch (error) {
console.error("获取咪咕音乐歌曲 ID 失败:", error);
return null;
}
};
/**
* 获取咪咕音乐歌曲 URL
* @param {string} keyword - 搜索关键字
* @returns {Promise<any>}
*/
const getMiguSongUrl = async (keyword) => {
try {
const songId = await getMiguSongId(keyword);
if (!songId) return null;
console.info("咪咕解灰歌曲 ID", songId);
const soundQuality = "PQ";
const url =
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
songId.toString() +
"&toneFlag=" +
soundQuality;
const result = await axios.get(url, {
headers: requestHeader,
});
if (result.data?.data?.url) {
const songUrl = result.data.data.url;
console.info("咪咕解灰歌曲 URL", songUrl);
return songUrl;
}
return null;
} catch (error) {
console.error("获取咪咕音乐歌曲 URL 失败:", error);
return null;
}
};
/**
* 获取酷我音乐歌曲 ID
* @param {string} keyword - 搜索关键字
* @returns {Promise<any>}
*/
const getKuwoSongId = async (keyword) => {
try {
const url =
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8" +
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
keyword.toString();
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 originalName = keyword.split("-");
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
if (songName && !songName?.includes(originalName[0])) {
return null;
}
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
return songId.slice("MUSIC_".length);
} catch (error) {
console.error("获取酷我音乐歌曲 ID 失败:", error);
return null;
}
};
/**
* 获取酷我音乐歌曲 URL
* @param {string} keyword - 搜索关键字
* @returns {Promise<any>}
*/
const getKuwoSongUrl = async (keyword) => {
try {
const songId = await getKuwoSongId(keyword);
if (!songId) return null;
console.info("酷我解灰歌曲 ID", songId);
const url = encryptQuery
? "http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
encryptQuery(
"corp=kuwo&source=kwplayer_ar_8.5.5.0_apk_keluze.apk&p2p=1&type=convert_url2&sig=0&format=mp3" +
"&rid=" +
songId,
)
: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_" +
songId;
const result = await axios.get(url, { "user-agent": "okhttp/3.10.0" });
if (result.data) {
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
console.info("酷我解灰歌曲 URL", urlMatch);
return urlMatch;
}
return null;
} catch (error) {
console.error("获取酷我音乐歌曲 URL 失败:", error);
return null;
}
};
/**
* 获取给定关键字的音乐 URL
* @param {string} keyword - 关键字
* @returns {Promise<?string>} 音乐 URL
*/
const getNeteaseMusicUrl = async (keyword) => {
try {
const [kuwoSongUrl, miguSongUrl] = await Promise.all([
getKuwoSongUrl(keyword),
getMiguSongUrl(keyword),
]);
if (kuwoSongUrl) {
return kuwoSongUrl;
}
if (miguSongUrl) {
return miguSongUrl;
}
return null;
} catch (error) {
console.error("获取解灰 URL 全部失败:", error);
return null;
}
};
export default getNeteaseMusicUrl;

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

@@ -1,151 +0,0 @@
import { parseFile } from "music-metadata";
import fs from "fs/promises";
import path from "path";
/**
* 从指定文件夹中递归读取音乐文件,并将它们以数组形式返回
* @param {string} directoryPath - 要读取的文件夹路径
* @param {number} fileLimit - 返回的音乐文件数量限制
* @returns {Array} - 包含音乐文件信息的数组
*/
export const readDirAsync = async (directoryPath, fileLimit = 5000) => {
const result = [];
// 递归读取文件夹中的项目
const readItem = async (item) => {
const itemPath = path.join(directoryPath, item);
const stats = await fs.stat(itemPath);
// 若为音频文件
if (stats.isFile() && isAudioFile(itemPath)) {
try {
const { common, format } = await parseFile(itemPath);
// 音乐文件信息
const fileInfo = {
id: generateId(itemPath),
name: common.title,
path: itemPath,
size: (stats.size / (1024 * 1024)).toFixed(2),
time: stats.mtime?.getTime(),
artists: common.artists?.[0],
album: common.album,
alia: common.comment?.[0],
duration: formatDuration(format.duration),
};
result.push(fileInfo);
} catch (error) {
console.error("解析音乐文件元数据时出错:", error);
}
}
// 若为文件夹
if (stats.isDirectory()) {
// 读取子文件夹中的项目
const subItems = await fs.readdir(itemPath);
for (const subItem of subItems) {
await readItem(path.join(item, subItem));
}
}
};
// 从根目录开始读取
await readItem("");
// 返回不超过上限的音乐文件列表
return result.slice(0, fileLimit);
};
/**
* 递归地读取文件夹内容,包括文件和子文件夹的信息
* @param {string} directoryPath - 要读取的文件夹路径
* @param {number} depth - 递归深度(默认为 -1无限递归
* @param {number} fileLimit - 文件总数
* @returns {Promise<Array>} 包含文件和子文件夹信息的树形数组
*/
export const readDirTreeAsync = async (directoryPath, depth = -1, fileLimit = 5000) => {
const result = [];
const readItem = async (item) => {
const itemPath = path.join(directoryPath, item);
const stats = await fs.stat(itemPath);
const fileInfo = {
id: generateId(item),
name: item,
path: itemPath,
type: stats.isFile() ? "song" : "dir",
size: (stats.size / (1024 * 1024)).toFixed(2), // 文件大小
modified: stats.mtime, // 修改日期
};
if (stats.isFile() && isAudioFile(itemPath)) {
try {
const { common, format } = await parseFile(itemPath);
fileInfo.metadata = {
name: common.title,
artists: common.artists,
album: common.album,
date: common.date,
alia: common.comment?.[0],
year: common.year,
duration: formatDuration(format.duration),
};
} catch (error) {
console.error("解析音乐文件元数据时出错:", error);
}
}
if (stats.isDirectory() && (depth === -1 || depth > 0)) {
// 如果是文件夹且未达到递归深度限制,且文件数量未达到上限,则递归读取文件夹内容
if (fileInfo.type === "dir" && result.length < fileLimit) {
fileInfo.children = await readDirAsync(
itemPath,
depth === -1 ? -1 : depth - 1,
fileLimit - result.length,
);
}
}
result.push(fileInfo);
};
const items = await fs.readdir(directoryPath);
await Promise.all(items.map(readItem));
return result.slice(0, fileLimit); // 返回不超过上限的文件列表
};
/**
* 歌曲时长时间戳转换
* @param {number} mss 毫秒数
* @returns {string} 格式为 "mm:ss" 的字符串
*/
const formatDuration = (seconds) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`;
return `${formattedMinutes}:${formattedSeconds}`;
};
/**
* 判断文件是否为音频文件
* @param {string} filePath - 文件路径
* @returns {boolean} - 是否为音频文件
*/
const isAudioFile = (filePath) => {
const audioExtensions = [".flac", ".mp3"];
const extension = path.extname(filePath).toLowerCase();
return audioExtensions.includes(extension);
};
/**
* 从文件名生成数字 ID
* @param {string} fileName - 文件名
* @returns {number} - 生成的数字ID
*/
const generateId = (fileName) => {
// 将文件名转换为哈希值
let hash = 0;
for (let i = 0; i < fileName.length; i++) {
hash = (hash << 5) - hash + fileName.charCodeAt(i);
}
// 将哈希值转换为正整数
const numericId = Math.abs(hash % 10000000000);
return numericId;
};

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

17
electron/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import { ElectronAPI } from "@electron-toolkit/preload";
import type { StoreType } from "../main/store";
declare global {
interface Window {
electron: ElectronAPI;
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,15 +0,0 @@
import { contextBridge } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
// 如果启用了上下文隔离,使用 `contextBridge` 将 Electron API 暴露给渲染进程
if (process.contextIsolated) {
try {
// 使用 contextBridge 暴露 electronAPI 到渲染进程的全局对象中
contextBridge.exposeInMainWorld("electron", electronAPI);
} catch (error) {
console.error(error);
}
} else {
// 如果上下文隔离未启用,将 electronAPI 添加到 DOM 全局对象
window.electron = electronAPI;
}

23
electron/preload/index.ts Normal file
View File

@@ -0,0 +1,23 @@
import { contextBridge, ipcRenderer } from "electron";
import { electronAPI } from "@electron-toolkit/preload";
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
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);
}
}

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" },
);
};

63
electron/server/index.ts Normal file
View File

@@ -0,0 +1,63 @@
import { join } from "path";
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";
const initAppServer = async () => {
try {
const server = fastify({
routerOptions: {
// 忽略尾随斜杠
ignoreTrailingSlash: true,
},
});
// 注册插件
server.register(fastifyCookie);
server.register(fastifyMultipart);
// 生产环境启用静态文件
if (!isDev) {
serverLog.info("📂 Serving static files from /renderer");
server.register(fastifyStatic, {
root: join(__dirname, "../renderer"),
});
}
// 声明
server.get("/api", (_, reply) => {
reply.send({
name: "SPlayer API",
description: "SPlayer API service",
author: "@imsyy",
list: [
{
name: "NeteaseCloudMusicApi",
url: "/api/netease",
},
{
name: "UnblockAPI",
url: "/api/unblock",
},
],
});
});
// 注册接口
server.register(initNcmAPI, { prefix: "/api" });
server.register(initUnblockAPI, { prefix: "/api" });
server.register(initControlAPI, { prefix: "/api" });
// 启动端口
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
await server.listen({ port });
serverLog.info(`🌐 Starting AppServer on port ${port}`);
return server;
} catch (error) {
serverLog.error("🚫 AppServer failed to start");
throw error;
}
};
export default initAppServer;

View File

@@ -0,0 +1,87 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { pathCase } from "change-case";
import { serverLog } from "../../main/logger";
import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api";
// 获取数据
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
return async (
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply,
) => {
serverLog.log("🌐 Request NcmAPI:", name);
// 获取 NcmAPI 数据
try {
const result = await neteaseApi({
...req.query,
...(req.body as Record<string, any>),
cookie: req.cookies,
});
return reply.send(result.body);
} catch (error: any) {
serverLog.error("❌ NcmAPI Error:", error);
if ([400, 301].includes(error.status)) {
return reply.status(error.status).send(error.body);
}
return reply.status(500);
}
};
};
// 初始化 NcmAPI
export const initNcmAPI = async (fastify: FastifyInstance) => {
// 主信息
fastify.get("/netease", (_, reply) => {
reply.send({
name: "@neteaseapireborn/api",
version: "4.29.2",
description: "网易云音乐 API Enhanced",
author: "@MoeFurina",
license: "MIT",
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
});
});
// 注册 NeteaseCloudMusicApi 所有接口
Object.entries(NeteaseCloudMusicApi).forEach(([routerName, neteaseApi]: [string, any]) => {
// 例外
if (["serveNcmApi", "getModulesDefinitions"].includes(routerName)) return;
// 路由名称
const pathName = pathCase(routerName);
// 获取数据
const handler = getHandler(pathName, neteaseApi);
// 注册路由
fastify.get(`/netease/${pathName}`, handler);
fastify.post(`/netease/${pathName}`, handler);
// 兼容路由 - 中间具有 _ 的路由
if (routerName.includes("_")) {
fastify.get(`/netease/${routerName}`, handler);
fastify.post(`/netease/${routerName}`, handler);
}
});
// 获取 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);
}
},
);
serverLog.info("🌐 Register NcmAPI successfully");
};

14
electron/server/port.ts Normal file
View File

@@ -0,0 +1,14 @@
import getPort from "get-port";
// 默认端口
let webPort: number;
let servePort: number;
const getSafePort = async () => {
if (webPort && servePort) return { webPort, servePort };
webPort = await getPort({ port: 14558 });
servePort = await getPort({ port: 25884 });
return { webPort, servePort };
};
export default getSafePort;

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

@@ -0,0 +1,79 @@
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
import { SongUrlResult } from "./unblock";
import { serverLog } from "../../main/logger";
import getKuwoSongUrl from "./kuwo";
import axios from "axios";
import getBodianSongUrl from "./bodian";
/**
* 直接获取 网易云云盘 链接
* Thank @939163156
* Power by GD音乐台(music.gdstudio.xyz)
*/
const getNeteaseSongUrl = async (id: number | string): Promise<SongUrlResult> => {
try {
if (!id) return { code: 404, url: null };
const baseUrl = "https://music-api.gdstudio.xyz/api.php";
const result = await axios.get(baseUrl, {
params: { types: "url", id },
});
const songUrl = result.data.url;
serverLog.log("🔗 NeteaseSongUrl URL:", songUrl);
return { code: 200, url: songUrl };
} catch (error) {
serverLog.error("❌ Get NeteaseSongUrl Error:", error);
return { code: 404, url: null };
}
};
// 初始化 UnblockAPI
export const initUnblockAPI = async (fastify: FastifyInstance) => {
// 主信息
fastify.get("/unblock", (_, reply) => {
reply.send({
name: "UnblockAPI",
description: "SPlayer UnblockAPI service",
author: "@imsyy",
content:
"部分接口采用 @939163156 by GD音乐台(music.gdstudio.xyz),仅供本人学习使用,不可传播下载内容,不可用于商业用途。",
});
});
// netease
fastify.get(
"/unblock/netease",
async (
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply,
) => {
const { id } = req.query;
const result = await getNeteaseSongUrl(id);
return reply.send(result);
},
);
// kuwo
fastify.get(
"/unblock/kuwo",
async (
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
reply: FastifyReply,
) => {
const { keyword } = req.query;
const result = await getKuwoSongUrl(keyword);
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);
},
);
serverLog.info("🌐 Register UnblockAPI successfully");
};

View File

@@ -0,0 +1,66 @@
import { encryptQuery } from "./kwDES";
import { SongUrlResult } from "./unblock";
import { serverLog } from "../../main/logger";
import axios from "axios";
// 获取酷我音乐歌曲 ID
const getKuwoSongId = async (keyword: string): Promise<string | null> => {
try {
const url =
"http://search.kuwo.cn/r.s?&correct=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 songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
// 是否与原曲吻合
const originalName = keyword?.split("-") ?? keyword;
if (songName && !songName?.includes(originalName[0])) return null;
return songId.slice("MUSIC_".length);
} catch (error) {
serverLog.error("❌ Get KuwoSongId Error:", error);
return null;
}
};
// 获取酷我音乐歌曲 URL
const getKuwoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
try {
if (!keyword) return { code: 404, url: null };
const songId = await getKuwoSongId(keyword);
if (!songId) return { code: 404, url: null };
// 请求地址
const PackageName = "kwplayer_ar_5.1.0.0_B_jiakong_vh.apk";
const url =
"http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
encryptQuery(
`corp=kuwo&source=${PackageName}&p2p=1&type=convert_url2&sig=0&format=mp3` +
"&rid=" +
songId,
);
const result = await axios.get(url, {
headers: {
"User-Agent": "okhttp/3.10.0",
},
});
if (result.data) {
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
serverLog.log("🔗 KuwoSong URL:", urlMatch);
return { code: 200, url: urlMatch };
}
return { code: 404, url: null };
} catch (error) {
serverLog.error("❌ Get KuwoSong URL Error:", error);
return { code: 404, url: null };
}
};
export default getKuwoSongUrl;

View File

@@ -1,4 +1,3 @@
/* eslint-disable no-undef */
/*
Thanks to
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py

4
electron/server/unblock/unblock.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export type SongUrlResult = {
code: number;
url: string | null;
};

5
env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
export interface MainEnv {
readonly VITE_WEB_PORT: string;
readonly VITE_SERVER_PORT: string;
readonly VITE_API_URL: string;
}

65
eslint.config.mjs Normal file
View File

@@ -0,0 +1,65 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
import vue from "eslint-plugin-vue";
import js from "@eslint/js";
import globals from "globals";
import path from "node:path";
import autoEslint from "./auto-eslint.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: [
"**/node_modules",
"**/dist",
"**/out",
"**/.gitignore",
"**/auto-imports.d.ts",
"**/components.d.ts",
],
},
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
{
plugins: {
"@typescript-eslint": typescriptEslint,
vue,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...autoEslint.globals,
},
ecmaVersion: "latest",
sourceType: "module",
parserOptions: {
parser: "@typescript-eslint/parser",
},
},
rules: {
"@typescript-eslint/no-explicit-any": "off",
"vue/multi-word-component-names": "off",
},
},
{
files: ["**/.eslintrc.{js,cjs}"],
languageOptions: {
globals: { ...globals.node },
ecmaVersion: 5,
sourceType: "commonjs",
},
},
];

View File

@@ -1,23 +1,21 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%RENDERER_VITE_SITE_TITLE%</title>
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/icon" href="/icons/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>SPlayer</title>
<!-- font -->
<link rel="stylesheet" href="/fonts/font.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,8 +1,9 @@
server {
gzip on;
listen 7899;
listen [::]:7899;
listen 25884;
listen [::]:25884;
server_name localhost;
client_max_body_size 100M;
location / {
root /usr/share/nginx/html;
@@ -14,10 +15,27 @@ server {
rewrite ^(.*)$ /index.html last;
}
location /api/ {
proxy_buffers 16 32k;
location /api/netease/song/url/v1 {
proxy_buffers 16 64k;
proxy_buffer_size 128k;
proxy_busy_buffers_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;
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;
@@ -25,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,75 +1,137 @@
{
"name": "splayer",
"version": "2.0.0",
"productName": "SPlayer",
"version": "3.0.0-beta.5",
"description": "A minimalist music player",
"main": "./out/main/index.js",
"author": "imsyy",
"home": "https://imsyy.top",
"github": "https://github.com/imsyy/SPlayer",
"blog": "https://blog.imsyy.top",
"repository": "github:imsyy/SPlayer",
"license": "AGPL-3.0",
"license-file": "LICENSE",
"engines": {
"node": ">=18.16.0",
"npm": ">=9.6.7",
"pnpm": ">=8.14.0"
"node": ">=20",
"npm": ">=10"
},
"type": "module",
"scripts": {
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
"lint": "npx eslint . --fix",
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
"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 --watch",
"build": "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:win": "npm run build && electron-builder --win --config",
"build:mac": "npm run build && electron-builder --mac --config",
"build:linux": "npm run build && electron-builder --linux --config"
"build:web": "npm run build",
"build:unpack": "npm run build && electron-builder --dir",
"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": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@material/material-color-utilities": "^0.2.7",
"NeteaseCloudMusicApi": "^4.14.0",
"axios": "^1.6.5",
"colorthief": "^2.4.0",
"electron-dl": "^3.5.1",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"express": "^4.18.2",
"express-http-proxy": "^2.0.0",
"@applemusic-like-lyrics/core": "^0.1.3",
"@applemusic-like-lyrics/lyric": "^0.3.0",
"@applemusic-like-lyrics/vue": "^0.1.5",
"@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",
"@neteasecloudmusicapienhanced/api": "^4.29.17",
"@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.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.19",
"electron-dl": "^4.0.0",
"electron-store": "^11.0.2",
"electron-updater": "^6.6.2",
"file-saver": "^2.0.5",
"font-list": "^2.0.1",
"get-port": "^7.1.0",
"github-markdown-css": "^5.8.1",
"howler": "^2.2.4",
"js-cookie": "^3.0.5",
"jss": "^10.10.0",
"jss-preset-default": "^10.10.0",
"localforage": "^1.10.0",
"music-metadata": "7.14.0",
"node-id3": "^0.2.6",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"plyr": "^3.7.8",
"screenfull": "^6.0.2",
"vue-router": "^4.2.5",
"vue-slider-component": "4.1.0-beta.7"
"lodash-es": "^4.17.21",
"marked": "^16.4.2",
"md5": "^2.3.0",
"music-metadata": "^11.10.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"plyr": "^3.8.3",
"vue-virt-list": "^1.6.1"
},
"devDependencies": {
"@electron-toolkit/eslint-config": "^1.0.2",
"@rushstack/eslint-patch": "^1.6.1",
"@vitejs/plugin-vue": "^5.0.2",
"@vue/eslint-config-prettier": "^9.0.0",
"ajv": "^8.12.0",
"electron": "^28.1.2",
"electron-builder": "^24.9.1",
"electron-log": "^5.0.3",
"electron-vite": "^2.0.0",
"eslint": "^8.56.0",
"eslint-plugin-vue": "^9.19.2",
"naive-ui": "^2.37.3",
"prettier": "^3.1.1",
"sass": "^1.69.7",
"terser": "^5.26.0",
"unplugin-auto-import": "^0.17.3",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.0.11",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/tsconfig": "^2.0.0",
"@electron-toolkit/utils": "^4",
"@fastify/cookie": "^11.0.2",
"@fastify/http-proxy": "^11.3.0",
"@fastify/multipart": "^9.3.0",
"@fastify/static": "^8.3.0",
"@types/file-saver": "^2.0.7",
"@types/howler": "^2.2.12",
"@types/js-cookie": "^3.0.6",
"@types/md5": "^2.3.6",
"@types/node": "^24.10.1",
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"@vitejs/plugin-vue": "^6.0.1",
"ajv": "^8.17.1",
"crypto-js": "^4.2.0",
"electron": "38.2.2",
"electron-builder": "^26.0.12",
"electron-log": "^5.4.3",
"electron-vite": "^4.0.1",
"eslint": "^9.39.1",
"eslint-plugin-vue": "^10.5.1",
"fast-glob": "^3.3.3",
"fastify": "^5.6.2",
"naive-ui": "^2.43.1",
"node-taglib-sharp": "^6.0.1",
"prettier": "^3.6.2",
"sass": "^1.94.0",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^29.2.0",
"vite": "^7.2.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^0.17.4",
"vue": "3.4.4"
"vite-plugin-vue-devtools": "^8.0.3",
"vite-plugin-wasm": "^3.5.0",
"vue": "^3.5.24",
"vue-router": "^4.6.3",
"vue-tsc": "^3.1.3"
},
"pnpm": {
"overrides": {
"dmg-builder": "26.0.12",
"electron-builder-squirrel-windows": "26.0.12"
},
"onlyBuiltDependencies": [
"@applemusic-like-lyrics/core",
"@applemusic-like-lyrics/lyric",
"@applemusic-like-lyrics/vue",
"@parcel/watcher",
"core-js",
"electron",
"electron-winstaller",
"esbuild",
"sharp",
"vue-demi"
]
}
}

13550
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,723 +0,0 @@
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.a.woff2) format("woff2");
unicode-range: U+9aa2-ffe5;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.b.woff2) format("woff2");
unicode-range: U+8983-9aa0;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.c.woff2) format("woff2");
unicode-range: U+78f2-897b;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.d.woff2) format("woff2");
unicode-range: U+646d-78d9;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.e.woff2) format("woff2");
unicode-range: U+30e0-6445;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.f.woff2) format("woff2");
unicode-range: U+101-30df;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.g.woff2) format("woff2");
unicode-range: U+9aa8, U+9ab8, U+9ad3, U+9ad8, U+9b03, U+9b3c, U+9b41-9b42, U+9b44, U+9b4f, U+9b54,
U+9c7c, U+9c81, U+9c8d, U+9c9c, U+9ca4, U+9cb8, U+9cc3, U+9cd6, U+9cde, U+9e1f, U+9e21, U+9e23,
U+9e25-9e26, U+9e2d, U+9e2f, U+9e33, U+9e35, U+9e3d, U+9e3f, U+9e43, U+9e45, U+9e4a, U+9e4f,
U+9e64, U+9e70, U+9e7f, U+9e93, U+9ea6, U+9ebb, U+9ec4, U+9ecd-9ece, U+9ed1, U+9ed4, U+9ed8,
U+9f0e, U+9f13, U+9f20, U+9f3b, U+9f50, U+9f7f, U+9f84, U+9f8b, U+9f99-9f9a, U+9f9f, U+ff01,
U+ff08-ff09, U+ff0c, U+ff1a-ff1b, U+ff1f;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.h.woff2) format("woff2");
unicode-range: U+975b, U+975e, U+9760-9762, U+9769, U+9773-9774, U+9776, U+978b, U+978d, U+9798,
U+97a0, U+97ad, U+97e6-97e7, U+97e9, U+97ed, U+97f3, U+97f5-97f6, U+9875-9877, U+9879-987b,
U+987d-987f, U+9881-9882, U+9884-9888, U+988a, U+9890-9891, U+9893, U+9896-9898, U+989c-989d,
U+98a0, U+98a4, U+98a7, U+98ce, U+98d8, U+98de-98df, U+9910, U+9965, U+996d-9972, U+9975-9976,
U+997a, U+997c, U+997f, U+9981, U+9985-9986, U+9988, U+998b, U+998f, U+9992, U+9996, U+9999,
U+9a6c-9a71, U+9a73-9a74, U+9a76, U+9a79, U+9a7b-9a7c, U+9a7e, U+9a82, U+9a84, U+9a86-9a87,
U+9a8b-9a8c, U+9a8f, U+9a91, U+9a97, U+9a9a, U+9aa1, U+9aa4;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.i.woff2) format("woff2");
unicode-range: U+9570, U+9576, U+957f, U+95e8, U+95ea, U+95ed-95f0, U+95f2, U+95f4, U+95f7-95fb,
U+95fd, U+9600-9602, U+9605, U+9609, U+960e, U+9610-9611, U+9614, U+961c, U+961f, U+962e,
U+9632-9636, U+963b, U+963f-9640, U+9644-9648, U+964b-964d, U+9650, U+9655, U+965b, U+9661-9662,
U+9664, U+9668-966a, U+9675-9677, U+9685-9686, U+968b, U+968f-9690, U+9694, U+9698-9699, U+969c,
U+96a7, U+96b6, U+96be, U+96c0-96c1, U+96c4-96c7, U+96cc-96cd, U+96cf, U+96d5, U+96e8, U+96ea,
U+96f6-96f7, U+96f9, U+96fe, U+9700, U+9704, U+9707, U+9709, U+970d, U+9713, U+9716, U+971c,
U+971e, U+9732, U+9738-9739, U+9752, U+9756, U+9759;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.j.woff2) format("woff2");
unicode-range: U+9179, U+917f, U+9187, U+9189, U+918b, U+918d, U+9190, U+9192, U+919a-919b, U+91ba,
U+91c7, U+91c9-91ca, U+91cc-91cf, U+91d1, U+91dc, U+9274, U+93d6, U+9488-9489, U+948e,
U+9492-9493, U+9497, U+9499, U+949d-94a3, U+94a5-94a9, U+94ae, U+94b1, U+94b3, U+94b5, U+94bb,
U+94be, U+94c0-94c3, U+94c5-94c6, U+94dc-94dd, U+94e1, U+94e3, U+94ec-94ed, U+94f0-94f2, U+94f6,
U+94f8, U+94fa, U+94fe, U+9500-9501, U+9504-9505, U+9508, U+950b-950c, U+9510-9511, U+9517,
U+9519-951a, U+9521, U+9523-9526, U+9528, U+952d-9530, U+9539, U+953b, U+9540-9541, U+9547,
U+954a, U+954d, U+9550-9551, U+955c, U+9563, U+956d;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.k.woff2) format("woff2");
unicode-range: U+9001-9003, U+9005-9006, U+9009-900a, U+900d, U+900f-9012, U+9014, U+9017,
U+901a-901b, U+901d-9022, U+902e, U+9038, U+903b-903c, U+903e, U+9041-9042, U+9044, U+9047,
U+904d, U+904f-9053, U+9057, U+905b, U+9062-9063, U+9065, U+9068, U+906d-906e, U+9075, U+907d,
U+907f-9080, U+9082-9083, U+908b, U+9091, U+9093, U+9099, U+90a2-90a3, U+90a6, U+90aa,
U+90ae-90af, U+90b1, U+90b5, U+90b8-90b9, U+90bb, U+90c1, U+90ca, U+90ce, U+90d1, U+90dd, U+90e1,
U+90e7-90e8, U+90ed, U+90f4, U+90f8, U+90fd, U+9102, U+9119, U+9149, U+914b-914d, U+9152, U+9157,
U+915a, U+915d-915e, U+9161, U+9163, U+9165, U+916a, U+916c, U+916e, U+9171, U+9175-9178;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.l.woff2) format("woff2");
unicode-range: U+8e44, U+8e47-8e48, U+8e4a-8e4b, U+8e51, U+8e59, U+8e66, U+8e6c-8e6d, U+8e6f,
U+8e72, U+8e74, U+8e76, U+8e7f, U+8e81, U+8e87, U+8e8f, U+8eab-8eac, U+8eaf, U+8eb2, U+8eba,
U+8f66-8f69, U+8f6c, U+8f6e-8f72, U+8f74, U+8f7b, U+8f7d, U+8f7f, U+8f83-8f8a, U+8f8d-8f8e,
U+8f90-8f91, U+8f93, U+8f95-8f99, U+8f9b-8f9c, U+8f9e-8f9f, U+8fa3, U+8fa8-8fa9, U+8fab,
U+8fb0-8fb1, U+8fb9, U+8fbd-8fbe, U+8fc1-8fc2, U+8fc4-8fc5, U+8fc7-8fc8, U+8fce, U+8fd0-8fd1,
U+8fd3-8fd5, U+8fd8-8fd9, U+8fdb-8fdf, U+8fe2, U+8fe6, U+8fe8, U+8fea-8feb, U+8fed, U+8ff0,
U+8ff3, U+8ff7-8ff9, U+8ffd, U+9000;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.m.woff2) format("woff2");
unicode-range: U+8d24-8d31, U+8d34-8d35, U+8d37-8d3f, U+8d41-8d45, U+8d48, U+8d4a-8d4c,
U+8d4e-8d50, U+8d54, U+8d56, U+8d58, U+8d5a-8d5b, U+8d5d-8d5e, U+8d60-8d64, U+8d66-8d67, U+8d6b,
U+8d70, U+8d74-8d77, U+8d81, U+8d85, U+8d8a-8d8b, U+8d9f, U+8da3, U+8db3-8db4, U+8db8,
U+8dbe-8dbf, U+8dc3-8dc4, U+8dcb-8dcc, U+8dd1, U+8dd7, U+8ddb, U+8ddd, U+8ddf, U+8de4, U+8de8,
U+8dea, U+8def, U+8df3, U+8df5, U+8df7, U+8dfa-8dfb, U+8e09-8e0a, U+8e0c, U+8e0f, U+8e1d-8e1e,
U+8e22, U+8e29-8e2a, U+8e2e, U+8e31, U+8e35, U+8e39, U+8e42;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.n.woff2) format("woff2");
unicode-range: U+8bc9-8bcd, U+8bcf, U+8bd1, U+8bd3, U+8bd5, U+8bd7-8bd8, U+8bda-8bdb, U+8bdd-8bde,
U+8be0-8be9, U+8beb-8bf5, U+8bf7-8bf8, U+8bfa-8bfb, U+8bfd-8c01, U+8c03-8c06, U+8c08,
U+8c0a-8c0b, U+8c0d-8c13, U+8c15, U+8c17, U+8c19-8c1c, U+8c22-8c24, U+8c26-8c2a, U+8c2c-8c2d,
U+8c30-8c35, U+8c37, U+8c41, U+8c46, U+8c4c, U+8c61-8c62, U+8c6a-8c6b, U+8c79-8c7a, U+8c82,
U+8c89, U+8c8c, U+8d1d-8d1f, U+8d21-8d23;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.o.woff2) format("woff2");
unicode-range: U+889c, U+88a4, U+88ab, U+88ad, U+88b1, U+88c1-88c2, U+88c5-88c6, U+88c9,
U+88d4-88d5, U+88d8-88d9, U+88df, U+88e3-88e4, U+88e8, U+88f1, U+88f3-88f4, U+88f8-88f9, U+88fe,
U+8902, U+8910, U+8912-8913, U+891a-891b, U+8921, U+8925, U+892a-892b, U+8934, U+8936, U+8941,
U+8944, U+895e-895f, U+8966, U+897f, U+8981, U+8986, U+89c1-89c2, U+89c4-89c6, U+89c8-89cb,
U+89ce, U+89d0-89d2, U+89e3, U+89e5-89e6, U+8a00, U+8a07, U+8a79, U+8a89-8a8a, U+8a93, U+8b66,
U+8b6c, U+8ba1-8bab, U+8bad-8bb0, U+8bb2-8bb3, U+8bb6-8bba, U+8bbc-8bc1, U+8bc4-8bc6, U+8bc8;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.p.woff2) format("woff2");
unicode-range: U+8695, U+869c, U+86a3-86a4, U+86a7, U+86aa, U+86af, U+86b1, U+86c0, U+86c6-86c7,
U+86ca-86cb, U+86d0, U+86d4, U+86d9, U+86db, U+86df, U+86e4, U+86ee, U+86f0, U+86f9, U+86fe,
U+8700, U+8702-8703, U+8708-8709, U+870d, U+8712-8713, U+8715, U+8717-8718, U+871a, U+871c,
U+8721, U+8725, U+8734, U+8737, U+873b, U+873f, U+8747, U+8749, U+874c, U+874e, U+8757, U+8759,
U+8760, U+8763, U+8774, U+8776, U+877c, U+8782-8783, U+8785, U+878d, U+8793, U+879f, U+87af,
U+87b3, U+87ba, U+87c6, U+87ca, U+87d1-87d2, U+87e0, U+87e5, U+87f9, U+87fe, U+8815, U+8822,
U+8839, U+8840, U+8845, U+884c-884d, U+8854, U+8857, U+8859, U+8861, U+8863, U+8865, U+8868,
U+886b-886c, U+8870, U+8877, U+887d-887f, U+8881-8882, U+8884-8885, U+8888, U+888b, U+888d,
U+8892, U+8896;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.q.woff2) format("woff2");
unicode-range: U+83dc-83dd, U+83e0, U+83e9, U+83f1-83f2, U+8403-8404, U+840b-840e, U+841d,
U+8424-8428, U+843d, U+8451, U+8457, U+8459, U+845b, U+8461, U+8463, U+8469, U+846b-846c, U+8471,
U+8475, U+847a, U+8482, U+848b, U+8499, U+849c, U+84b2, U+84b8, U+84bf, U+84c4, U+84c9, U+84d1,
U+84d6, U+84dd, U+84df, U+84e6, U+84ec, U+8511, U+8513, U+8517, U+851a, U+851f, U+8521,
U+852b-852c, U+8537, U+853b-853d, U+8549-854a, U+8559, U+8574, U+857e, U+8584, U+8587, U+858f,
U+859b, U+85aa, U+85af-85b0, U+85c9, U+85cf-85d0, U+85d3, U+85d5, U+85e4, U+85e9, U+85fb, U+8611,
U+8638, U+864e-8651, U+8654, U+865a, U+865e, U+866b-866c, U+8671, U+8679, U+867d-867e,
U+8680-8682, U+868a, U+868c-868d, U+8693;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.r.woff2) format("woff2");
unicode-range: U+8273, U+827a, U+827e, U+8282, U+828a-828b, U+828d, U+8292, U+8299, U+829c-829d,
U+82a5-82a6, U+82a9, U+82ab-82ad, U+82af, U+82b1, U+82b3, U+82b7-82b9, U+82bd, U+82c7, U+82cd,
U+82cf, U+82d1, U+82d3-82d4, U+82d7, U+82db, U+82de-82df, U+82e3, U+82e5-82e6, U+82eb, U+82ef,
U+82f1, U+82f9, U+82fb, U+8301-8305, U+8309, U+830e, U+8314, U+8317, U+8327-8328, U+832b-832c,
U+832f, U+8335-8336, U+8338-8339, U+8340, U+8346-8347, U+8349, U+834f-8352, U+8354, U+835a,
U+835c, U+8361, U+8363-8364, U+8367, U+836b, U+836f, U+8377, U+837c, U+8386, U+8389, U+838e,
U+8393, U+839e, U+83a0, U+83ab, U+83b1-83b4, U+83b7, U+83b9-83ba, U+83bd, U+83c1, U+83c5, U+83c7,
U+83ca, U+83cc, U+83cf;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.s.woff2) format("woff2");
unicode-range: U+80de, U+80e1, U+80e7, U+80ea-80eb, U+80ed, U+80ef-80f0, U+80f3-80f4, U+80f6,
U+80f8, U+80fa, U+80fd, U+8102, U+8106, U+8109-810a, U+810d, U+810f-8111, U+8113-8114, U+8116,
U+8118, U+811a, U+812f, U+8131, U+8138, U+813e, U+8146, U+814a-814c, U+8150-8151, U+8154-8155,
U+8165, U+816e, U+8170, U+8174, U+8179-817c, U+817e-8180, U+818a, U+818f, U+8198, U+819b-819d,
U+81a8, U+81b3, U+81ba-81bb, U+81c0, U+81c2-81c3, U+81c6, U+81ca, U+81e3, U+81ea, U+81ec-81ed,
U+81f3-81f4, U+81fb-81fc, U+81fe, U+8200, U+8205-8206, U+820c-820d, U+8210, U+8212, U+8214,
U+821c, U+821e-821f, U+822a-822c, U+8230-8231, U+8235-8239, U+8247, U+8258, U+826f-8270, U+8272;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.t.woff2) format("woff2");
unicode-range: U+7f72, U+7f81, U+7f8a, U+7f8c, U+7f8e, U+7f94, U+7f9a, U+7f9e, U+7fa1, U+7fa4,
U+7fb2, U+7fb8-7fb9, U+7fbd, U+7fc1, U+7fc5, U+7fcc, U+7fce, U+7fd4-7fd5, U+7fd8, U+7fdf-7fe1,
U+7fe6, U+7fe9, U+7ff0-7ff1, U+7ff3, U+7ffb-7ffc, U+8000-8001, U+8003, U+8005, U+800c-800d,
U+8010, U+8012, U+8015, U+8017-8019, U+8027, U+802a, U+8033, U+8036-8038, U+803b, U+803d, U+803f,
U+8042, U+8046, U+804a-804c, U+8052, U+8054, U+8058, U+805a, U+806a, U+807f, U+8083-8084,
U+8086-8087, U+8089, U+808b-808c, U+8096, U+8098, U+809a-809b, U+809d, U+80a0-80a2, U+80a4-80a5,
U+80a9-80aa, U+80ae-80af, U+80b2, U+80b4, U+80ba, U+80be-80c1, U+80c3-80c4, U+80c6, U+80cc,
U+80ce, U+80d6, U+80da-80dc;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.u.woff2) format("woff2");
unicode-range: U+7eb5-7eba, U+7ebd, U+7ebf, U+7ec2-7eca, U+7ecd-7ed5, U+7ed8-7edf, U+7ee1-7ee3,
U+7ee5-7ee7, U+7ee9-7eeb, U+7eed, U+7eef-7ef0, U+7ef3-7ef8, U+7efc-7efd, U+7eff-7f00,
U+7f04-7f09, U+7f0e-7f0f, U+7f13-7f16, U+7f18, U+7f1a, U+7f1c-7f1d, U+7f1f-7f22, U+7f24-7f26,
U+7f28-7f2a, U+7f2d-7f2e, U+7f30, U+7f34, U+7f38, U+7f3a, U+7f42, U+7f50-7f51, U+7f54-7f55,
U+7f57, U+7f5a, U+7f61-7f62, U+7f69-7f6a, U+7f6e;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.v.woff2) format("woff2");
unicode-range: U+7b4c, U+7b4f-7b52, U+7b54, U+7b56, U+7b5b, U+7b5d, U+7b75, U+7b77, U+7b79, U+7b7e,
U+7b80, U+7b8d, U+7b94-7b95, U+7b97, U+7ba1, U+7ba9-7bab, U+7bad, U+7bb1, U+7bb8, U+7bc6-7bc7,
U+7bd1, U+7bd3, U+7bd9, U+7bdd, U+7be1, U+7bee, U+7bf1, U+7bf7, U+7bfe, U+7c07, U+7c0c, U+7c27,
U+7c2a, U+7c38, U+7c3f, U+7c41, U+7c4d, U+7c73, U+7c7b, U+7c7d, U+7c89, U+7c92, U+7c95,
U+7c97-7c98, U+7c9f, U+7ca4-7ca5, U+7caa, U+7cae, U+7cb1, U+7cb3, U+7cb9, U+7cbc-7cbe, U+7cc5,
U+7cca, U+7cd5-7cd7, U+7cd9, U+7cdc, U+7cdf-7ce0, U+7cef, U+7cfb, U+7d0a, U+7d20, U+7d22, U+7d27,
U+7d2b, U+7d2f, U+7d6e, U+7e41, U+7e82, U+7ea0-7ea4, U+7ea6-7ea8, U+7eaa-7ead, U+7eaf-7eb3;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.w.woff2) format("woff2");
unicode-range: U+7981, U+7984-7985, U+798f, U+79b9, U+79bb, U+79bd-79be, U+79c0-79c1, U+79c3,
U+79c6, U+79c9, U+79cb, U+79cd, U+79d1-79d2, U+79d8, U+79df, U+79e3-79e4, U+79e6-79e7, U+79e9,
U+79ef-79f0, U+79f8, U+79fb, U+79fd, U+7a00, U+7a0b, U+7a0d-7a0e, U+7a14, U+7a17, U+7a1a, U+7a20,
U+7a33, U+7a37, U+7a39, U+7a3b-7a3d, U+7a3f, U+7a46, U+7a51, U+7a57, U+7a74, U+7a76-7a77,
U+7a79-7a7a, U+7a7f, U+7a81, U+7a83-7a84, U+7a88, U+7a8d, U+7a91-7a92, U+7a95-7a98, U+7a9c-7a9d,
U+7a9f, U+7aa5-7aa6, U+7abf, U+7acb, U+7ad6, U+7ad9, U+7ade-7ae0, U+7ae3, U+7ae5-7ae6, U+7aed,
U+7aef, U+7af9, U+7afd, U+7aff, U+7b03, U+7b06, U+7b08, U+7b0b, U+7b11, U+7b14, U+7b19, U+7b1b,
U+7b20, U+7b26, U+7b28, U+7b2c, U+7b3a, U+7b3c, U+7b49, U+7b4b;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.x.woff2) format("woff2");
unicode-range: U+77aa, U+77ac, U+77b0, U+77b3, U+77b5, U+77bb, U+77bf, U+77d7, U+77db-77dc,
U+77e2-77e3, U+77e5, U+77e9, U+77eb, U+77ed-77ee, U+77f3, U+77fd-77ff, U+7801-7802, U+780c-780d,
U+7812, U+7814, U+7816, U+781a, U+781d, U+7823, U+7825, U+7827, U+7830, U+7834, U+7837-7838,
U+783a, U+783e, U+7840, U+7845, U+784c, U+7852, U+7855, U+785d, U+786b-786c, U+786e, U+787c,
U+7887, U+7889, U+788c-788e, U+7891, U+7897-7898, U+789c, U+789f, U+78a5, U+78a7, U+78b0-78b1,
U+78b3-78b4, U+78be, U+78c1, U+78c5, U+78ca-78cb, U+78d0, U+78d5, U+78e8, U+78ec, U+78f7, U+78fa,
U+7901, U+7934, U+793a, U+793c, U+793e, U+7940-7941, U+7948, U+7956-7957, U+795a-795b,
U+795d-7960, U+7965, U+7968, U+796d, U+796f, U+7977-7978, U+797a, U+7980;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.y.woff2) format("woff2");
unicode-range: U+761f, U+7624, U+7626, U+7629-762b, U+7634-7635, U+7638, U+763e, U+764c, U+7656,
U+765e, U+7663, U+766b, U+7678, U+767b, U+767d-767e, U+7682, U+7684, U+7686-7688, U+768b, U+768e,
U+7691, U+7693, U+7696, U+7699, U+76ae, U+76b1, U+76b4, U+76bf, U+76c2, U+76c5-76c6, U+76c8,
U+76ca, U+76ce-76d2, U+76d4, U+76d6-76d8, U+76db, U+76df, U+76ee-76ef, U+76f2, U+76f4,
U+76f8-76f9, U+76fc, U+76fe, U+7701, U+7708-7709, U+770b, U+771f-7720, U+7726, U+7728-7729,
U+772f, U+7736-7738, U+773a, U+773c, U+7740-7741, U+7750-7751, U+775a-775b, U+7761, U+7763,
U+7765-7766, U+7768, U+776b-776c, U+7779, U+777d, U+777f, U+7784-7785, U+778c, U+778e,
U+7791-7792, U+779f-77a0, U+77a5, U+77a7, U+77a9;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.z.woff2) format("woff2");
unicode-range: U+7435-7436, U+743c, U+7455, U+7459-745a, U+745c, U+745e-745f, U+7470, U+7476,
U+7480, U+7483, U+7487, U+749c, U+749e, U+74a7-74a8, U+74dc, U+74e2-74e4, U+74e6, U+74ee,
U+74f6-74f7, U+7504, U+7518, U+751a, U+751c, U+751f, U+7525, U+7528-7529, U+752b-752d,
U+7530-7533, U+7535, U+7537-7538, U+753b, U+7545, U+754c, U+754f, U+7554, U+7559, U+755c,
U+7565-7566, U+756a, U+7574, U+7578, U+7583, U+7586, U+758f, U+7591, U+7597, U+7599-759a, U+759f,
U+75a1, U+75a4-75a5, U+75ab, U+75ae-75b2, U+75b4-75b5, U+75b9, U+75bc-75be, U+75c5, U+75c7-75ca,
U+75cd, U+75d2, U+75d4-75d5, U+75d8, U+75db, U+75de, U+75e2-75e3, U+75e8, U+75ea, U+75f0, U+75f4,
U+75f9, U+7600-7601;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.aa.woff2) format("woff2");
unicode-range: U+725f, U+7261-7262, U+7267, U+7269, U+7272, U+7275, U+7279-727a, U+7280-7281,
U+7284, U+728a, U+7292, U+729f, U+72ac, U+72af, U+72b6-72b9, U+72c1-72c2, U+72c4, U+72c8, U+72ce,
U+72d0, U+72d2, U+72d7, U+72d9, U+72de, U+72e0-72e1, U+72e9, U+72ec-72f2, U+72f7-72f8, U+72fc,
U+730a, U+730e, U+7316, U+731b-731d, U+7322, U+7325, U+7329-732c, U+732e, U+7334, U+733e-733f,
U+7350, U+7357, U+7360, U+736d, U+7384, U+7387, U+7389, U+738b, U+7396, U+739b, U+73a9, U+73ab,
U+73af-73b0, U+73b2, U+73b7, U+73ba-73bb, U+73c0, U+73c8, U+73ca, U+73cd, U+73d0-73d1, U+73d9,
U+73e0, U+73ed, U+7403, U+7405-7406, U+7409-740a, U+740f-7410, U+741a, U+7422, U+7425, U+742a,
U+7433-7434;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ab.woff2) format("woff2");
unicode-range: U+706d, U+706f-7070, U+7075-7076, U+7078, U+707c, U+707e-707f, U+7089-708a, U+708e,
U+7092, U+7094-7096, U+7099, U+70ab-70af, U+70b1, U+70b3, U+70b8-70b9, U+70bc-70bd, U+70c1-70c3,
U+70c8, U+70ca, U+70d8-70d9, U+70db, U+70df, U+70e4, U+70e6-70e7, U+70e9, U+70eb-70ed, U+70ef,
U+70f7, U+70f9, U+70fd, U+7109-710a, U+7115, U+7119-711a, U+7126, U+7130-7131, U+7136, U+714c,
U+714e, U+715e, U+7164, U+7166-7168, U+716e, U+7172-7173, U+717d, U+7184, U+718a, U+718f, U+7194,
U+7198-7199, U+719f-71a0, U+71a8, U+71ac, U+71b9, U+71c3, U+71ce, U+71d5, U+71e5, U+7206, U+722a,
U+722c, U+7231, U+7235-7239, U+723d, U+7247-7248, U+724c-724d, U+7252, U+7259, U+725b;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ac.woff2) format("woff2");
unicode-range: U+6df7, U+6df9, U+6dfb, U+6e05, U+6e0a, U+6e0d-6e0e, U+6e10, U+6e14, U+6e17, U+6e1a,
U+6e1d, U+6e20-6e21, U+6e23-6e25, U+6e29, U+6e2d, U+6e2f, U+6e32, U+6e34, U+6e38, U+6e3a, U+6e43,
U+6e4d, U+6e56, U+6e58, U+6e5b, U+6e6e, U+6e7e-6e7f, U+6e83, U+6e85, U+6e89, U+6e90, U+6e9c,
U+6ea2, U+6ea5, U+6eaa, U+6eaf, U+6eb6, U+6eba, U+6ec1, U+6ec7, U+6ecb, U+6ed1, U+6ed3-6ed5,
U+6eda, U+6ede, U+6ee1, U+6ee4-6ee6, U+6ee8-6ee9, U+6ef4, U+6f02, U+6f06, U+6f09, U+6f0f,
U+6f13-6f15, U+6f20, U+6f29-6f2b, U+6f31, U+6f33, U+6f3e, U+6f46-6f47, U+6f4d, U+6f58, U+6f5c,
U+6f5e, U+6f62, U+6f66, U+6f6d-6f6e, U+6f84, U+6f88-6f89, U+6f8e, U+6f9c, U+6fa1, U+6fb3, U+6fb9,
U+6fc0, U+6fd1-6fd2, U+6fe1, U+7011, U+701a, U+7023, U+704c, U+706b;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ad.woff2) format("woff2");
unicode-range: U+6ccc, U+6cd3, U+6cd5, U+6cdb, U+6cde, U+6ce1-6ce3, U+6ce5, U+6ce8, U+6cea-6ceb,
U+6cef-6cf1, U+6cf3, U+6cf5, U+6cfb-6cfe, U+6d01, U+6d0b, U+6d12, U+6d17, U+6d1b, U+6d1e, U+6d25,
U+6d27, U+6d2a, U+6d31-6d32, U+6d3b-6d3e, U+6d41, U+6d43, U+6d45-6d47, U+6d4a-6d4b, U+6d4e-6d4f,
U+6d51, U+6d53, U+6d59-6d5a, U+6d63, U+6d66, U+6d69-6d6a, U+6d6e, U+6d74, U+6d77-6d78, U+6d82,
U+6d85, U+6d88-6d89, U+6d8c, U+6d8e, U+6d93, U+6d95, U+6d9b, U+6d9d, U+6d9f-6da1, U+6da3-6da4,
U+6da6-6daa, U+6dae-6daf, U+6db2, U+6db5, U+6db8, U+6dc0, U+6dc4-6dc7, U+6dcb-6dcc, U+6dd1,
U+6dd6, U+6dd8-6dd9, U+6de1, U+6de4, U+6deb-6dec, U+6dee, U+6df1, U+6df3;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ae.woff2) format("woff2");
unicode-range: U+6b92, U+6b96, U+6b9a, U+6ba1, U+6bb4-6bb5, U+6bb7, U+6bbf, U+6bc1, U+6bc5, U+6bcb,
U+6bcd, U+6bcf, U+6bd2, U+6bd4-6bd7, U+6bd9, U+6bdb, U+6be1, U+6beb, U+6bef, U+6c05, U+6c0f,
U+6c11, U+6c13-6c14, U+6c16, U+6c1b, U+6c1f, U+6c22, U+6c24, U+6c26-6c28, U+6c2e-6c30, U+6c32,
U+6c34, U+6c38, U+6c3d, U+6c40-6c42, U+6c47, U+6c49, U+6c50, U+6c55, U+6c57, U+6c5b, U+6c5d-6c61,
U+6c64, U+6c68-6c6a, U+6c70, U+6c72, U+6c76, U+6c79, U+6c7d-6c7e, U+6c81-6c83, U+6c86,
U+6c88-6c89, U+6c8c, U+6c8f-6c90, U+6c93, U+6c99, U+6c9b, U+6c9f, U+6ca1, U+6ca4-6ca7,
U+6caa-6cab, U+6cae, U+6cb3, U+6cb8-6cb9, U+6cbb-6cbf, U+6cc4-6cc5, U+6cc9-6cca;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.af.woff2) format("woff2");
unicode-range: U+68ad, U+68af-68b0, U+68b3, U+68b5, U+68c0, U+68c2, U+68c9, U+68cb, U+68cd, U+68d2,
U+68d5, U+68d8, U+68da, U+68e0, U+68ee, U+68f1, U+68f5, U+68fa, U+6905, U+690d-690e, U+6912,
U+692d, U+6930, U+693d, U+693f, U+6942, U+6954, U+6957, U+695a, U+695e, U+6963, U+696b,
U+6977-6978, U+697c, U+6982, U+6984, U+6986, U+6994, U+699c, U+69a8, U+69ad, U+69b4, U+69b7,
U+69bb, U+69c1, U+69cc, U+69d0, U+69db, U+69fd, U+69ff, U+6a0a, U+6a1f, U+6a21, U+6a2a, U+6a31,
U+6a35, U+6a3d, U+6a44, U+6a47, U+6a58-6a59, U+6a61, U+6a71, U+6a80, U+6a84, U+6a8e, U+6a90,
U+6aac, U+6b20-6b23, U+6b27, U+6b32, U+6b3a, U+6b3e, U+6b47, U+6b49, U+6b4c, U+6b62-6b67, U+6b6a,
U+6b79, U+6b7b-6b7c, U+6b81, U+6b83-6b84, U+6b86-6b87, U+6b89-6b8b;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ag.woff2) format("woff2");
unicode-range: U+6756, U+675c, U+675e-6761, U+6765, U+6768, U+676d, U+676f-6770, U+6773, U+6775,
U+6777, U+677c, U+677e-677f, U+6781, U+6784, U+6787, U+6789, U+6790, U+6795, U+6797, U+679a,
U+679c-679d, U+67a2-67a3, U+67aa-67ab, U+67ad, U+67af-67b0, U+67b6-67b7, U+67c4, U+67cf-67d4,
U+67d9-67da, U+67dc, U+67de, U+67e0, U+67e5, U+67e9, U+67ec, U+67ef, U+67f1, U+67f3-67f4,
U+67ff-6800, U+6805, U+6807-6808, U+680b, U+680f, U+6811, U+6813, U+6816-6817, U+6821,
U+6829-682a, U+6837-6839, U+683c-683d, U+6840, U+6842-6843, U+6845-6846, U+6848, U+684c,
U+6850-6851, U+6853-6854, U+6863, U+6865, U+6868-6869, U+6874, U+6876, U+6881, U+6885-6886,
U+6893, U+6897, U+68a2, U+68a6-68a8;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ah.woff2) format("woff2");
unicode-range: U+65f7, U+65fa, U+6602, U+6606, U+660a, U+660c, U+660e-660f, U+6613-6614, U+6619,
U+661d, U+661f-6620, U+6625, U+6627-6628, U+662d, U+662f, U+6631, U+6635, U+663c, U+663e, U+6643,
U+664b-664c, U+664f, U+6652-6653, U+6655-6657, U+665a, U+6664, U+6666, U+6668, U+666e-6670,
U+6674, U+6676-6677, U+667a, U+667e, U+6682, U+6684, U+6687, U+668c, U+6691, U+6696-6697, U+669d,
U+66a7, U+66ae, U+66b4, U+66d9, U+66dc-66dd, U+66e6, U+66f0, U+66f2-66f4, U+66f9, U+66fc,
U+66fe-6700, U+6708-6709, U+670b, U+670d, U+6714-6715, U+6717, U+671b, U+671d, U+671f, U+6726,
U+6728, U+672a-672d, U+672f, U+6731, U+6734-6735, U+673a, U+673d, U+6740, U+6742-6743, U+6746,
U+6748-6749, U+674e-6751;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ai.woff2) format("woff2");
unicode-range: U+6467, U+6469, U+6478-6479, U+6482, U+6485, U+6487, U+6491-6492, U+6495, U+649e,
U+64a4, U+64a9, U+64ac-64ae, U+64b0, U+64b5, U+64b8, U+64ba, U+64bc, U+64c2, U+64c5, U+64cd-64ce,
U+64d2, U+64d8, U+64de, U+64e2, U+64e6, U+6500, U+6512, U+6518, U+6525, U+652b, U+652f, U+6536,
U+6538-6539, U+653b, U+653e-653f, U+6545, U+6548, U+654c, U+654f, U+6551, U+6555-6556, U+6559,
U+655b, U+655d-655e, U+6562-6563, U+6566, U+656c, U+6570, U+6572, U+6574, U+6577, U+6587,
U+658b-658c, U+6590-6591, U+6593, U+6597, U+6599, U+659c, U+659f, U+65a1, U+65a4-65a5, U+65a7,
U+65a9, U+65ab, U+65ad, U+65af-65b0, U+65b9, U+65bd, U+65c1, U+65c4-65c5, U+65cb-65cc, U+65cf,
U+65d7, U+65e0, U+65e2, U+65e5-65e9, U+65ec-65ed, U+65f1, U+65f6;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.aj.woff2) format("woff2");
unicode-range: U+6323-6325, U+6328, U+632a-632b, U+632f, U+6332, U+633a, U+633d, U+6342,
U+6345-6346, U+6349, U+634b-6350, U+6355, U+635e-635f, U+6361-6363, U+6367, U+636e, U+6371,
U+6376-6377, U+637a-637b, U+6380, U+6382, U+6387-6389, U+638c, U+638f-6390, U+6392, U+6396,
U+6398, U+63a0, U+63a2-63a3, U+63a5, U+63a7-63aa, U+63ac, U+63b0, U+63b3-63b4, U+63b7-63b8,
U+63ba, U+63c4, U+63c9, U+63cd, U+63cf-63d0, U+63d2, U+63d6, U+63e1, U+63e3, U+63e9-63ea, U+63ed,
U+63f4, U+63f6, U+63fd, U+6400-6402, U+6405, U+640f-6410, U+6413-6414, U+641c, U+641e, U+6421,
U+642a, U+642c-642d, U+643a, U+643d, U+6441, U+6444, U+6446-6448, U+644a, U+6452, U+6454, U+6458,
U+645e;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ak.woff2) format("woff2");
unicode-range: U+6258, U+625b, U+6263, U+6266-6267, U+6269-6270, U+6273, U+6276, U+6279, U+627c,
U+627e-6280, U+6284, U+6289-628a, U+6291-6293, U+6295-6298, U+629a-629b, U+62a0-62a2,
U+62a4-62a5, U+62a8, U+62ab-62ac, U+62b1, U+62b5, U+62b9, U+62bc-62bd, U+62bf, U+62c2,
U+62c4-62ca, U+62cc-62ce, U+62d0, U+62d2-62d4, U+62d6-62d9, U+62db-62dc, U+62df, U+62e2-62e3,
U+62e5-62e9, U+62ec-62ed, U+62ef, U+62f1, U+62f3-62f4, U+62f7, U+62fc-62ff, U+6301-6302, U+6307,
U+6309, U+630e, U+6311, U+6316, U+631a-631b, U+631d-6321;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.al.woff2) format("woff2");
unicode-range: U+60cb, U+60d1, U+60d5, U+60d8, U+60da, U+60dc, U+60df-60e0, U+60e6-60e9,
U+60eb-60f0, U+60f3-60f4, U+60f6, U+60f9-60fa, U+6101, U+6108-6109, U+610e-610f, U+6115, U+611a,
U+611f-6120, U+6123-6124, U+6127, U+612b, U+613f, U+6148, U+614a, U+614c, U+614e, U+6151, U+6155,
U+6162, U+6167-6168, U+6170, U+6175, U+6177, U+618b, U+618e, U+6194, U+61a7-61a9, U+61ac, U+61be,
U+61c2, U+61c8, U+61ca, U+61d1-61d2, U+61d4, U+61e6, U+61f5, U+61ff, U+6208, U+620a, U+620c-6212,
U+6216, U+6218, U+621a-621b, U+621f, U+622a, U+622c, U+622e, U+6233-6234, U+6237, U+623e-6241,
U+6247-6249, U+624b, U+624d-624e, U+6251-6254;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.am.woff2) format("woff2");
unicode-range: U+5fcc-5fcd, U+5fcf-5fd2, U+5fd6-5fd9, U+5fdd, U+5fe0-5fe1, U+5fe4, U+5fe7,
U+5fea-5feb, U+5ff1, U+5ff5, U+5ffb, U+5ffd-6002, U+6005-6006, U+600d-600f, U+6012, U+6014-6016,
U+6019, U+601c-601d, U+6020-6021, U+6025-6028, U+602a, U+602f, U+6035, U+603b-603c, U+6041,
U+6043, U+604b, U+604d, U+6050, U+6052, U+6055, U+6059-605a, U+6062-6064, U+6068-606d,
U+606f-6070, U+6073, U+6076, U+6078-607c, U+607f, U+6084, U+6089, U+608c-608d, U+6094, U+6096,
U+609a, U+609f-60a0, U+60a3, U+60a6, U+60a8, U+60ac, U+60af, U+60b1-60b2, U+60b4, U+60b8,
U+60bb-60bc, U+60c5-60c6, U+60ca;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.an.woff2) format("woff2");
unicode-range: U+5e7f, U+5e84, U+5e86-5e87, U+5e8a, U+5e8f-5e90, U+5e93-5e97, U+5e99-5e9a, U+5e9c,
U+5e9e-5e9f, U+5ea6-5ea7, U+5ead, U+5eb5-5eb8, U+5ec9-5eca, U+5ed1, U+5ed3, U+5ed6, U+5ef6-5ef7,
U+5efa, U+5f00, U+5f02-5f04, U+5f08, U+5f0a-5f0b, U+5f0f, U+5f11, U+5f13, U+5f15, U+5f17-5f18,
U+5f1b, U+5f1f-5f20, U+5f25-5f27, U+5f29, U+5f2f, U+5f31, U+5f39-5f3a, U+5f52-5f53, U+5f55,
U+5f57, U+5f5d, U+5f62, U+5f64, U+5f66, U+5f69-5f6a, U+5f6c-5f6d, U+5f70-5f71, U+5f77, U+5f79,
U+5f7b-5f7c, U+5f80-5f81, U+5f84-5f85, U+5f87-5f8b, U+5f90, U+5f92, U+5f95, U+5f97-5f98, U+5fa1,
U+5fa8, U+5faa, U+5fad-5fae, U+5fb5, U+5fb7, U+5fbc-5fbd, U+5fc3, U+5fc5-5fc6;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ao.woff2) format("woff2");
unicode-range: U+5c7f, U+5c81-5c82, U+5c8c, U+5c94, U+5c96-5c97, U+5c9a-5c9b, U+5ca9, U+5cad,
U+5cb3, U+5cb8, U+5cbf, U+5ccb, U+5cd9, U+5ce1, U+5ce5-5ce6, U+5ce8, U+5cea, U+5ced, U+5cf0,
U+5cfb, U+5d02, U+5d07, U+5d0e, U+5d14, U+5d16, U+5d1b, U+5d24, U+5d29, U+5d2d, U+5d34, U+5d3d,
U+5d4c, U+5d58, U+5d6c, U+5d82, U+5d99, U+5dc5, U+5dcd, U+5ddd-5dde, U+5de1-5de2, U+5de5-5de9,
U+5deb, U+5dee, U+5df1-5df4, U+5df7, U+5dfe, U+5e01-5e03, U+5e05-5e06, U+5e08, U+5e0c,
U+5e10-5e11, U+5e15-5e16, U+5e18, U+5e1a-5e1d, U+5e26-5e27, U+5e2d-5e2e, U+5e37-5e38,
U+5e3c-5e3d, U+5e42, U+5e44-5e45, U+5e4c, U+5e54-5e55, U+5e61-5e62, U+5e72-5e74, U+5e76, U+5e78,
U+5e7a-5e7d;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ap.woff2) format("woff2");
unicode-range: U+5b85, U+5b87-5b89, U+5b8b-5b8c, U+5b8f, U+5b95, U+5b97-5b9e, U+5ba0-5ba4, U+5ba6,
U+5baa-5bab, U+5bb0, U+5bb3-5bb6, U+5bb9, U+5bbd-5bbf, U+5bc2, U+5bc4-5bc7, U+5bcc, U+5bd0,
U+5bd2-5bd3, U+5bdd-5bdf, U+5be1, U+5be4-5be5, U+5be8, U+5bf0, U+5bf8-5bfc, U+5bff, U+5c01,
U+5c04, U+5c06, U+5c09-5c0a, U+5c0f, U+5c11, U+5c14, U+5c16, U+5c18, U+5c1a, U+5c1d, U+5c24,
U+5c27, U+5c2c, U+5c31, U+5c34, U+5c38-5c3a, U+5c3c-5c42, U+5c45, U+5c48-5c4b, U+5c4e-5c51,
U+5c55, U+5c5e, U+5c60-5c61, U+5c65, U+5c6f, U+5c71, U+5c79;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.aq.woff2) format("woff2");
unicode-range: U+5996, U+5999, U+599e, U+59a5, U+59a8-59aa, U+59ae, U+59b2, U+59b9, U+59bb, U+59be,
U+59c6, U+59cb, U+59d0-59d1, U+59d3-59d4, U+59d7-59d8, U+59da, U+59dc-59dd, U+59e3, U+59e5,
U+59e8, U+59ec, U+59f9, U+59fb, U+59ff, U+5a01, U+5a03-5a04, U+5a06-5a07, U+5a11, U+5a13, U+5a18,
U+5a1c, U+5a1f-5a20, U+5a25, U+5a29, U+5a31-5a32, U+5a34, U+5a36, U+5a3c, U+5a40, U+5a46,
U+5a49-5a4a, U+5a5a, U+5a62, U+5a6a, U+5a74, U+5a76-5a77, U+5a7f, U+5a92, U+5a9a-5a9b,
U+5ab2-5ab3, U+5ac1-5ac2, U+5ac9, U+5acc, U+5ad4, U+5ad6, U+5ae1, U+5ae3, U+5ae6, U+5ae9, U+5b09,
U+5b34, U+5b37, U+5b40, U+5b50, U+5b54-5b55, U+5b57-5b59, U+5b5c-5b5d, U+5b5f, U+5b63-5b64,
U+5b66, U+5b69-5b6a, U+5b6c, U+5b70-5b71, U+5b75, U+5b7a, U+5b7d, U+5b81, U+5b83;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ar.woff2) format("woff2");
unicode-range: U+57ce, U+57d4, U+57df-57e0, U+57f9-57fa, U+5800, U+5802, U+5806, U+5811, U+5815,
U+5821, U+5824, U+582a, U+5830, U+5835, U+584c, U+5851, U+5854, U+5858, U+585e, U+586b, U+587e,
U+5883, U+5885, U+5892-5893, U+5899, U+589e-589f, U+58a8-58a9, U+58c1, U+58d1, U+58d5, U+58e4,
U+58eb-58ec, U+58ee, U+58f0, U+58f3, U+58f6, U+58f9, U+5904, U+5907, U+590d, U+590f, U+5915-5916,
U+5919-591a, U+591c, U+591f, U+5927, U+5929-592b, U+592d-592f, U+5931, U+5934, U+5937-593a,
U+5942, U+5944, U+5947-5949, U+594b, U+594e-594f, U+5951, U+5954-5957, U+595a, U+5960, U+5962,
U+5965, U+5973-5974, U+5976, U+5978-5979, U+597d, U+5981-5984, U+5986-5988, U+598a, U+598d,
U+5992-5993;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.as.woff2) format("woff2");
unicode-range: U+561b, U+561e-561f, U+5624, U+562d, U+5631-5632, U+5634, U+5636, U+5639, U+563b,
U+563f, U+564c, U+564e, U+5654, U+5657, U+5659, U+565c, U+5662, U+5664, U+5668-566c, U+5676,
U+567c, U+5685, U+568e-568f, U+5693, U+56a3, U+56b7, U+56bc, U+56ca, U+56d4, U+56da-56db, U+56de,
U+56e0, U+56e2, U+56e4, U+56ed, U+56f0-56f1, U+56f4, U+56f9-56fa, U+56fd-56ff, U+5703, U+5706,
U+5708-5709, U+571f, U+5723, U+5728, U+572d, U+5730, U+573a, U+573e, U+5740, U+5747, U+574a,
U+574d-5751, U+5757, U+575a-575b, U+575d-5761, U+5764, U+5766, U+5768, U+576a, U+576f, U+5773,
U+5777, U+5782-5784, U+578b, U+5792, U+579b, U+57a0, U+57a2-57a3, U+57a6, U+57ab, U+57ae,
U+57c2-57c3, U+57cb;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.at.woff2) format("woff2");
unicode-range: U+54e5-54ea, U+54ed-54ee, U+54f2, U+54fa, U+54fc-54fd, U+5501, U+5506-5507, U+5509,
U+550f-5510, U+5514, U+5520, U+5522, U+5524, U+5527, U+552c, U+552e-5531, U+5533, U+553e-553f,
U+5543-5544, U+5546, U+554a, U+5550, U+5555-5556, U+555c, U+5561, U+5564-5567, U+556a, U+556c,
U+556e, U+5575, U+5577-5578, U+557b-557c, U+557e, U+5580, U+5582-5584, U+5587, U+5589-558b,
U+558f, U+5591, U+5594, U+5598-5599, U+559c-559d, U+559f, U+55a7, U+55b3, U+55b7, U+55bb, U+55bd,
U+55c5, U+55d1-55d4, U+55d6, U+55dc-55dd, U+55df, U+55e1, U+55e3-55e6, U+55e8, U+55eb-55ec,
U+55ef, U+55f7, U+55fd, U+5600-5601, U+5608-5609, U+560e, U+5618;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.au.woff2) format("woff2");
unicode-range: U+5411, U+5413, U+5415, U+5417, U+541b, U+541d-5420, U+5426-5429, U+542b-542f,
U+5431, U+5434-5435, U+5438-5439, U+543b-543c, U+543e, U+5440, U+5443, U+5446, U+5448, U+544a,
U+5450, U+5453, U+5455, U+5457-5458, U+545b-545c, U+5462, U+5464, U+5466, U+5468, U+5471-5473,
U+5475, U+5478, U+547b-547d, U+5480, U+5482, U+5484, U+5486, U+548b-548c, U+548e-5490, U+5492,
U+5494-5496, U+5499-549b, U+54a4, U+54a6-54ad, U+54af, U+54b1, U+54b3, U+54b8, U+54bb, U+54bd,
U+54bf-54c2, U+54c4, U+54c6-54c9, U+54cd-54ce, U+54d0-54d2, U+54d5, U+54d7, U+54da, U+54dd,
U+54df;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.av.woff2) format("woff2");
unicode-range: U+5348-534a, U+534e-534f, U+5351-5353, U+5355-5357, U+535a, U+535c, U+535e-5362,
U+5364, U+5366-5367, U+536b, U+536f-5371, U+5373-5375, U+5377-5378, U+537f, U+5382, U+5384-5386,
U+5389, U+538b-538c, U+5395, U+5398, U+539a, U+539f, U+53a2, U+53a5-53a6, U+53a8-53a9, U+53ae,
U+53bb, U+53bf, U+53c1-53c2, U+53c8-53cd, U+53d1, U+53d4, U+53d6-53d9, U+53db, U+53df-53e0,
U+53e3-53e6, U+53e8-53f3, U+53f6-53f9, U+53fc-53fd, U+5401, U+5403-5404, U+5408-540a,
U+540c-5410;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.aw.woff2) format("woff2");
unicode-range: U+5207, U+520a, U+520d-520e, U+5211-5212, U+5217-521b, U+521d, U+5220, U+5224,
U+5228-5229, U+522b, U+522d-522e, U+5230, U+5236-523b, U+523d, U+5241-5243, U+524a, U+524c-524d,
U+5250-5251, U+5254, U+5256, U+525c, U+5265, U+5267, U+5269-526a, U+526f, U+5272, U+527d, U+527f,
U+5288, U+529b, U+529d-52a1, U+52a3, U+52a8-52ab, U+52ad, U+52b1-52b3, U+52be-52bf, U+52c3,
U+52c7, U+52c9, U+52cb, U+52d0, U+52d2, U+52d8, U+52df, U+52e4, U+52fa, U+52fe-5300, U+5305-5306,
U+5308, U+530d, U+5310, U+5315-5317, U+5319, U+531d, U+5320-5321, U+5323, U+532a, U+532e,
U+5339-533b, U+533e-533f, U+5341, U+5343, U+5347;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ax.woff2) format("woff2");
unicode-range: U+50cf, U+50d6, U+50da, U+50e7, U+50ee, U+50f3, U+50f5, U+50fb, U+5106, U+510b,
U+5112, U+5121, U+513f-5141, U+5143-5146, U+5148-5149, U+514b, U+514d, U+5151, U+5154, U+515a,
U+515c, U+5162, U+5165, U+5168, U+516b-516e, U+5170-5171, U+5173-5179, U+517b-517d, U+5180,
U+5185, U+5188-5189, U+518c-518d, U+5192, U+5195, U+5197, U+5199, U+519b-519c, U+51a0, U+51a2,
U+51a4-51a5, U+51ac, U+51af-51b0, U+51b2-51b3, U+51b5-51b7, U+51bb, U+51bd, U+51c0, U+51c4,
U+51c6, U+51c9, U+51cb-51cc, U+51cf, U+51d1, U+51db, U+51dd, U+51e0-51e1, U+51e4, U+51ed,
U+51ef-51f0, U+51f3, U+51f6, U+51f8-51fb, U+51fd, U+51ff-5201, U+5203, U+5206;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.ay.woff2) format("woff2");
unicode-range: U+4f60, U+4f63, U+4f65, U+4f69, U+4f6c, U+4f6f-4f70, U+4f73-4f74, U+4f7b-4f7c,
U+4f7f, U+4f83-4f84, U+4f88, U+4f8b, U+4f8d, U+4f97, U+4f9b, U+4f9d, U+4fa0, U+4fa3, U+4fa5-4faa,
U+4fac, U+4fae-4faf, U+4fb5, U+4fbf, U+4fc3-4fc5, U+4fca, U+4fce-4fd1, U+4fd7-4fd8, U+4fda,
U+4fdd-4fde, U+4fe1, U+4fe6, U+4fe8-4fe9, U+4fed-4fef, U+4ff1, U+4ff8, U+4ffa, U+4ffe,
U+500c-500d, U+500f, U+5012, U+5014, U+5018-501a, U+501c, U+501f, U+5021, U+5026, U+5028-502a,
U+502d, U+503a, U+503c, U+503e, U+5043, U+5047-5048, U+504c, U+504e-504f, U+5055, U+505a, U+505c,
U+5065, U+5076-5077, U+507b, U+507f-5080, U+5085, U+5088, U+508d, U+50a3, U+50a5, U+50a8, U+50ac,
U+50b2, U+50bb;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.az.woff2) format("woff2");
unicode-range: U+4e94-4e95, U+4e98, U+4e9a-4e9b, U+4e9f, U+4ea1-4ea2, U+4ea4-4ea9, U+4eab-4eae,
U+4eb2, U+4eb5, U+4eba, U+4ebf-4ec1, U+4ec3-4ec7, U+4eca-4ecb, U+4ecd-4ece, U+4ed1, U+4ed3-4ed9,
U+4ede-4edf, U+4ee3-4ee5, U+4ee8, U+4eea, U+4eec, U+4ef0, U+4ef2, U+4ef5-4ef7, U+4efb, U+4efd,
U+4eff, U+4f01, U+4f0a, U+4f0d-4f11, U+4f17-4f1a, U+4f1e-4f20, U+4f22, U+4f24-4f26, U+4f2a-4f2b,
U+4f2f-4f30, U+4f34, U+4f36, U+4f38, U+4f3a, U+4f3c-4f3d, U+4f43, U+4f46, U+4f4d-4f51, U+4f53,
U+4f55, U+4f58-4f59, U+4f5b-4f5e;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.a0.woff2) format("woff2");
unicode-range: U+d7, U+e0-e1, U+e8-ea, U+ec-ed, U+f2-f3, U+f7, U+f9-fa, U+fc, U+2014, U+2018-2019,
U+201c-201d, U+3001-3002, U+300a-300b, U+3010-3011, U+4e00-4e01, U+4e03, U+4e07-4e0b,
U+4e0d-4e0e, U+4e10-4e11, U+4e13-4e14, U+4e16, U+4e18-4e1e, U+4e22, U+4e24-4e25, U+4e27,
U+4e2a-4e2b, U+4e2d, U+4e30, U+4e32, U+4e34, U+4e38-4e3b, U+4e3d-4e3e, U+4e43, U+4e45,
U+4e48-4e49, U+4e4b-4e50, U+4e52-4e54, U+4e56, U+4e58-4e59, U+4e5c-4e61, U+4e66, U+4e70-4e71,
U+4e73, U+4e7e, U+4e86, U+4e88-4e89, U+4e8b-4e8c, U+4e8e-4e8f, U+4e91-4e93;
}
@font-face {
font-family: HarmonyOS_Regular;
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(font_files/HarmonyOS_Regular.a1.woff2) format("woff2");
unicode-range: U+21-7e, U+a4, U+a7-a8, U+b0-b1, U+b7;
}

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