Compare commits

...

218 Commits

Author SHA1 Message Date
alger
1f0f35dd51 🌈 style: v3.8.0 2025-01-18 03:26:14 +08:00
alger
be94d6ff8e feat: 添加歌词界面样式配置功能 2025-01-18 03:25:21 +08:00
alger
1bdb8fcb4a feat: 添加字体配置功能 可配置歌词页面 或全局字体 2025-01-17 22:45:59 +08:00
alger
914e043502 feat: 去除歌曲缓存 优化播放下一首 2025-01-17 22:35:42 +08:00
alger
dfa175b8b2 feat: 应用单例模式 2025-01-17 22:35:33 +08:00
alger
a94e0efba5 feat: 优化播放 2025-01-17 22:34:49 +08:00
alger
573023600a 🌈 style: v3.7.2 2025-01-16 23:27:51 +08:00
Alger
041aad31b4 Merge pull request #47 from algerkong/fix/player-freeze-recovery-46
 feat: 优化播放体验 优化代码 优化歌词缓存
2025-01-16 23:20:57 +08:00
alger
f652932d71 feat: 优化播放体验 优化代码 优化歌词缓存 2025-01-16 23:19:16 +08:00
alger
7efeb9492b 🐞 fix: 修复类型错误 2025-01-16 00:58:57 +08:00
alger
055536eb5c 🌈 style: v3.7.1 2025-01-16 00:46:33 +08:00
Alger
14852fc8d3 Merge pull request #44 from algerkong/fix/minimize-on-space-after-restore-42
🐞 fix: 最小化点击后恢复窗口按空格会继续最小化
2025-01-16 00:43:05 +08:00
Alger
9866e772df Merge pull request #45 from algerkong/fix/repeat-sound-on-lyric-seek-43
🐞 fix: 修复单曲循环后点击歌词快进会有重复的声音播放
2025-01-16 00:42:50 +08:00
alger
87ca0836b1 🐞 fix: 修复单曲循环后点击歌词快进会有重复的声音播放 2025-01-16 00:40:44 +08:00
alger
fa07c5a40c 🐞 fix: 最小化点击后恢复窗口按空格会继续最小化
fixed #42
2025-01-16 00:38:47 +08:00
alger
5dbfea240b 🌈 style: v3.7.0 2025-01-15 00:52:09 +08:00
alger
c1344393c3 🐞 fix: 修复清除缓存问题 2025-01-15 00:51:32 +08:00
alger
426888f77c feat: 优化设置页面样式以及布局 2025-01-15 00:31:40 +08:00
alger
45cbc15c0f feat: 添加快捷键 以及快捷键管理功能
ref #39
2025-01-15 00:30:00 +08:00
alger
072025a543 🐞 fix: 修复抽屉zindex 修复一些样式问题
closes  #37 #38
2025-01-15 00:26:42 +08:00
alger
c6427aa3e1 feat: v3.6.0 2025-01-13 23:08:47 +08:00
alger
632cdb1239 feat: 优化页面样式 2025-01-13 22:55:46 +08:00
alger
8ffe472605 feat: 添加歌手详情抽屉 2025-01-13 22:13:46 +08:00
alger
8e86d378d0 feat: 优化音乐解析,添加搜索记录 添加搜索滚动加载更多 添加关闭动画功能 2025-01-13 22:13:21 +08:00
alger
744fd53fb1 feat: 添加歌词缓存功能 2025-01-12 20:59:36 +08:00
alger
3c64473dbb feat: 优化音乐播放 控制 系统控制功能 (#36,#16)
fixed #36,#16
2025-01-12 19:14:25 +08:00
alger
e70fed37da feat: 添加下载列表显示功能 可播放已经下载的歌曲 添加清除缓存功能 修复下载文件类型问题 2025-01-12 16:04:03 +08:00
alger
b749854c5e feat: 优化留言显示 2025-01-12 12:38:51 +08:00
alger
d9210cc50a feat: 修改 捐赠支持 添加留言显示 可隐藏列表 2025-01-12 01:25:39 +08:00
alger
f186d34885 📃 docs: 更新README 2025-01-11 19:12:26 +08:00
alger
ba992b7c33 📃 docs: v3.4.0 2025-01-11 19:03:06 +08:00
alger
24d7c839c7 🌈 style: 添加 "animate.css" 2025-01-11 18:51:40 +08:00
alger
a4f3df80c9 📃 docs: v3.4.0 2025-01-11 18:45:42 +08:00
alger
866fec6ee3 feat: 优化收藏逻辑 本地和线上同步 添加批量下载 2025-01-11 18:38:34 +08:00
alger
8f7d6fbb8d feat: 设置页 添加捐赠支持列表 2025-01-11 18:22:14 +08:00
alger
62e26cae7d 🌈 style: 优化代码格式化 2025-01-10 22:49:55 +08:00
alger
ddb814da10 feat: v3.3.0 2025-01-06 22:33:13 +08:00
alger
e266ea8ef8 🐞 fix: 修复类型校验问题 2025-01-06 22:24:37 +08:00
alger
a894954641 🐞 fix: 修复类型校验问题 2025-01-06 22:15:25 +08:00
alger
f640ab9969 feat: v3.3.0 2025-01-06 22:10:20 +08:00
alger
9eb17fd978 feat: 优化登录失效 2025-01-06 22:03:50 +08:00
alger
020aca7384 feat: 添加音质选择 优化灰色歌曲解析 2025-01-06 20:54:42 +08:00
alger
fcc47dc0ff feat: 添加退出登录 2025-01-05 15:58:48 +08:00
alger
17ce268da6 feat: 修复未登录 收藏问题 2025-01-05 15:01:55 +08:00
alger
43c64b1b43 feat: 收藏功能改为接口对接 2025-01-04 16:58:08 +08:00
alger
11ced6b418 feat: 优化更新检查 下载 功能 2025-01-04 16:13:37 +08:00
alger
3d3992154a 🐞 fix: 修复歌词滚动问题 2025-01-04 00:26:30 +08:00
alger
81e7b67c7f 📃 docs: 3.2.0 2025-01-03 23:59:07 +08:00
alger
d7e94a342b feat: 添加代理功能和 realIP配置功能 2025-01-03 23:53:07 +08:00
alger
46f8067577 feat: 关闭应用的提示修改 可存储配置最小化 还是 关闭 2025-01-03 22:24:13 +08:00
alger
1dc7d0ceca 🐞 fix: 修复歌词页面与底栏冲突问题(#26) 修复搜索歌曲列表页面显示错误问题 (#33)
closed #26   #33
2025-01-03 22:03:26 +08:00
alger
ba64631a17 🐞 fix: 修复搜索类型切换 没有重新加载搜索的问题(#25)
closed #25
2025-01-03 21:28:48 +08:00
alger
cdb9524f04 feat: 解决检查更新请求失败问题 2025-01-02 00:45:01 +08:00
alger
5213aa13c5 🌈 style: 修复格式问题 2025-01-02 00:27:31 +08:00
alger
d870d0198f 🌈 style: 修复格式问题 2025-01-02 00:25:54 +08:00
alger
976a9afd2f 📃 docs: v3.1.0 2025-01-02 00:18:41 +08:00
alger
018218a5bf feat: 优化主入口代码 添加歌曲下载功能 2025-01-02 00:14:05 +08:00
alger
38a9d6ed31 feat: 完善网页版 安装应用功能 2025-01-01 22:42:25 +08:00
alger
8dab799939 feat: 修改更新检查功能 2025-01-01 15:05:49 +08:00
alger
1ddbe6f24e feat: 修改 github action 2025-01-01 14:48:31 +08:00
alger
4d5bcba6c7 fix: update macOS build config 2025-01-01 14:42:19 +08:00
alger
f833306b60 feat: 修改 github action 2025-01-01 14:31:27 +08:00
alger
4d92ed9963 🐞 fix: 修复mac 安装包损坏问题 2025-01-01 14:19:44 +08:00
alger
a22285156a feat: 修改 github action 添加更新日志 2025-01-01 14:01:55 +08:00
alger
d1029f16d6 feat: 修改 github action 2025-01-01 13:42:08 +08:00
alger
4908555635 feat: 修改 github action 2025-01-01 13:34:36 +08:00
alger
750cf7a484 🐞 fix: 去除无用导入 2025-01-01 13:26:06 +08:00
alger
a334743f6f feat: 添加 github action 自动 打包 发布 2025-01-01 13:14:56 +08:00
alger
14747cac10 feat: 优化打包和版本更新功能 2025-01-01 13:12:46 +08:00
alger
cc239aeaba 📃 docs: 修改文档 2025-01-01 02:44:39 +08:00
alger
eeda296589 📃 docs: 修改文档 2025-01-01 02:43:00 +08:00
alger
edb7ea201c 🌈 style: 去除无用提交 2025-01-01 02:30:37 +08:00
alger
17d20fa299 🦄 refactor: 重构整个项目 优化打包 修改后台服务为本地运行 添加更新版本检测功能 2025-01-01 02:25:18 +08:00
alger
f8d421c9b1 🐞 fix: 修复web移动端 页面空白问题 (#24)
closed #24
2024-12-30 11:20:23 +08:00
alger
dfdf02a17f feat: 优化主题效果 添加展开 menu功能 优化图片清晰度 添加随机播放功能(#20) 2024-12-29 00:43:39 +08:00
alger
abdb2bcd50 feat: 新增主题色切换功能 默认为日间主题可切换夜间 (#19、#21)
fixes #19  #21
2024-12-28 16:43:52 +08:00
alger
f728191a8f feat: 顶栏修改 2024-12-27 18:27:01 +08:00
alger
dfa8b51a53 📃 docs: qq 2024-12-25 19:59:58 +08:00
alger
b2c13121fd feat: 添加mv分类 2024-12-25 19:55:24 +08:00
alger
d28adb61a4 feat: 优化 list 加载 2024-12-17 23:23:20 +08:00
alger
9a7d5a3834 feat: 记忆歌词窗口位置 主窗口可关闭歌词窗口 2024-12-16 22:25:38 +08:00
alger
2037798fbe feat: 修复桌面歌词滚动问题 2024-12-16 22:15:25 +08:00
alger
85bd0ad015 feat: 优化桌面歌词添加歌曲控制 上一首下一首 播放暂停 2024-12-16 22:12:28 +08:00
alger
e1557a51a3 feat: 优化下载应用功能 去除web 窗口样式 2024-12-16 20:40:57 +08:00
alger
1ecc6f136f feat: 添加网页端可拖动边缘调整窗口大小功能 2024-12-15 21:17:35 +08:00
alger
53b3061b03 feat: 优化歌单列表页面 添加分类 2024-12-15 18:19:58 +08:00
alger
3d2f6a2330 feat: 将收藏与历史合并 2024-12-15 15:12:45 +08:00
alger
3b1470f28f feat: 添加设置菜单 优化移动端菜单显示 2024-12-15 14:35:18 +08:00
alger
100268448a feat: 优化图片加载 2024-12-15 14:13:13 +08:00
alger
51f67bb2c2 feat: 优化应用下载 2024-12-15 13:00:20 +08:00
alger
7be126cf5f feat: 优化播放器样式 添加单曲循环 优化桌面歌词效果 2024-12-15 01:40:13 +08:00
alger
f2f5d3ac15 feat: 优化web端页面效果 展示为 pc应用样式 2024-12-14 13:49:32 +08:00
alger
34c45e0105 📃 docs: 修该注释 2024-12-14 13:15:59 +08:00
alger
f9333f5f78 🐞 fix: 修复搜索时 使用空格导致的空格快捷键冲突问题(#18)
fixes #18
2024-12-14 13:00:06 +08:00
alger
7365daf700 🐞 fix: 修复播放暂停控制问题 后续优化为参数监听 2024-12-12 22:36:07 +08:00
alger
cebf313075 feat: 优化播放 修改为howler 修复搜索导致播放无限卡顿问题(#15)
- 优化了整个项目的播放
- 去除audio
- 优化歌词页 歌词同步时间

fixes #15
2024-12-12 22:18:52 +08:00
alger
bb99049991 feat: 优化页面效果 2024-12-09 22:58:57 +08:00
alger
df74dafbc5 feat: 优化歌单列表页面 2024-12-09 22:39:33 +08:00
alger
721d2a9704 feat: 添加搜藏功能 与页面 2024-12-09 21:55:08 +08:00
alger
1e60fa9a95 feat: 添加展开收起歌词的提示 2024-12-09 20:51:40 +08:00
alger
f24e8232f8 feat: 修复布局问题 2024-12-09 20:39:32 +08:00
alger
a1b1d861ac feat: 修改下载地址 2024-12-09 18:43:05 +08:00
alger
f24263b416 🐞 fix: 修复滚动问题 2024-12-08 21:57:34 +08:00
alger
17795e5da2 feat: 添加动画速度调整功能 优化页面自适应效果 2024-12-08 21:50:58 +08:00
alger
f1030d3a78 feat: seo 优化 2024-12-08 21:35:15 +08:00
alger
b979ce250f feat: 添加 Coffee 2024-12-07 23:20:31 +08:00
alger
d0d8966875 feat: 优化移动端 歌词与歌单页面显示 2024-12-07 22:54:45 +08:00
alger
d39ba65263 📃 docs: 修改文档 2024-12-07 22:38:56 +08:00
alger
62d400827e 📃 docs: 修改文档 2024-12-07 22:33:36 +08:00
alger
75b99c46b5 📃 docs: 修改文档 2024-12-07 22:32:06 +08:00
alger
e7ae79144c feat: 修改登录背景 2024-12-07 21:50:18 +08:00
alger
04d6cbe7f3 🐞 fix: 修复二维码登录 重复触发请求问题 修改为手机号优先 2024-12-07 21:37:10 +08:00
alger
bea1e5751f feat: 优化cpu占用过高问题 2024-12-07 14:30:20 +08:00
alger
f2ebb04fab 📃 docs: 修改文档 2024-12-07 12:02:54 +08:00
alger
42048764d5 📃 docs: 修改文档 2024-12-07 11:38:56 +08:00
alger
e326253fd8 feat: 优化打包命令 2024-12-06 23:52:08 +08:00
alger
edf5c77ea0 feat: 优化桌面歌词功能 添加歌词进度 优化歌词页面样式 2024-12-06 23:50:44 +08:00
alger
8870390770 feat: 修复下载提示弹出问题 2024-12-06 20:57:33 +08:00
alger
c9514e6e19 feat: 在页面显示 github地址 2024-12-05 21:57:14 +08:00
alger
08fa160de4 🐞 fix: 修复搜索下 mv和歌曲同时播放问题 2024-12-05 21:35:20 +08:00
alger
5d4c4922fd feat: 修复搜索播放 bug 优化搜索 mv播放器 2024-12-05 21:29:13 +08:00
alger
c5e7c87658 feat: 优化列表渲染 2024-12-04 20:38:26 +08:00
alger
f6923b4c47 🐞 fix: 修复调整窗口大小 歌单列表重新加载问题 2024-12-01 16:41:23 +08:00
alger
4cf7598a7d 📃 docs: 更新 docs 2024-12-01 16:28:26 +08:00
alger
81b09bef0d feat: 修复无法播放的问题 2024-12-01 15:55:09 +08:00
alger
b21df3de25 feat: 修改下载地址 2024-11-29 08:49:32 +08:00
alger
c49d814182 feat: 优化滚动条 位置 2024-11-28 23:45:44 +08:00
alger
1cb3c72ab7 feat: 优化歌单列表数量 2024-11-28 23:39:56 +08:00
alger
f03372de6a feat: 优化歌单列表 添加加载更多 优化自动布局 优化歌单 mv 歌单类型的动画效果 2024-11-28 23:33:38 +08:00
alger
d925f40303 feat: 优化歌词进度 添加下载 优化播放 优化历史记录 2024-11-28 08:12:37 +08:00
alger
dc12d895d8 feat: 2.1.0 2024-11-23 22:43:01 +08:00
alger
0bb14902f2 feat: 优化播放样式 优化歌曲背景色 优化 mv播放样式 添加循环播放 等控制功能 2024-11-23 22:42:23 +08:00
alger
3027a5f6ff feat: 完善mac打包规则 修复 icon显示问题 2024-11-20 22:44:17 +08:00
alger
f320f4760b feat: 添加网页标题修改 2024-11-01 17:39:18 +08:00
alger
e939933d6f feat: 添加减轻动画效果选项 添加indexdb方法 2024-10-22 21:09:51 +08:00
alger
06bffe7618 feat: 优化歌词页面样式 添加歌词进度显示 优化歌曲及列表加载方式 大幅提升歌曲歌词播放速度 2024-10-18 18:37:53 +08:00
alger
7abc087d70 feat: 添加播放列表自动滚动到播放的那个 2024-09-18 17:05:36 +08:00
alger
eb2ea1981d feat: 优化歌词背景色 加载问题 2024-09-18 15:11:20 +08:00
alger
6dc14ec51b feat: 优化歌词背景 修改为背景色 以解决卡顿问题 2024-09-14 18:22:56 +08:00
alger
36f8257a3e 🐞 fix: 上一首下一首逻辑错乱问题 2024-09-13 17:23:03 +08:00
alger
c55544df46 feat: 修复排行播放列表问题 优化暂停播放逻辑 2024-09-13 17:07:45 +08:00
alger
008f2183de 🐞 fix: 修复历史播放 不触发播放列表问题 2024-09-13 14:14:32 +08:00
alger
dd3a3c3bbb 🐞 fix: 类型问题修复 2024-09-13 14:11:02 +08:00
alger
941eb2e66e 🐞 fix: 修复作者不显示问题 2024-09-13 09:43:05 +08:00
alger
a98fcb43d6 🐞 fix: 修复播放列表无法显示问题 2024-09-13 09:08:57 +08:00
alger
791121ae06 feat: 优化搜索 2024-09-12 17:28:51 +08:00
alger
0c156e2708 feat: V1.7.0 2024-09-12 16:48:13 +08:00
alger
017b47fded 🐞 fix: 修复各种报错问题 2024-09-12 16:44:42 +08:00
alger
e27ed22c16 feat: 完善搜索歌单列表加载问题 2024-09-12 15:26:07 +08:00
alger
904d8744ef feat: 优化播放栏背景问题 2024-09-12 15:00:00 +08:00
alger
800e0b7360 feat: 完善歌单列表组件 实现滚动加载更多 2024-09-11 16:29:43 +08:00
alger
b6a5461a1d 🎈 perf: 优化加载 升级vue3.5 electron32等多个包 添加v-loading指令 2024-09-04 15:20:43 +08:00
alger
a4eda61a86 🌈 style: 更新版本 1.5.1 2024-06-25 15:22:30 +08:00
alger
a79d0712a4 🌈 style: 修改mv搜索项样式 2024-06-05 17:03:27 +08:00
alger
8f782cdc9d 🌈 style: 修改mv搜索项样式 2024-06-05 15:53:12 +08:00
alger
2f851f3172 🎈 perf: 优化歌曲列表以及图片加载 2024-06-05 15:35:31 +08:00
alger
9fcf455c08 🌈 style: 更新版本 1.5.0 2024-05-31 08:56:16 +08:00
alger
9b14906a46 🌈 style: add LICENSE. 2024-05-27 18:18:02 +08:00
alger
14ce428951 🌈 style: 修改README 2024-05-27 11:52:10 +08:00
alger
8c93124311 🌈 style: 修改README 2024-05-27 11:51:15 +08:00
alger
c09707867b 🦄 refactor: 适配 web移动端 改造 2024-05-23 17:12:35 +08:00
alger
a2af0f3904 feat: 优化搜索功能 2024-05-22 19:20:57 +08:00
alger
73982f0e84 feat: 添加manifest.json 2024-05-22 15:38:43 +08:00
alger
449a6fd335 feat: 登录问题修复 2024-05-22 15:14:26 +08:00
alger
32b39c7927 feat: 添加每日推荐 样式, 请求等大量优化 2024-05-22 12:07:48 +08:00
alger
c6f1e0b233 🌈 style: 修改issue模板 2024-05-21 11:36:06 +08:00
alger
7c1a3ae4bc 🌈 style: 添加issue模板 2024-05-21 11:33:57 +08:00
alger
6bd6622484 🌈 style: 添加gzip压缩配置 2024-05-21 11:24:33 +08:00
alger
433aff385d 🌈 style: 更换ico文件 2024-05-21 11:06:20 +08:00
alger
c37ad07f93 🌈 style: 优化类型 2024-05-21 11:01:23 +08:00
alger
e4c1f855fb 🐞 fix: 修复关闭报错 2024-05-21 10:27:46 +08:00
alger
6978656061 🌈 style: 更新logo.png 2024-05-21 10:24:32 +08:00
alger
973d60c98f 🌈 style: 完善gitgnore 2024-05-21 10:21:51 +08:00
alger
5a43ba2576 🌈 style: 删除无用配置 2024-05-21 10:18:40 +08:00
alger
e52a02cf3c 🌈 style: 去除无用代码 2024-05-21 10:16:30 +08:00
alger
da8216e2ca 🦄 refactor: 重构打包方式 2024-05-21 08:52:34 +08:00
alger
bd0e2ec35c feat: 历史纪录去除按钮 2024-05-20 19:55:52 +08:00
alger
7c8598ffa5 feat: 优化歌词体验 2024-05-20 19:54:00 +08:00
alger
50e594b91d 🐞 fix: 修复歌词界面无法打开问题 2024-05-16 19:57:20 +08:00
alger
a9e5bb33e4 feat: 添加eslint 和 桌面歌词(未完成) 2024-05-16 18:54:30 +08:00
alger
5e8676a039 📃 docs: 完善文档 2024-05-14 18:11:54 +08:00
alger
3522011224 🐞 fix: 修复登录状态问题 2024-01-04 10:18:00 +08:00
alger
820597e903 🐞 fix: 修复菜单问题 2024-01-04 10:02:57 +08:00
alger
f8efbe8ec6 🐞 fix: 修复播放次序问题 2024-01-04 09:55:41 +08:00
alger
67d42a2291 feat: 限制只能启动一个应用 2024-01-04 09:39:37 +08:00
algerkong
7ab43d2e9e feat: 样式优化 2024-01-03 22:27:58 +08:00
algerkong
a59351adf7 feat: 修复样式问题 2024-01-02 22:19:39 +08:00
alger
ad5d5458f1 🐞 fix: 修复播放历史不展示上下一首的问题 2024-01-02 11:08:02 +08:00
alger
adb539fbde 🌈 style: 去除无用代码 2024-01-02 09:22:31 +08:00
alger
ecd7a56df0 feat: 添加专辑列表播放 2024-01-01 00:06:52 +08:00
alger
2dbf5dbf03 feat: 添加播放历史计数 2023-12-29 16:13:05 +08:00
alger
492164d008 feat: 添加播放历史页面 2023-12-29 16:04:44 +08:00
alger
8da7fdabe5 🐞 fix: 修复搜索的播放列表错误问题 2023-12-28 11:40:29 +08:00
alger
a2c49d354e feat: 添加设置页面 可配置代理开关 2023-12-28 10:45:11 +08:00
algerkong
c7c1143cb4 feat: 修改搜索列表 2023-12-27 23:25:26 +08:00
algerkong
f5d097e975 feat: 优化路由持久化 2023-12-27 21:44:55 +08:00
algerkong
d04aeef40b feat: 优化mv播放 2023-12-27 21:44:32 +08:00
algerkong
a504b914fe feat: 优化播放条和mv播放时没有暂停音乐的问题 2023-12-27 21:05:25 +08:00
alger
62d414d659 feat: 添加热门mv页面 2023-12-27 18:21:01 +08:00
alger
6c57e77969 🎈 perf: 添加自动导入,优化性能 2023-12-27 14:40:22 +08:00
alger
70139e3ca4 feat: 优化样式,添加播放列表 2023-12-27 14:39:52 +08:00
alger
4dde40ac60 feat(MusicFull): 添加歌词背景动画 2023-12-22 09:50:03 +08:00
alger
be83a79b05 🐞 fix(Audio): 修复搜索跳转时 音乐一直播放暂停 2023-12-21 18:09:12 +08:00
alger
a77afb57fd feat(app): 修改app图标 2023-12-21 16:59:27 +08:00
alger
cd11db63eb feat: 完善播放列表问题 修复 滚动 2023-12-21 16:45:06 +08:00
alger
f81127432e 📃 docs: 添加概述 2023-12-21 16:44:17 +08:00
alger
4466713d1a 🐞 fix(app): 修复托盘透明问题 2023-12-21 16:43:51 +08:00
alger
73c915d184 🦄 refactor(MusicList): 重构播放列表组件 2023-12-21 11:26:51 +08:00
alger
7e6788a057 🐞 fix(Play): 修复播放监听和vip歌曲解析问题 2023-12-21 11:26:03 +08:00
alger
19140cd680 🐞 fix: 修复解析方法的问题 2023-12-20 16:19:16 +08:00
alger
a1780bc9d4 feat(music): 添加自动解析 并修改获取url的逻辑 2023-12-20 15:53:33 +08:00
alger
bb1b07e0b3 feat: 添加快捷键和关闭提示以及最小化功能 2023-12-20 10:23:15 +08:00
alger
7cb1b5fc7c 🐞 fix: 修复用户头像显示问题 2023-12-19 14:48:27 +08:00
alger
f70aa9e0a0 🐞 fix: 修复用户背景不展示的问题 2023-12-19 14:45:12 +08:00
alger
6c8229a21d feat: 修改样式和启动命令 2023-12-19 14:42:53 +08:00
algerkong
9211dcd3bb feat(build): 完善打包 2023-12-18 23:07:44 +08:00
alger
043ad5906b feat(打包初始化): 2023-12-18 19:39:36 +08:00
algerkc@qq.com
cf598f1c9c feat: 添加依赖以及配置 2023-12-18 16:41:56 +08:00
195 changed files with 17795 additions and 2893 deletions

View File

@@ -1,4 +1,12 @@
VITE_API = /api
VITE_API_MT = /mt
VITE_API_MUSIC = /music
VITE_API_PROXY = http://110.42.251.190:9856
# 你的接口地址 (必填)
VITE_API_LOCAL = ***
# 音乐破解接口地址
VITE_API_MUSIC = ***
# 代理地址
VITE_API_PROXY = ***
# 本地运行代理地址
VITE_API_PROXY = /api
VITE_API_MUSIC_PROXY = /music
VITE_API_PROXY_MUSIC = /music_proxy

View File

@@ -1,4 +0,0 @@
VITE_API = http://110.42.251.190:9898
VITE_API_MT = http://mt.myalger.top
VITE_API_MUSIC = http://myalger.top:4000
VITE_API_PROXY = http://110.42.251.190:9856

4
.eslintignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
out
.gitignore

137
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,137 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'eslint-config-airbnb-base',
'@vue/typescript/recommended',
'plugin:vue/vue3-recommended',
'plugin:vue-scoped-css/base',
'@electron-toolkit',
'@electron-toolkit/eslint-config-ts/eslint-recommended',
'plugin:prettier/recommended'
],
env: {
browser: true,
node: true,
jest: true,
es6: true
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly'
},
plugins: ['vue', '@typescript-eslint', 'simple-import-sort'],
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
allowImportExportEverywhere: true,
ecmaFeatures: {
jsx: true
}
},
settings: {
'import/extensions': ['.js', '.jsx', '.ts', '.tsx']
},
rules: {
'vue/require-default-prop': 'off',
'vue/multi-word-component-names': 'off',
'no-nested-ternary': 'off',
'no-console': 'off',
'no-await-in-loop': 'off',
'no-continue': 'off',
'no-restricted-syntax': 'off',
'no-return-assign': 'off',
'no-unused-expressions': 'off',
'no-return-await': 'off',
'no-plusplus': 'off',
'no-param-reassign': 'off',
'no-shadow': 'off',
'guard-for-in': 'off',
'import/extensions': 'off',
'import/no-unresolved': 'off',
'import/no-extraneous-dependencies': 'off',
'import/prefer-default-export': 'off',
'import/first': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'vue/first-attribute-linebreak': 0,
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}
],
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'class-methods-use-this': 'off',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error'
},
overrides: [
{
files: ['*.vue'],
rules: {
'vue/component-name-in-template-casing': [2, 'kebab-case'],
'vue/require-default-prop': 0,
'vue/multi-word-component-names': 0,
'vue/no-reserved-props': 0,
'vue/no-v-html': 0,
'vue-scoped-css/enforce-style-type': [
'error',
{
allows: ['scoped']
}
],
'@typescript-eslint/explicit-function-return-type': 'off',
// 需要行尾分号
'prettier/prettier': ['error', { endOfLine: 'auto' }]
}
},
{
files: ['*.ts', '*.tsx'],
rules: {
'max-classes-per-file': 'off',
'no-await-in-loop': 'off',
'dot-notation': 'off',
'constructor-super': 'off',
'getter-return': 'off',
'no-const-assign': 'off',
'no-dupe-args': 'off',
'no-dupe-class-members': 'off',
'no-dupe-keys': 'off',
'no-func-assign': 'off',
'no-import-assign': 'off',
'no-new-symbol': 'off',
'no-obj-calls': 'off',
'no-redeclare': 'off',
'no-setter-return': 'off',
'no-this-before-super': 'off',
'no-undef': 'off',
'no-unreachable': 'off',
'no-unsafe-negation': 'off',
'no-var': 'error',
'prefer-const': 'error',
'prefer-rest-params': 'error',
'prefer-spread': 'error',
'valid-typeof': 'off',
'consistent-return': 'off',
'no-promise-executor-return': 'off',
'prefer-promise-reject-errors': 'off',
'@typescript-eslint/explicit-function-return-type': 'off'
}
}
]
};

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @algerkong

View File

@@ -0,0 +1,70 @@
name: 反馈 Bug
description: 通过 github 模板进行 Bug 反馈。
title: "描述问题的标题"
body:
- type: markdown
attributes:
value: |
# 欢迎你的参与
Issue 列表接受 bug 报告或是新功能请求。
在发布一个 Issue 前,请确保:
- 在Issue中搜索过你的问题。你的问题可能已有人提出也可能已在最新版本中被修正
- 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。
- type: input
id: reproduce
attributes:
label: 重现链接
description: 请提供尽可能精简的 CodePen、CodeSandbox 或 GitHub 仓库的链接。请不要填无关链接,否则你的 Issue 将被关闭。
placeholder: 请填写
- type: textarea
id: reproduceSteps
attributes:
label: 重现步骤
description: 请清晰的描述重现该 Issue 的步骤,这能帮助我们快速定位问题。没有清晰重现步骤将不会被修复,标有 'need reproduction' 的 Issue 在 7 天内不提供相关步骤,将被关闭。
placeholder: 请填写
- type: textarea
id: expect
attributes:
label: 期望结果
placeholder: 请填写
- type: textarea
id: actual
attributes:
label: 实际结果
placeholder: 请填写
- type: input
id: frameworkVersion
attributes:
label: 框架版本
placeholder: Vue(3.3.0)
- type: input
id: browsersVersion
attributes:
label: 浏览器版本
placeholder: Chrome(8.213.231.123)
- type: input
id: systemVersion
attributes:
label: 系统版本
placeholder: MacOS(11.2.3)
- type: input
id: nodeVersion
attributes:
label: Node版本
placeholder: 请填写
- type: textarea
id: remarks
attributes:
label: 补充说明
description: 可以是遇到这个 bug 的业务场景、上下文等信息。
placeholder: 请填写

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name:
url:
about:

View File

@@ -0,0 +1,29 @@
name: 反馈新功能
description: 通过 github 模板进行新功能反馈。
title: "描述问题的标题"
body:
- type: markdown
attributes:
value: |
# 欢迎你的参与
在发布一个 Issue 前,请确保:
- 在 Issue 中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正)
- 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。
- type: textarea
id: functionContent
attributes:
label: 这个功能解决了什么问题
description: 请详尽说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。我们将考虑添加在现有 API 无法轻松实现的功能。新功能的用例也应当足够常见。
placeholder: 请填写
validations:
required: true
- type: textarea
id: functionalExpectations
attributes:
label: 你建议的方案是什么
placeholder: 请填写
validations:
required: true

51
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,51 @@
<!--
首先,感谢你的贡献!😄
PR 在维护者审核通过后会合并,谢谢!
-->
### 🤔 这个 PR 的性质是?
- [ ] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他
### 🔗 相关 Issue
<!--
1. 描述相关需求的来源,如相关的 issue 讨论链接。
-->
### 💡 需求背景和解决方案
<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->
### 📝 更新日志
<!--
从用户角度描述具体变化,以及可能的 breaking change 和其他风险。
-->
- fix(组件名称): 处理问题或特性描述 ...
- [ ] 本条 PR 不需要纳入 Changelog
### ☑️ 请求合并前的自查清单
⚠️ 请自检并全部**勾选全部选项**。⚠️
- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
- [ ] Changelog 已提供或无须提供

20
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
# Basic dependabot.yml file with
# minimum configuration for two package managers
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: 'npm'
# Look for `package.json` and `lock` files in the `root` directory
directory: '/'
# Check the npm registry for updates every day (weekdays)
schedule:
interval: 'monthly'
# Enable version updates for Docker
- package-ecosystem: 'docker'
# Look for a `Dockerfile` in the `root` directory
directory: '/'
# Check for updates once a week
schedule:
interval: 'monthly'

8
.github/issue-shoot.md vendored Normal file
View File

@@ -0,0 +1,8 @@
## IssueShoot
- 预估时长: {{ .duration }}
- 期望完成时间: {{ .deadline }}
- 开发难度: {{ .level }}
- 参与人数: 1
- 需求对接人: ivringpeng
- 验收标准: 实现期望改造效果,提 PR 并通过验收无误
- 备注: 最终激励以实际提交 `pull request` 并合并为准

87
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 18
- name: Install Dependencies
run: npm install
# MacOS Build
- name: Build MacOS
if: matrix.os == 'macos-latest'
run: |
export ELECTRON_BUILDER_EXTRA_ARGS="--universal"
npm run build:mac
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CSC_IDENTITY_AUTO_DISCOVERY: false
DEBUG: electron-builder
# Windows Build
- name: Build Windows
if: matrix.os == 'windows-latest'
run: npm run build:win
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Linux Build
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
npm run build:linux
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Get version from tag
- name: Get version from tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
shell: bash
# Read release notes
- name: Read release notes
id: release_notes
run: |
NOTES=$(awk "/## \[v${{ env.VERSION }}\]/{p=1;print;next} /## \[v/{p=0}p" CHANGELOG.md)
echo "NOTES<<EOF" >> $GITHUB_ENV
echo "$NOTES" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
shell: bash
# Upload artifacts
- name: Upload artifacts
uses: softprops/action-gh-release@v1
with:
files: |
dist/*.dmg
dist/*.exe
dist/*.deb
dist/*.AppImage
dist/latest*.yml
dist/*.blockmap
body: ${{ env.NOTES }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

18
.gitignore vendored
View File

@@ -1,8 +1,24 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
dist_electron
.idea
# lock
yarn.lock
pnpm-lock.yaml
dist.zip
package-lock.json
dist.zip
.vscode
bun.lockb
.env.*.local
out
.cursorrules

6
.prettierignore Normal file
View File

@@ -0,0 +1,6 @@
out
dist
pnpm-lock.yaml
LICENSE.md
tsconfig.json
tsconfig.*.json

5
.prettierrc.yaml Normal file
View File

@@ -0,0 +1,5 @@
singleQuote: true
semi: true
printWidth: 100
trailingComma: none
endOfLine: auto

View File

@@ -1,3 +0,0 @@
{
"compile-hero.disable-compile-files-on-did-save-code": true
}

18
CHANGELOG.md Normal file
View File

@@ -0,0 +1,18 @@
# 更新日志
## v3.8.0
### ✨ 新功能
- 添加歌词界面样式配置功能
- 添加字体配置功能,支持歌词页面或全局字体配置
- 实现应用单例模式,防止重复启动
### ⚡ 优化
- 优化播放体验,去除歌曲地址缓存
- 优化播放下一首逻辑
- 优化播放控制逻辑
## 咖啡☕️
| 微信 | | 支付宝 |
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
| <img src="https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/src/renderer/assets/wechat.png" alt="WeChat QRcode" width=200>| | <img src="https://www.ghproxy.cn/https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/dev_electron/src/renderer/assets/alipay.png" alt="Wechat QRcode" width=200> |

View File

@@ -1,5 +1,63 @@
# Vue 3 + Typescript + Vite
# Alger Music Player
主要功能如下
- 🎵 音乐推荐
- 🔐 网易云账号登录与同步
- 📝 功能
- 播放历史记录
- 歌曲收藏管理
- 自定义快捷键配置
- 🎨 界面与交互
- 沉浸式歌词显示(点击左下角封面进入)
- 独立桌面歌词窗口
- 明暗主题切换
- 🎼 音乐功能
- 支持歌单、MV、专辑等完整音乐服务
- 灰色音乐资源解析(基于 @unblockneteasemusic/server
- 高品质音乐试听需网易云VIP
- 音乐文件下载(支持右键下载和批量下载)
- 🚀 技术特性
- 本地化服务无需依赖在线API (基于 netease-cloud-music-api)
- 自动更新检测
- 全平台适配Desktop & Web & Mobile Web
vue3 + TypeScript + NaiveUI + animateCss + Vuex + VueRouter + Axios等实现音乐桌面web端
实现各项功能
网站地址http://mc.myalger.top/
## 项目简介
一个基于 electron typescript vue3 的桌面音乐播放器 适配 web端 桌面端 web移动端
## 预览地址
[http://mc.alger.fun/](http://mc.alger.fun/)
QQ群:789288579
## 软件截图
![首页白](./docs/image.png)
![首页黑](./docs/image3.png)
![歌词](./docs/image1.png)
![桌面歌词](./docs/image2.png)
![设置页面](./docs/image4.png)
## 技术栈
### 主要框架
- Vue 3 - 渐进式 JavaScript 框架
- TypeScript - JavaScript 的超集,添加了类型系统
- Electron - 跨平台桌面应用开发框架
- Vite - 下一代前端构建工具
- Naive UI - 基于 Vue 3 的组件库
## 咖啡☕️
| 微信 | 支付宝 |
| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |
| <img src="https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true" alt="WeChat QRcode" width=200> | <img src="https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true" alt="Wechat QRcode" width=200> |
## Stargazers over time
[![Stargazers over time](https://starchart.cc/algerkong/AlgerMusicPlayer.svg?variant=adaptive)](https://starchart.cc/algerkong/AlgerMusicPlayer)
## 欢迎提Issues
## 免责声明
本软件仅用于学习交流,禁止用于商业用途,否则后果自负。

90
auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,90 @@
/* eslint-disable */
/* prettier-ignore */
// @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 onWatcherCleanup: (typeof import('vue'))['onWatcherCleanup'];
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 useId: (typeof import('vue'))['useId'];
const useLoadingBar: (typeof import('naive-ui'))['useLoadingBar'];
const useMessage: (typeof import('naive-ui'))['useMessage'];
const useModel: (typeof import('vue'))['useModel'];
const useNotification: (typeof import('naive-ui'))['useNotification'];
const useSlots: (typeof import('vue'))['useSlots'];
const useTemplateRef: (typeof import('vue'))['useTemplateRef'];
const watch: (typeof import('vue'))['watch'];
const watchEffect: (typeof import('vue'))['watchEffect'];
const watchPostEffect: (typeof import('vue'))['watchPostEffect'];
const watchSyncEffect: (typeof import('vue'))['watchSyncEffect'];
}
// for type re-export
declare global {
// @ts-ignore
export type {
Component,
ComponentPublicInstance,
ComputedRef,
DirectiveBinding,
ExtractDefaultPropTypes,
ExtractPropTypes,
ExtractPublicPropTypes,
InjectionKey,
PropType,
Ref,
MaybeRef,
MaybeRefOrGetter,
VNode,
WritableComputedRef
} from 'vue';
import('vue');
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.files.downloads.read-write</key>
<true/>
</dict>
</plist>

BIN
build/icon.icns Normal file

Binary file not shown.

BIN
build/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

BIN
build/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

13
build/installer.nsh Normal file
View File

@@ -0,0 +1,13 @@
# 设置 Windows 7 兼容性
ManifestDPIAware true
ManifestSupportedOS all
!macro customInit
# 检查系统版本
${If} ${AtLeastWin7}
# Windows 7 或更高版本
${Else}
MessageBox MB_OK|MB_ICONSTOP "此应用程序需要 Windows 7 或更高版本。"
Abort
${EndIf}
!macroend

33
components.d.ts vendored Normal file
View File

@@ -0,0 +1,33 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {};
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDropdown: typeof import('naive-ui')['NDropdown']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NPopover: typeof import('naive-ui')['NPopover']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSlider: typeof import('naive-ui')['NSlider']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

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

@@ -0,0 +1,3 @@
provider: generic
url: https://example.com/auto-updates
updaterCacheDirName: electron-lan-file-updater

BIN
docs/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
docs/image1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

BIN
docs/image2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
docs/image3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

BIN
docs/image4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

45
electron-builder.yml Normal file
View File

@@ -0,0 +1,45 @@
appId: com.electron.app
productName: electron-lan-file
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}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
asarUnpack:
- resources/**
win:
executableName: electron-lan-file
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
mac:
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.
notarize: false
dmg:
artifactName: ${name}-${version}.${ext}
linux:
target:
- AppImage
- snap
- deb
maintainer: electronjs.org
category: Utility
appImage:
artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
provider: generic
url: https://example.com/auto-updates
electronDownload:
mirror: https://npmmirror.com/mirrors/electron/

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

@@ -0,0 +1,60 @@
import vue from '@vitejs/plugin-vue';
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import { resolve } from 'path';
import AutoImport from 'unplugin-auto-import/vite';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()]
},
preload: {
plugins: [externalizeDepsPlugin()]
},
renderer: {
resolve: {
alias: {
'@': resolve('src/renderer'),
'@renderer': resolve('src/renderer')
}
},
plugins: [
vue(),
viteCompression(),
// VueDevTools(),
AutoImport({
imports: [
'vue',
{
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']
}
]
}),
Components({
resolvers: [NaiveUiResolver()]
})
],
server: {
proxy: {
// with options
[process.env.VITE_API_LOCAL as string]: {
target: process.env.VITE_API,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_LOCAL}`), '')
},
[process.env.VITE_API_MUSIC_PROXY as string]: {
target: process.env.VITE_API_MUSIC,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_MUSIC_PROXY}`), '')
},
[process.env.VITE_API_PROXY_MUSIC as string]: {
target: process.env.VITE_API_PROXY,
changeOrigin: true,
rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_PROXY_MUSIC}`), '')
}
}
}
}
});

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>网抑云 | algerkong</title>
<link
rel="stylesheet"
href="//at.alicdn.com/t/font_2685283_5bo4ekd5wh.css"
/>
<link rel="stylesheet" href="./public/css/animate.css" />
<style>
:root {
--animate-delay: 0.5s;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,33 +1,165 @@
{
"version": "0.0.0",
"name": "AlgerMusicPlayer",
"version": "3.8.0",
"description": "Alger Music Player",
"author": "Alger <algerkc@qq.com>",
"main": "./out/main/index.js",
"homepage": "https://github.com/algerkong/AlgerMusicPlayer",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
"format": "prettier --write .",
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --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",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@tailwindcss/postcss7-compat": "^2.2.4",
"@vue/runtime-core": "^3.3.4",
"autoprefixer": "^9.8.6",
"axios": "^0.21.1",
"lodash": "^4.17.21",
"postcss": "^7.0.36",
"sass": "^1.35.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vuex": "^4.1.0"
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@unblockneteasemusic/server": "^0.27.8-patch.1",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.7",
"font-list": "^1.5.1",
"netease-cloud-music-api-alger": "^4.25.0"
},
"devDependencies": {
"@sicons/antd": "^0.10.0",
"@vicons/antd": "^0.10.0",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.3.4",
"naive-ui": "^2.34.4",
"typescript": "^4.3.2",
"@electron-toolkit/eslint-config": "^1.0.2",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@rushstack/eslint-patch": "^1.10.3",
"@tailwindcss/postcss7-compat": "^2.2.4",
"@types/howler": "^2.2.12",
"@types/node": "^20.14.8",
"@types/tinycolor2": "^1.4.6",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/compiler-sfc": "^3.5.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/runtime-core": "^3.5.0",
"@vueuse/core": "^11.0.3",
"@vueuse/electron": "^11.0.3",
"animate.css": "^4.1.1",
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"cross-env": "^7.0.3",
"electron": "^31.0.2",
"electron-builder": "^24.13.3",
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-vue": "^9.26.0",
"eslint-plugin-vue-scoped-css": "^2.7.2",
"howler": "^2.2.4",
"lodash": "^4.17.21",
"marked": "^15.0.4",
"naive-ui": "^2.41.0",
"postcss": "^8.4.49",
"prettier": "^3.3.2",
"remixicon": "^4.2.0",
"sass": "^1.82.0",
"tailwindcss": "^3.4.15",
"tinycolor2": "^1.6.0",
"typescript": "^5.5.2",
"unplugin-auto-import": "^0.18.2",
"unplugin-vue-components": "^0.27.4",
"vfonts": "^0.1.0",
"vite": "^4.4.7",
"vite-plugin-vue-devtools": "1.0.0-beta.5",
"vue-tsc": "^0.0.24"
"vite": "^5.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vue-devtools": "7.4.0",
"vue": "^3.4.30",
"vue-router": "^4.4.3",
"vue-tsc": "^2.0.22",
"vuex": "^4.1.0"
},
"build": {
"appId": "com.alger.music",
"productName": "AlgerMusicPlayer",
"publish": [
{
"provider": "github",
"owner": "algerkong",
"repo": "AlgerMusicPlayer"
}
],
"mac": {
"icon": "resources/icon.icns",
"target": [
{
"target": "dmg",
"arch": [
"universal"
]
}
],
"artifactName": "${productName}-${version}-mac-${arch}.${ext}",
"darkModeSupport": true,
"hardenedRuntime": false,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist",
"notarize": false,
"identity": null,
"type": "distribution",
"binaries": [
"Contents/MacOS/AlgerMusicPlayer"
]
},
"win": {
"icon": "resources/favicon.ico",
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
}
],
"artifactName": "${productName}-${version}-win-${arch}.${ext}",
"requestedExecutionLevel": "asInvoker"
},
"linux": {
"icon": "resources/icon.png",
"target": [
{
"target": "AppImage",
"arch": [
"x64"
]
},
{
"target": "deb",
"arch": [
"x64"
]
}
],
"artifactName": "${productName}-${version}-linux-${arch}.${ext}",
"category": "Audio",
"maintainer": "Alger <algerkc@qq.com>"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"installerIcon": "resources/favicon.ico",
"uninstallerIcon": "resources/favicon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "AlgerMusicPlayer",
"include": "build/installer.nsh"
}
}
}

View File

@@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
autoprefixer: {}
}
};

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

BIN
resources/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

BIN
resources/icon.icns Normal file

Binary file not shown.

BIN
resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
resources/icon_16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

10
resources/manifest.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "Alger Music PWA",
"icons": [
{
"src": "./icon.png",
"type": "image/png",
"sizes": "256x256"
}
]
}

View File

@@ -1,22 +0,0 @@
<template>
<div class="app">
<n-config-provider :theme="darkTheme">
<router-view></router-view>
</n-config-provider>
</div>
</template>
<script lang="ts" setup>
import { darkTheme } from 'naive-ui'
</script>
<style lang="scss" scoped >
div {
box-sizing: border-box;
}
.app {
user-select: none;
}
</style>

View File

@@ -1,45 +0,0 @@
import request from "@/utils/request";
import { IHotSinger } from "@/type/singer";
import { ISearchKeyword, IHotSearch } from "@/type/search";
import { IPlayListSort } from "@/type/playlist";
import { IRecommendMusic } from "@/type/music";
import { IAlbumNew } from "@/type/album";
interface IHotSingerParams {
offset: number;
limit: number;
}
interface IRecommendMusicParams {
limit: number;
}
// 获取热门歌手
export const getHotSinger = (params: IHotSingerParams) => {
return request.get<IHotSinger>("/top/artists", { params });
};
// 获取搜索推荐词
export const getSearchKeyword = () => {
return request.get<ISearchKeyword>("/search/default");
};
// 获取热门搜索
export const getHotSearch = () => {
return request.get<IHotSearch>("/search/hot/detail");
};
// 获取歌单分类
export const getPlaylistCategory = () => {
return request.get<IPlayListSort>("/playlist/catlist");
};
// 获取推荐音乐
export const getRecommendMusic = (params: IRecommendMusicParams) => {
return request.get<IRecommendMusic>("/personalized/newsong", { params });
};
// 获取最新专辑推荐
export const getNewAlbum = () => {
return request.get<IAlbumNew>("/album/newest");
};

View File

@@ -1,17 +0,0 @@
import { IPlayMusicUrl } from "@/type/music"
import { ILyric } from "@/type/lyric"
import request from "@/utils/request"
import requestMusic from "@/utils/request_music"
// 根据音乐Id获取音乐播放URl
export const getMusicUrl = (id: number) => {
return request.get<IPlayMusicUrl>("/song/url", { params: { id: id } })
}
// 根据音乐Id获取音乐歌词
export const getMusicLrc = (id: number) => {
return request.get<ILyric>("/lyric", { params: { id: id } })
}
export const getParsingMusicUrl = (id: number) => {
return requestMusic.get<any>("/music", { params: { id: id } })
}

View File

@@ -1,9 +0,0 @@
import request from "@/utils/request"
import { ISearchDetail } from "@/type/search"
// 搜索内容
export const getSearch = (keywords: any) => {
return request.get<any>("/cloudsearch", {
params: { keywords: keywords, type: 1 },
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,79 +0,0 @@
<template>
<!-- 歌单分类列表 -->
<div class="play-list-type">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">歌单分类</div>
<n-layout>
<template v-for="(item, index) in playlistCategory?.sub" :key="item.name">
<span
class="play-list-type-item"
:class="setAnimationClass('animate__bounceIn')"
:style="setAnimationDelay(index <= 19 ? index : index - 19)"
v-show="isShowAllPlaylistCategory || index <= 19"
@click="handleClickPlaylistType(item.name)"
>{{ item.name }}</span>
</template>
<div
class="play-list-type-showall"
:class="setAnimationClass('animate__bounceIn')"
:style="
setAnimationDelay(
!isShowAllPlaylistCategory
? 25
: playlistCategory?.sub.length || 100 + 30
)
"
@click="isShowAllPlaylistCategory = !isShowAllPlaylistCategory"
>{{ !isShowAllPlaylistCategory ? "显示全部" : "隐藏一些" }}</div>
</n-layout>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
import { getPlaylistCategory } from "@/api/home";
import type { IPlayListSort } from "@/type/playlist";
import { setAnimationDelay, setAnimationClass } from "@/utils";
import { useRoute, useRouter } from "vue-router";
// 歌单分类
const playlistCategory = ref<IPlayListSort>();
// 是否显示全部歌单分类
const isShowAllPlaylistCategory = ref<boolean>(false);
// 加载歌单分类
const loadPlaylistCategory = async () => {
const { data } = await getPlaylistCategory();
playlistCategory.value = data;
};
const router = useRouter();
const handleClickPlaylistType = (type: any) => {
router.push({
path: "/list",
query: {
type: type,
}
});
};
// 页面初始化
onMounted(() => {
loadPlaylistCategory();
});
</script>
<style lang="scss" scoped>
.title {
@apply text-lg font-bold mb-4;
}
.play-list-type {
width: 250px;
@apply mx-6;
&-item,
&-showall {
@apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 transition;
background-color: #1a1a1a;
}
&-showall {
@apply block text-center;
}
}
</style>

View File

@@ -1,69 +0,0 @@
<template>
<div class="recommend-album">
<div class="title" :class="setAnimationClass('animate__fadeInLeft')">最新专辑</div>
<div class="recommend-album-list">
<template v-for="(item,index) in albumData?.albums" :key="item.id">
<div
v-if="index < 6"
class="recommend-album-list-item"
:class="setAnimationClass('animate__backInUp')"
:style="setAnimationDelay(index, 100)"
>
<n-image
class="recommend-album-list-item-img"
:src="getImgUrl( item.blurPicUrl, '200y200')"
lazy
preview-disabled
/>
<div class="recommend-album-list-item-content">{{ item.name }}</div>
</div>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { getNewAlbum } from "@/api/home"
import { ref, onMounted } from "vue";
import type { IAlbumNew } from "@/type/album"
import { setAnimationClass, setAnimationDelay, getImgUrl } from "@/utils";
const albumData = ref<IAlbumNew>()
const loadAlbumList = async () => {
const { data } = await getNewAlbum();
albumData.value = data
}
onMounted(() => {
loadAlbumList()
})
</script>
<style lang="scss" scoped>
.recommend-album {
@apply flex-1 mx-5;
.title {
@apply text-lg font-bold mb-4;
}
.recommend-album-list {
@apply grid grid-cols-2 grid-rows-3 gap-2;
&-item {
@apply rounded-xl overflow-hidden relative;
&-img {
@apply rounded-xl transition w-full h-full;
}
&:hover img {
filter: brightness(50%);
}
&-content {
@apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl bg-opacity-60 bg-black;
}
&-content:hover {
opacity: 1;
}
}
}
}
</style>

View File

@@ -1,85 +0,0 @@
<template>
<!-- 推荐歌手 -->
<div class="recommend-singer">
<div class="recommend-singer-list">
<div
class="recommend-singer-item relative"
:class="setAnimationClass('animate__backInRight')"
v-for="(item, index) in hotSingerData?.artists"
:style="setAnimationDelay(index, 100)"
:key="item.id"
>
<div
:style="setBackgroundImg(getImgUrl(item.picUrl,'300y300'))"
class="recommend-singer-item-bg"
></div>
<div
class="recommend-singer-item-count p-2 text-base text-gray-200 z-10"
>{{ item.musicSize }}</div>
<div class="recommend-singer-item-info z-10">
<div class="recommend-singer-item-info-play" @click="toSearchSinger(item.name)">
<i class="iconfont icon-playfill text-xl"></i>
</div>
<div class="ml-4">
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
<div class="recommend-singer-item-info-name">{{ item.name }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { setBackgroundImg, setAnimationDelay, setAnimationClass,getImgUrl } from "@/utils";
import { onMounted, ref } from "vue";
import { getHotSinger } from "@/api/home";
import type { IHotSinger } from "@/type/singer";
import router from "@/router";
// 歌手信息
const hotSingerData = ref<IHotSinger>();
//加载推荐歌手
const loadSingerList = async () => {
const { data } = await getHotSinger({ offset: 0, limit: 5 });
hotSingerData.value = data;
};
// 页面初始化
onMounted(() => {
loadSingerList();
});
const toSearchSinger = (keyword: string) => {
router.push({
path: "/search",
query: {
keyword: keyword,
},
});
};
</script>
<style lang="scss" scoped>
.recommend-singer {
&-list {
@apply flex;
height: 350px;
}
&-item {
@apply flex-1 h-full rounded-3xl p-5 mr-5 flex flex-col justify-between;
&-bg {
@apply bg-gray-900 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;
filter: brightness(80%);
}
&-info {
@apply flex items-center p-2;
&-play {
@apply w-12 h-12 bg-green-500 rounded-full flex justify-center items-center hover:bg-green-600 cursor-pointer;
}
}
}
}
</style>

View File

@@ -1,43 +0,0 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { setAnimationClass, setAnimationDelay } from "@/utils";
const props = defineProps({
showPop: {
type: Boolean,
default: false
},
showClose: {
type: Boolean,
default: true
},
})
const musicFullClass = computed(() => {
if (props.showPop) {
return setAnimationClass('animate__fadeInUp')
} else {
return setAnimationClass('animate__fadeOutDown')
}
})
</script>
<template>
<div class="pop-page" v-show="props.showPop" :class="musicFullClass">
<i class="iconfont icon-icon_error close" v-if="props.showClose" @click="close()"></i>
<img src="http://code.myalger.top/2000*2000.jpg,f054f0,0f2255" />
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
.pop-page {
height: 800px;
@apply absolute top-4 left-0 w-full;
background-color: #000000f0;
.close {
@apply absolute top-4 right-4 cursor-pointer text-white text-3xl;
}
}
</style>

View File

@@ -1,116 +0,0 @@
<template>
<div class="recommend-music-list-item">
<n-image
:src="getImgUrl( item.picUrl, '40y40')"
class="recommend-music-list-item-img"
lazy
preview-disabled
/>
<div class="recommend-music-list-item-content">
<div class="recommend-music-list-item-content-title">
<n-ellipsis class="text-ellipsis" line-clamp="1">{{
item.name
}}</n-ellipsis>
</div>
<div class="recommend-music-list-item-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1">
<span
v-for="(artists, artistsindex) in item.song.artists"
:key="artistsindex"
>{{ artists.name
}}{{
artistsindex < item.song.artists.length - 1 ? ' / ' : ''
}}</span
>
</n-ellipsis>
</div>
</div>
<div class="recommend-music-list-item-operating">
<div class="recommend-music-list-item-operating-like">
<i class="iconfont icon-likefill"></i>
</div>
<div
class="recommend-music-list-item-operating-play bg-black"
:class="isPlaying ? 'bg-green-600' : ''"
@click="playMusicEvent(item)"
>
<i v-if="isPlaying && play" class="iconfont icon-stop"></i>
<i v-else class="iconfont icon-playfill"></i>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useStore } from 'vuex'
import type { SongResult } from '@/type/music'
import { computed } from 'vue'
import { getImgUrl } from '@/utils'
const props = defineProps({
item: {
type: Object,
required: true,
},
})
const store = useStore()
const play = computed(() => store.state.play as boolean)
const playMusic = computed(() => store.state.playMusic)
// 判断是否为正在播放的音乐
const isPlaying = computed(() => {
return playMusic.value.id == props.item.id
})
const emits = defineEmits(['play'])
// 播放音乐 设置音乐详情 打开音乐底栏
const playMusicEvent = (item: any) => {
store.commit('setPlay', item)
store.commit('setIsPlay', true)
store.state.playListIndex = 0
emits('play', item)
}
</script>
<style lang="scss" scoped>
.text-ellipsis {
width: 100%;
}
.recommend-music-list-item {
@apply rounded-3xl p-3 flex items-center hover:bg-gray-800 transition;
&-img {
@apply w-12 h-12 rounded-2xl mr-4;
}
&-content {
@apply flex-1;
&-title {
@apply text-base text-white;
}
&-name {
@apply text-xs;
@apply text-gray-400;
}
}
&-operating {
@apply flex items-center pl-4 rounded-full border border-gray-700;
background-color: #0d0d0d;
.iconfont {
@apply text-xl;
}
.icon-likefill {
color: #868686;
@apply text-xl hover:text-red-600 transition;
}
&-like {
@apply mr-2 cursor-pointer;
}
&-play {
@apply cursor-pointer border border-gray-500 rounded-full w-10 h-10 flex justify-center items-center hover:bg-green-600 transition;
}
}
}
</style>

View File

@@ -1,104 +0,0 @@
import { getMusicLrc } from '@/api/music'
import { ILyric } from '@/type/lyric'
import { ref } from 'vue'
interface ILrcData {
text: string
trText: string
}
const lrcData = ref<ILyric>()
const newLrcIndex = ref<number>(0)
const lrcArray = ref<Array<ILrcData>>([])
const lrcTimeArray = ref<Array<Number>>([])
const parseTime = (timeString: string) => {
const [minutes, seconds] = timeString.split(':')
return parseInt(minutes) * 60 + parseFloat(seconds)
}
const TIME_REGEX = /(\d{2}:\d{2}(\.\d*)?)/g
const LRC_REGEX = /(\[(\d{2}):(\d{2})(\.(\d*))?\])/g
function parseLyricLine(lyricLine: string) {
// [00:00.00] 作词 : 长友美知惠/
const timeText = lyricLine.match(TIME_REGEX)?.[0] || ''
const time = parseTime(timeText)
const text = lyricLine.replace(LRC_REGEX, '').trim()
return { time, text }
}
interface ILyricText {
text: string
trText: string
}
function parseLyrics(lyricsString: string) {
const lines = lyricsString.split('\n')
const lyrics: Array<ILyricText> = []
const times: number[] = []
lines.forEach((line) => {
const { time, text } = parseLyricLine(line)
times.push(time)
lyrics.push({ text, trText: '' })
})
return { lyrics, times }
}
const loadLrc = async (playMusicId: number): Promise<void> => {
try {
const { data } = await getMusicLrc(playMusicId)
const { lyrics, times } = parseLyrics(data.lrc.lyric)
lrcTimeArray.value = times
lrcArray.value = lyrics
} catch (err) {
console.error('err', err)
}
}
// 歌词矫正时间Correction time
const correctionTime = ref(0)
// 增加矫正时间
const addCorrectionTime = (time: number) => {
correctionTime.value += time
}
// 减少矫正时间
const reduceCorrectionTime = (time: number) => {
correctionTime.value -= time
}
const isCurrentLrc = (index: any, time: number) => {
const currentTime = Number(lrcTimeArray.value[index])
const nextTime = Number(lrcTimeArray.value[index + 1])
const nowTime = time + correctionTime.value
const isTrue = nowTime > currentTime && nowTime < nextTime
if (isTrue) {
newLrcIndex.value = index
}
return isTrue
}
const nowTime = ref(0)
const allTime = ref(0)
// 设置当前播放时间
const setAudioTime = (index: any, audio: HTMLAudioElement) => {
audio.currentTime = lrcTimeArray.value[index] as number
audio.play()
}
export {
lrcData,
lrcArray,
lrcTimeArray,
newLrcIndex,
loadLrc,
isCurrentLrc,
addCorrectionTime,
reduceCorrectionTime,
setAudioTime,
nowTime,
allTime,
}

View File

@@ -1,11 +0,0 @@
/* ./src/index.css */
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;
.n-image img {
@apply bg-gray-900;
width: 100%;
}

View File

@@ -1,78 +0,0 @@
<template>
<div class="layout-page">
<div class="layout-main">
<div class="flex">
<!-- 侧边菜单栏 -->
<app-menu class="menu" :menus="menus" />
<div class="main">
<!-- 搜索栏 -->
<search-bar />
<!-- 主页面路由 -->
<div class="main-content bg-black" :native-scrollbar="false">
<n-message-provider>
<router-view class="main-page" v-slot="{ Component }">
<!-- <keep-alive>
<component :is="Component" v-if="$route.meta.keepAlive" />
</keep-alive>
<component :is="Component" v-if="!$route.meta.keepAlive" />-->
<component :is="Component" />
</router-view>
</n-message-provider>
</div>
</div>
</div>
<!-- 底部音乐播放 -->
<play-bar v-if="isPlay" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { SongResult } from '@/type/music';
import { computed } from 'vue';
import { useStore } from 'vuex';
// import { AppMenu, PlayBar, SearchBar } from './components';
import { defineAsyncComponent } from 'vue';
const AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));
const PlayBar = defineAsyncComponent(() => import('./components/PlayBar.vue'));
const SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));
const store = useStore();
const isPlay = computed(() => store.state.isPlay as boolean)
const menus = store.state.menus;
</script>
<style lang="scss" scoped>
.layout-page {
width: 100vw;
height: 100vh;
@apply flex justify-center items-center overflow-hidden;
}
.layout-main {
@apply bg-black rounded-lg text-white shadow-xl flex-col relative;
height: 100%;
width: 100%;
overflow: hidden;
.menu {
width: 90px;
}
.main {
@apply pt-6 pr-6 flex-1 box-border;
height: 100vh;
&-content {
@apply rounded-2xl;
height: calc(100vh - 60px);
margin-bottom: 90px;
}
&-page {
margin: 20px 0;
}
}
}
</style>

View File

@@ -1,87 +0,0 @@
<template>
<div>
<!-- menu -->
<div class="app-menu">
<div class="app-menu-header">
<div class="app-menu-logo">
<img src="@/assets/logo.png" class="w-9 h-9 mt-2" alt="logo" />
</div>
</div>
<div class="app-menu-list">
<div class="app-menu-item" v-for="(item,index) in menus">
<router-link class="app-menu-item-link" :to="item.path">
<i
class="iconfont app-menu-item-icon"
:style="iconStyle(index)"
:class="item.mate.icon"
></i>
<span v-if="isText" class="app-menu-item-text ml-3">{{ item.mate.title }}</span>
</router-link>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from "@vue/runtime-core";
import { useRoute } from "vue-router";
const props = defineProps({
isText: {
type: Boolean,
default: false
},
size: {
type: String,
default: '26px'
},
color: {
type: String,
default: '#aaa'
},
selectColor: {
type: String,
default: '#10B981'
},
menus: {
type: Array as any,
default: []
}
})
const route = useRoute();
const path = ref(route.path);
watch(() => route.path, async newParams => {
path.value = newParams
})
const iconStyle = (index: any) => {
let style = {
fontSize: props.size,
color: path.value === props.menus[index].path ? props.selectColor : props.color
}
return style
}
</script>
<style lang="scss" scoped>
.app-menu {
@apply flex-col items-center justify-center p-6;
max-width: 100px;
}
.app-menu-item-link,
.app-menu-header {
@apply flex items-center justify-center;
}
.app-menu-item-link {
@apply mb-6 mt-6;
}
.app-menu-item-icon:hover {
color: #10b981 !important;
transform: scale(1.05);
transition: 0.2s ease-in-out;
}
</style>

View File

@@ -1,158 +0,0 @@
<template>
<n-drawer
:show="musicFull"
height="100vh"
placement="bottom"
:drawer-style="{ backgroundColor: 'transparent' }"
>
<div id="drawer-target">
<div class="music-img">
<n-image
ref="PicImgRef"
:src="getImgUrl(playMusic?.picUrl, '300y300')"
class="img"
lazy
preview-disabled
/>
</div>
<div class="music-content">
<div class="music-content-name">{{ playMusic.song.name }}</div>
<div class="music-content-singer">
<span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span>
</div>
<n-layout
class="music-lrc"
style="height: 550px"
ref="lrcSider"
:native-scrollbar="false"
@mouseover="mouseOverLayout"
@mouseleave="mouseLeaveLayout"
>
<template v-for="(item, index) in lrcArray" :key="index">
<div
class="music-lrc-text"
:class="{ 'now-text': isCurrentLrc(index, nowTime) }"
@click="setAudioTime(index, audio)"
>
{{ item.text }}
</div>
</template>
</n-layout>
<!-- 时间矫正 -->
<div class="music-content-time"></div>
<n-button @click="reduceCorrectionTime(0.2)">-0.2</n-button>
<n-button @click="addCorrectionTime(0.2)">+0.2</n-button>
</div>
</div>
</n-drawer>
</template>
<script setup lang="ts">
import type { SongResult } from '@/type/music'
import { getImgUrl } from '@/utils'
import { computed, ref } from 'vue'
import { useStore } from 'vuex'
import {
lrcArray,
newLrcIndex,
isCurrentLrc,
addCorrectionTime,
reduceCorrectionTime,
setAudioTime,
nowTime,
} from '@/hooks/MusicHook'
const store = useStore()
const props = defineProps({
musicFull: {
type: Boolean,
default: false,
},
audio: {
type: HTMLAudioElement,
default: null,
},
})
const emit = defineEmits(['update:musicFull'])
// 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult)
// 获取歌词滚动dom
const lrcSider = ref<any>(null)
const isMouse = ref(false)
// 歌词滚动方法
const lrcScroll = () => {
if (props.musicFull && !isMouse.value) {
let top = newLrcIndex.value * 50 - 225
lrcSider.value.scrollTo({ top: top, behavior: 'smooth' })
}
}
const mouseOverLayout = () => {
isMouse.value = true
}
const mouseLeaveLayout = () => {
setTimeout(() => {
isMouse.value = false
}, 3000)
}
defineExpose({
lrcScroll,
})
</script>
<style scoped lang="scss">
#drawer-target {
@apply top-0 left-0 absolute w-full h-full overflow-hidden rounded px-24 pt-24 pb-48 flex items-center;
backdrop-filter: blur(20px);
background-color: rgba(0, 0, 0, 0.747);
animation-duration: 300ms;
.music-img {
@apply flex-1 flex justify-center mr-24;
.img {
width: 450px;
height: 450px;
@apply rounded-xl;
}
}
.music-content {
@apply flex flex-col justify-center items-center;
&-name {
@apply font-bold text-3xl py-2;
}
&-singer {
@apply text-base py-2;
}
}
.music-lrc {
background-color: inherit;
width: 800px;
height: 550px;
&-text {
@apply text-white text-lg flex justify-center items-center cursor-pointer;
height: 50px;
transition: all 0.2s ease-out;
&:hover {
@apply font-bold text-xl text-red-500;
}
}
.now-text {
@apply font-bold text-xl text-red-500;
}
}
}
</style>

View File

@@ -1,373 +0,0 @@
<template>
<!-- 展开全屏 -->
<music-full ref="MusicFullRef" v-model:musicFull="musicFull" :audio="(audio as HTMLAudioElement)" />
<!-- 底部播放栏 -->
<div class="music-play-bar" :class="setAnimationClass('animate__bounceInUp')">
<n-image
:src="getImgUrl(playMusic?.picUrl, '300y300')"
class="play-bar-img"
lazy
preview-disabled
@click="setMusicFull"
/>
<div class="music-content">
<div class="music-content-title">
<n-ellipsis class="text-ellipsis" line-clamp="1">
{{ playMusic.name }}
</n-ellipsis>
</div>
<div class="music-content-name">
<n-ellipsis class="text-ellipsis" line-clamp="1">
<span v-for="(item, index) in playMusic.song.artists" :key="index">
{{ item.name
}}{{ index < playMusic.song.artists.length - 1 ? ' / ' : '' }}
</span>
</n-ellipsis>
</div>
</div>
<div class="music-buttons">
<div @click="handlePrev">
<i class="iconfont icon-prev"></i>
</div>
<div class="music-buttons-play" @click="playMusicEvent">
<i class="iconfont icon" :class="play ? 'icon-stop' : 'icon-play'"></i>
</div>
<div @click="handleEnded">
<i class="iconfont icon-next"></i>
</div>
</div>
<div class="music-time">
<div class="time">{{ getNowTime }}</div>
<n-slider
v-model:value="timeSlider"
:step="0.05"
:tooltip="false"
></n-slider>
<div class="time">{{ getAllTime }}</div>
</div>
<div class="audio-volume">
<div>
<i class="iconfont icon-notificationfill"></i>
</div>
<n-slider
v-model:value="volumeSlider"
:step="0.01"
:tooltip="false"
></n-slider>
</div>
<div class="audio-button">
<n-tooltip trigger="hover">
<template #trigger>
<i class="iconfont icon-likefill"></i>
</template>
喜欢
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<i class="iconfont icon-Play" @click="parsingMusic"></i>
</template>
解析播放
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<i class="iconfont icon-full" @click="setMusicFull"></i>
</template>
歌词
</n-tooltip>
</div>
<!-- 播放音乐 -->
<audio ref="audio" :src="playMusicUrl" :autoplay="play"></audio>
</div>
</template>
<script lang="ts" setup>
import type { SongResult } from '@/type/music'
import { secondToMinute, getImgUrl } from '@/utils'
import { computed, onMounted, ref, watch } from 'vue'
import { useStore } from 'vuex'
import { setAnimationClass } from '@/utils'
import { getParsingMusicUrl } from '@/api/music'
import {
loadLrc,
nowTime,
allTime,
} from '@/hooks/MusicHook'
import MusicFull from './MusicFull.vue'
const store = useStore()
// 播放的音乐信息
const playMusic = computed(() => store.state.playMusic as SongResult)
// 是否播放
const play = computed(() => store.state.play as boolean)
// 播放链接
const ProxyUrl =
import.meta.env.VITE_API_PROXY + '' || 'http://110.42.251.190:9856'
const playMusicUrl = ref('')
watch(
() => store.state.playMusicUrl,
async (value, oldValue) => {
const isUrlHasMc = location.href.includes('mc.')
if (value && isUrlHasMc) {
let playMusicUrl1 = value as string
if (!ProxyUrl) {
playMusicUrl.value = playMusicUrl1
return
}
const url = new URL(playMusicUrl1)
const pathname = url.pathname
const subdomain = url.origin.split('.')[0].split('//')[1]
playMusicUrl1 = `${ProxyUrl}/mc?m=${subdomain}&url=${pathname}`
// console.log('playMusicUrl1', playMusicUrl1)
// // 获取音频文件
// const { data } = await axios.get(playMusicUrl1, {
// responseType: 'blob'
// })
// const musicUrl = URL.createObjectURL(data)
// console.log('musicUrl', musicUrl)
// playMusicUrl.value = musicUrl
playMusicUrl.value = playMusicUrl1
console.log('playMusicUrl1', playMusicUrl1)
setTimeout(() => {
onAudio()
store.commit('setPlayMusic', true)
}, 100)
} else {
playMusicUrl.value = value
}
loadLrc(playMusic.value.id)
},
{ immediate: true }
)
// 获取音乐播放Dom
onMounted(() => {
// 监听音乐是否播放
watch(
() => play.value,
(value, oldValue) => {
if (value && audio.value) {
audioPlay()
onAudio()
} else {
audioPause()
}
}
)
watch(
() => playMusicUrl.value,
(value, oldValue) => {
if (!value) {
parsingMusic()
}
}
)
// 抬起键盘按钮监听
document.onkeyup = (e) => {
switch (e.code) {
case 'Space':
playMusicEvent()
}
}
// 按下键盘按钮监听
document.onkeydown = (e) => {
switch (e.code) {
case 'Space':
return false
}
}
})
const audio = ref<HTMLAudioElement | null>(null)
const audioPlay = () => {
if (audio.value) {
audio.value.play()
}
}
const audioPause = () => {
if (audio.value) {
audio.value.pause()
}
}
// 计算属性 获取当前播放时间的进度
const timeSlider = computed({
get: () => (nowTime.value / allTime.value) * 100,
set: (value) => {
if (!audio.value) return
audio.value.currentTime = (value * allTime.value) / 100
audioPlay()
store.commit('setPlayMusic', true)
},
})
// 音量条
const audioVolume = ref(1)
const volumeSlider = computed({
get: () => audioVolume.value * 100,
set: (value) => {
if(!audio.value) return
audio.value.volume = value / 100
},
})
// 获取当前播放时间
const getNowTime = computed(() => {
return secondToMinute(nowTime.value)
})
// 获取总时间
const getAllTime = computed(() => {
return secondToMinute(allTime.value)
})
// 监听音乐播放 获取时间
const onAudio = () => {
if(audio.value){
audio.value.removeEventListener('timeupdate', handleGetAudioTime)
audio.value.removeEventListener('ended', handleEnded)
audio.value.addEventListener('timeupdate', handleGetAudioTime)
audio.value.addEventListener('ended', handleEnded)
}
}
function handleEnded() {
store.commit('nextPlay')
}
function handlePrev() {
store.commit('prevPlay')
}
const MusicFullRef = ref<any>(null)
function handleGetAudioTime(this: any) {
// 监听音频播放的实时时间事件
const audio = this as HTMLAudioElement
// 获取当前播放时间
nowTime.value = Math.floor(audio.currentTime)
// 获取总时间
allTime.value = audio.duration
// 获取音量
audioVolume.value = audio.volume
MusicFullRef.value?.lrcScroll()
}
// 播放暂停按钮事件
const playMusicEvent = async () => {
if (play.value) {
store.commit('setPlayMusic', false)
} else {
store.commit('setPlayMusic', true)
}
}
const musicFull = ref(false)
// 设置musicFull
const setMusicFull = () => {
musicFull.value = !musicFull.value
}
// 解析音乐
const parsingMusic = async () => {
const { data } = await getParsingMusicUrl(playMusic.value.id)
store.state.playMusicUrl = data.data.url
}
</script>
<style lang="scss" scoped>
.musicPage-enter-active {
animation: fadeInUp 0.4s ease-in-out;
}
.musicPage-leave-active {
animation: fadeOutDown 0.4s ease-in-out;
}
.text-ellipsis {
width: 100%;
}
.music-play-bar {
@apply h-20 w-full absolute bottom-0 left-0 flex items-center rounded-t-2xl overflow-hidden box-border px-6 py-2;
z-index: 99999999;
backdrop-filter: blur(20px);
background-color: rgba(0, 0, 0, 0.747);
.music-content {
width: 200px;
@apply ml-4;
&-title {
@apply text-base text-white;
}
&-name {
@apply text-xs mt-1;
@apply text-gray-400;
}
}
}
.play-bar-img {
@apply w-14 h-14 rounded-2xl;
}
.music-buttons {
@apply mx-6;
.iconfont {
@apply text-2xl hover:text-green-500 transition;
}
.icon {
@apply text-xl hover:text-white;
}
@apply flex items-center;
> div {
@apply cursor-pointer;
}
&-play {
@apply flex justify-center items-center w-12 h-12 rounded-full mx-4 hover:bg-green-500 transition;
background: #383838;
}
}
.music-time {
@apply flex flex-1 items-center;
.time {
@apply mx-4 mt-1;
}
}
.audio-volume {
width: 140px;
@apply flex items-center mx-4;
.iconfont {
@apply text-2xl hover:text-green-500 transition cursor-pointer mr-4;
}
}
.audio-button {
@apply flex items-center mx-4;
.iconfont {
@apply text-2xl hover:text-green-500 transition cursor-pointer m-4;
}
}
</style>

View File

@@ -1,166 +0,0 @@
<template>
<div class="search-box flex">
<div class="search-box-input flex-1">
<n-input
size="large"
round
v-model:value="searchValue"
:placeholder="hotSearchKeyword"
class="border border-gray-600"
@keydown.enter="search"
>
<template #prefix>
<i class="iconfont icon-search"></i>
</template>
</n-input>
</div>
<div class="user-box">
<n-dropdown trigger="hover" @select="selectItem" :options="options">
<i class="iconfont icon-xiasanjiaoxing"></i>
</n-dropdown>
<n-avatar
class="ml-2 cursor-pointer"
circle
size="large"
:src="store.state.user.avatarUrl"
v-if="store.state.user"
/>
<n-avatar
class="ml-2 cursor-pointer"
circle
size="large"
src="https://picsum.photos/200/300?random=1"
@click="toLogin()"
v-else
>登录</n-avatar>
</div>
</div>
</template>
<script lang="ts" setup>
import { getSearchKeyword, getHotSearch } from '@/api/home';
import { getUserDetail, logout } from '@/api/login';
import { onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import request from '@/utils/request_mt'
const router = useRouter()
const store = useStore();
// 推荐热搜词
const hotSearchKeyword = ref("搜索点什么吧...")
const hotSearchValue = ref("")
const loadHotSearchKeyword = async () => {
const { data } = await getSearchKeyword();
hotSearchKeyword.value = data.data.showKeyword
hotSearchValue.value = data.data.realkeyword
}
store.state.user = JSON.parse(localStorage.getItem('user') || '{}')
const loadPage = async () => {
const { data } = await getUserDetail()
store.state.user = data.profile
localStorage.setItem('user', JSON.stringify(data.profile))
}
const toLogin = () => {
router.push('/login')
}
// 页面初始化
onMounted(() => {
loadHotSearchKeyword()
loadPage()
})
// 搜索词
const searchValue = ref("")
const search = () => {
let value = searchValue.value
if (value == "") {
searchValue.value = hotSearchValue.value
} else {
router.push({
path: "/search",
query: {
keyword: value
}
})
}
}
const value = 'Drive My Car'
const options = [
{
label: '打卡',
key: 'card'
},
{
label: '听歌升级',
key: 'card_music'
},
{
label: '歌曲次数',
key: 'listen'
},
{
label: '登录',
key: 'login'
},
{
label: '退出登录',
key: 'logout'
}
]
const selectItem = async (key: any) => {
// switch 判断
switch (key) {
case 'card':
await request.get('/?do=sign')
.then(res => {
console.log(res)
})
break;
case 'card_music':
await request.get('/?do=daka')
.then(res => {
console.log(res)
})
break;
case 'listen':
await request.get('/?do=listen&id=1885175990&time=300')
.then(res => {
console.log(res)
})
break;
case 'logout':
logout().then(() => {
store.state.user = null
localStorage.clear()
})
break;
case 'login':
router.push("/login")
break;
}
}
</script>
<style lang="scss" scoped>
.user-box {
@apply ml-6 flex text-lg justify-center items-center rounded-full pl-3 border border-gray-600;
background: #1a1a1a;
}
.search-box-input {
@apply relative;
}
</style>

View File

@@ -1,5 +0,0 @@
import AppMenu from "./AppMenu.vue";
import PlayBar from "./PlayBar.vue";
import SearchBar from "./SearchBar.vue";
export { AppMenu, PlayBar, SearchBar };

View File

@@ -1,19 +0,0 @@
import { createApp } from "vue";
import App from "./App.vue";
import naive from "naive-ui";
import "vfonts/Lato.css";
import "vfonts/FiraCode.css";
// tailwind css
import "./index.css";
import router from "@/router";
import store from "@/store";
const app = createApp(App);
app.use(router);
app.use(store);
app.use(naive);
app.mount("#app");

111
src/main/index.ts Normal file
View File

@@ -0,0 +1,111 @@
import { electronApp, optimizer } from '@electron-toolkit/utils';
import { app, ipcMain, nativeImage } from 'electron';
import { join } from 'path';
import { loadLyricWindow } from './lyric';
import { initializeConfig } from './modules/config';
import { initializeFileManager } from './modules/fileManager';
import { initializeFonts } from './modules/fonts';
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
import { initializeTray } from './modules/tray';
import { createMainWindow, initializeWindowManager } from './modules/window';
import { startMusicApi } from './server';
// 导入所有图标
const iconPath = join(__dirname, '../../resources');
const icon = nativeImage.createFromPath(
process.platform === 'darwin'
? join(iconPath, 'icon.icns')
: process.platform === 'win32'
? join(iconPath, 'favicon.ico')
: join(iconPath, 'icon.png')
);
let mainWindow: Electron.BrowserWindow;
// 初始化应用
function initialize() {
// 初始化配置管理
initializeConfig();
// 初始化文件管理
initializeFileManager();
// 初始化窗口管理
initializeWindowManager();
// 初始化字体管理
initializeFonts();
// 创建主窗口
mainWindow = createMainWindow(icon);
// 初始化托盘
initializeTray(iconPath, mainWindow);
// 启动音乐API
startMusicApi();
// 加载歌词窗口
loadLyricWindow(ipcMain, mainWindow);
// 初始化快捷键
initializeShortcuts(mainWindow);
}
// 检查是否为第一个实例
const isSingleInstance = app.requestSingleInstanceLock();
if (!isSingleInstance) {
app.quit();
} else {
// 当第二个实例启动时,将焦点转移到第一个实例的窗口
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.show();
mainWindow.focus();
}
});
// 应用程序准备就绪时的处理
app.whenReady().then(() => {
// 设置应用ID
electronApp.setAppUserModelId('com.alger.music');
// 监听窗口创建事件
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window);
});
// 初始化应用
initialize();
// macOS 激活应用时的处理
app.on('activate', () => {
if (mainWindow === null) initialize();
});
});
// 监听快捷键更新
ipcMain.on('update-shortcuts', () => {
registerShortcuts(mainWindow);
});
// 所有窗口关闭时的处理
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// 重启应用
ipcMain.on('restart', () => {
app.relaunch();
app.exit(0);
});
// 获取系统架构信息
ipcMain.on('get-arch', (event) => {
event.returnValue = process.arch;
});
}

176
src/main/lyric.ts Normal file
View File

@@ -0,0 +1,176 @@
import { BrowserWindow, IpcMain, screen } from 'electron';
import Store from 'electron-store';
import path, { join } from 'path';
const store = new Store();
let lyricWindow: BrowserWindow | null = null;
const createWin = () => {
console.log('Creating lyric window');
// 获取保存的窗口位置
const windowBounds =
(store.get('lyricWindowBounds') as {
x?: number;
y?: number;
width?: number;
height?: number;
}) || {};
const { x, y, width, height } = windowBounds;
// 获取屏幕尺寸
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
// 验证保存的位置是否有效
const validPosition =
x !== undefined && y !== undefined && x >= 0 && y >= 0 && x < screenWidth && y < screenHeight;
lyricWindow = new BrowserWindow({
width: width || 800,
height: height || 200,
x: validPosition ? x : undefined,
y: validPosition ? y : undefined,
frame: false,
show: false,
transparent: true,
hasShadow: false,
alwaysOnTop: true,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true
}
});
// 监听窗口关闭事件
lyricWindow.on('closed', () => {
if (lyricWindow) {
lyricWindow.destroy();
lyricWindow = null;
}
});
return lyricWindow;
};
export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {
const showLyricWindow = () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
if (lyricWindow.isMinimized()) {
lyricWindow.restore();
}
lyricWindow.focus();
lyricWindow.show();
return true;
}
return false;
};
ipcMain.on('open-lyric', () => {
console.log('Received open-lyric request');
if (showLyricWindow()) {
return;
}
console.log('Creating new lyric window');
const win = createWin();
if (!win) {
console.error('Failed to create lyric window');
return;
}
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools({ mode: 'detach' });
win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);
} else {
const distPath = path.resolve(__dirname, '../renderer');
win.loadURL(`file://${distPath}/index.html#/lyric`);
}
win.setMinimumSize(600, 200);
win.setSkipTaskbar(true);
win.once('ready-to-show', () => {
console.log('Lyric window ready to show');
win.show();
});
});
ipcMain.on('send-lyric', (_, data) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
try {
lyricWindow.webContents.send('receive-lyric', data);
} catch (error) {
console.error('Error processing lyric data:', error);
}
}
});
ipcMain.on('top-lyric', (_, data) => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setAlwaysOnTop(data);
}
});
ipcMain.on('close-lyric', () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.webContents.send('lyric-window-close');
mainWin.webContents.send('lyric-control-back', 'close');
lyricWindow.destroy();
lyricWindow = null;
}
});
// 处理鼠标事件
ipcMain.on('mouseenter-lyric', () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(true);
}
});
ipcMain.on('mouseleave-lyric', () => {
if (lyricWindow && !lyricWindow.isDestroyed()) {
lyricWindow.setIgnoreMouseEvents(false);
}
});
// 处理拖动移动
ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
const [currentX, currentY] = lyricWindow.getPosition();
const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;
const [windowWidth, windowHeight] = lyricWindow.getSize();
// 计算新位置,确保窗口不会移出屏幕
const newX = Math.max(0, Math.min(currentX + deltaX, screenWidth - windowWidth));
const newY = Math.max(0, Math.min(currentY + deltaY, screenHeight - windowHeight));
lyricWindow.setPosition(newX, newY);
// 保存新位置
store.set('lyricWindowBounds', {
...lyricWindow.getBounds(),
x: newX,
y: newY
});
});
// 添加鼠标穿透事件处理
ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {
if (!lyricWindow || lyricWindow.isDestroyed()) return;
lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });
});
// 添加播放控制处理
ipcMain.on('control-back', (_, command) => {
console.log('command', command);
if (mainWin && !mainWin.isDestroyed()) {
console.log('Sending control-back command:', command);
mainWin.webContents.send('lyric-control-back', command);
}
});
};

89
src/main/modules/cache.ts Normal file
View File

@@ -0,0 +1,89 @@
import { ipcMain } from 'electron';
import Store from 'electron-store';
interface LyricData {
id: number;
data: any;
timestamp: number;
}
interface StoreSchema {
lyrics: Record<number, LyricData>;
}
class CacheManager {
private store: Store<StoreSchema>;
constructor() {
this.store = new Store<StoreSchema>({
name: 'lyrics',
defaults: {
lyrics: {}
}
});
}
async cacheLyric(id: number, data: any) {
try {
const lyrics = this.store.get('lyrics');
lyrics[id] = {
id,
data,
timestamp: Date.now()
};
this.store.set('lyrics', lyrics);
return true;
} catch (error) {
console.error('Error caching lyric:', error);
return false;
}
}
async getCachedLyric(id: number) {
try {
const lyrics = this.store.get('lyrics');
const result = lyrics[id];
if (!result) return undefined;
// 检查缓存是否过期24小时
if (Date.now() - result.timestamp > 24 * 60 * 60 * 1000) {
delete lyrics[id];
this.store.set('lyrics', lyrics);
return undefined;
}
return result.data;
} catch (error) {
console.error('Error getting cached lyric:', error);
return undefined;
}
}
async clearLyricCache() {
try {
this.store.set('lyrics', {});
return true;
} catch (error) {
console.error('Error clearing lyric cache:', error);
return false;
}
}
}
export const cacheManager = new CacheManager();
export function initializeCacheManager() {
// 添加歌词缓存相关的 IPC 处理
ipcMain.handle('cache-lyric', async (_, id: number, lyricData: any) => {
return await cacheManager.cacheLyric(id, lyricData);
});
ipcMain.handle('get-cached-lyric', async (_, id: number) => {
return await cacheManager.getCachedLyric(id);
});
ipcMain.handle('clear-lyric-cache', async () => {
return await cacheManager.clearLyricCache();
});
}

View File

@@ -0,0 +1,63 @@
import { app, ipcMain } from 'electron';
import Store from 'electron-store';
import set from '../set.json';
import { defaultShortcuts } from './shortcuts';
type SetConfig = {
isProxy: boolean;
proxyConfig: {
enable: boolean;
protocol: string;
host: string;
port: number;
};
enableRealIP: boolean;
realIP: string;
noAnimate: boolean;
animationSpeed: number;
author: string;
authorUrl: string;
musicApiPort: number;
closeAction: 'ask' | 'minimize' | 'close';
musicQuality: string;
fontFamily: string;
fontScope: 'global' | 'lyric';
};
interface StoreType {
set: SetConfig;
shortcuts: typeof defaultShortcuts;
}
let store: Store<StoreType>;
/**
* 初始化配置管理
*/
export function initializeConfig() {
store = new Store<StoreType>({
name: 'config',
defaults: {
set: set as SetConfig,
shortcuts: defaultShortcuts
}
});
store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads'));
// 定义ipcRenderer监听事件
ipcMain.on('set-store-value', (_, key, value) => {
store.set(key, value);
});
ipcMain.on('get-store-value', (_, key) => {
const value = store.get(key);
_.returnValue = value || '';
});
return store;
}
export function getStore() {
return store;
}

View File

@@ -0,0 +1,395 @@
import axios from 'axios';
import { app, dialog, ipcMain, protocol, shell } from 'electron';
import Store from 'electron-store';
import * as fs from 'fs';
import * as path from 'path';
const MAX_CONCURRENT_DOWNLOADS = 3;
const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = [];
let activeDownloads = 0;
// 创建一个store实例用于存储下载历史
const downloadStore = new Store({
name: 'downloads',
defaults: {
history: []
}
});
// 创建一个store实例用于存储音频缓存
const audioCacheStore = new Store({
name: 'audioCache',
defaults: {
cache: {}
}
});
/**
* 初始化文件管理相关的IPC监听
*/
export function initializeFileManager() {
// 注册本地文件协议
protocol.registerFileProtocol('local', (request, callback) => {
try {
const decodedUrl = decodeURIComponent(request.url);
const filePath = decodedUrl.replace('local://', '');
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
console.error('File not found:', filePath);
callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND
return;
}
callback({ path: filePath });
} catch (error) {
console.error('Error handling local protocol:', error);
callback({ error: -2 }); // net::FAILED
}
});
// 通用的选择目录处理
ipcMain.handle('select-directory', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
title: '选择目录'
});
return result;
});
// 通用的打开目录处理
ipcMain.on('open-directory', (_, filePath) => {
try {
if (fs.statSync(filePath).isDirectory()) {
shell.openPath(filePath);
} else {
shell.showItemInFolder(filePath);
}
} catch (error) {
console.error('Error opening path:', error);
}
});
// 下载音乐处理
ipcMain.on('download-music', handleDownloadRequest);
// 检查文件是否已下载
ipcMain.handle('check-music-downloaded', (_, filename: string) => {
const store = new Store();
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
const filePath = path.join(downloadPath, `${filename}.mp3`);
return fs.existsSync(filePath);
});
// 删除已下载的音乐
ipcMain.handle('delete-downloaded-music', async (_, filePath: string) => {
try {
if (fs.existsSync(filePath)) {
// 先删除文件
try {
await fs.promises.unlink(filePath);
} catch (error) {
console.error('Error deleting file:', error);
}
// 删除对应的歌曲信息
const store = new Store();
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
delete songInfos[filePath];
store.set('downloadedSongs', songInfos);
return true;
}
return false;
} catch (error) {
console.error('Error deleting file:', error);
return false;
}
});
// 获取已下载音乐列表
ipcMain.handle('get-downloaded-music', () => {
try {
const store = new Store();
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
// 过滤出实际存在的文件
const validSongs = Object.entries(songInfos)
.filter(([path]) => fs.existsSync(path))
.map(([_, info]) => info)
.sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0));
// 更新存储,移除不存在的文件记录
const newSongInfos = validSongs.reduce((acc, song) => {
acc[song.path] = song;
return acc;
}, {});
store.set('downloadedSongs', newSongInfos);
return validSongs;
} catch (error) {
console.error('Error getting downloaded music:', error);
return [];
}
});
// 检查歌曲是否已下载并返回本地路径
ipcMain.handle('check-song-downloaded', (_, songId: number) => {
const store = new Store();
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
// 通过ID查找已下载的歌曲
for (const [path, info] of Object.entries(songInfos)) {
if (info.id === songId && fs.existsSync(path)) {
return {
isDownloaded: true,
localPath: `local://${path}`,
songInfo: info
};
}
}
return {
isDownloaded: false,
localPath: '',
songInfo: null
};
});
// 添加清除下载历史的处理函数
ipcMain.on('clear-downloads-history', () => {
downloadStore.set('history', []);
});
// 添加清除音频缓存的处理函数
ipcMain.on('clear-audio-cache', () => {
audioCacheStore.set('cache', {});
// 清除临时音频文件目录
const tempDir = path.join(app.getPath('userData'), 'AudioCache');
if (fs.existsSync(tempDir)) {
try {
fs.readdirSync(tempDir).forEach((file) => {
const filePath = path.join(tempDir, file);
if (file.endsWith('.mp3') || file.endsWith('.m4a')) {
fs.unlinkSync(filePath);
}
});
} catch (error) {
console.error('清除音频缓存文件失败:', error);
}
}
});
}
/**
* 处理下载请求
*/
function handleDownloadRequest(
event: Electron.IpcMainEvent,
{
url,
filename,
songInfo,
type
}: { url: string; filename: string; songInfo?: any; type?: string }
) {
// 检查是否已经在队列中或正在下载
if (downloadQueue.some((item) => item.filename === filename)) {
event.reply('music-download-error', {
filename,
error: '该歌曲已在下载队列中'
});
return;
}
// 检查是否已下载
const store = new Store();
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
// 检查是否已下载通过ID
const isDownloaded =
songInfo?.id && Object.values(songInfos).some((info: any) => info.id === songInfo.id);
if (isDownloaded) {
event.reply('music-download-error', {
filename,
error: '该歌曲已下载'
});
return;
}
// 添加到下载队列
downloadQueue.push({ url, filename, songInfo, type });
event.reply('music-download-queued', {
filename,
songInfo
});
// 尝试开始下载
processDownloadQueue(event);
}
/**
* 处理下载队列
*/
async function processDownloadQueue(event: Electron.IpcMainEvent) {
if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) {
return;
}
const { url, filename, songInfo, type } = downloadQueue.shift()!;
activeDownloads++;
try {
await downloadMusic(event, { url, filename, songInfo, type });
} finally {
activeDownloads--;
processDownloadQueue(event);
}
}
/**
* 下载音乐功能
*/
async function downloadMusic(
event: Electron.IpcMainEvent,
{
url,
filename,
songInfo,
type = 'mp3'
}: { url: string; filename: string; songInfo: any; type?: string }
) {
let finalFilePath = '';
let writer: fs.WriteStream | null = null;
try {
const store = new Store();
const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');
// 从URL中获取文件扩展名如果没有则使用传入的type或默认mp3
const urlExt = type ? `.${type}` : '.mp3';
const filePath = path.join(downloadPath, `${filename}${urlExt}`);
// 检查文件是否已存在,如果存在则添加序号
finalFilePath = filePath;
let counter = 1;
while (fs.existsSync(finalFilePath)) {
const ext = path.extname(filePath);
const nameWithoutExt = filePath.slice(0, -ext.length);
finalFilePath = `${nameWithoutExt} (${counter})${ext}`;
counter++;
}
// 先获取文件大小
const headResponse = await axios.head(url);
const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10);
// 开始下载
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
timeout: 30000 // 30秒超时
});
writer = fs.createWriteStream(finalFilePath);
let downloadedSize = 0;
// 使用 data 事件来跟踪下载进度
response.data.on('data', (chunk: Buffer) => {
downloadedSize += chunk.length;
const progress = Math.round((downloadedSize / totalSize) * 100);
event.reply('music-download-progress', {
filename,
progress,
loaded: downloadedSize,
total: totalSize,
path: finalFilePath,
status: progress === 100 ? 'completed' : 'downloading',
songInfo: songInfo || {
name: filename,
ar: [{ name: '本地音乐' }],
picUrl: '/images/default_cover.png'
}
});
});
// 等待下载完成
await new Promise((resolve, reject) => {
writer!.on('finish', () => resolve(undefined));
writer!.on('error', (error) => reject(error));
response.data.pipe(writer!);
});
// 验证文件是否完整下载
const stats = fs.statSync(finalFilePath);
if (stats.size !== totalSize) {
throw new Error('文件下载不完整');
}
// 保存下载信息
try {
const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;
const defaultInfo = {
name: filename,
ar: [{ name: '本地音乐' }],
picUrl: '/images/default_cover.png'
};
const newSongInfo = {
id: songInfo?.id || 0,
name: songInfo?.name || filename,
filename,
picUrl: songInfo?.picUrl || defaultInfo.picUrl,
ar: songInfo?.ar || defaultInfo.ar,
size: totalSize,
path: finalFilePath,
downloadTime: Date.now(),
al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl },
type: type || 'mp3'
};
// 保存到下载记录
songInfos[finalFilePath] = newSongInfo;
store.set('downloadedSongs', songInfos);
// 添加到下载历史
const history = downloadStore.get('history', []) as any[];
history.unshift(newSongInfo);
downloadStore.set('history', history);
// 发送下载完成事件
event.reply('music-download-complete', {
success: true,
path: finalFilePath,
filename,
size: totalSize,
songInfo: newSongInfo
});
} catch (error) {
console.error('Error saving download info:', error);
throw new Error('保存下载信息失败');
}
} catch (error: any) {
console.error('Download error:', error);
// 清理未完成的下载
if (writer) {
writer.end();
}
if (finalFilePath && fs.existsSync(finalFilePath)) {
try {
fs.unlinkSync(finalFilePath);
} catch (e) {
console.error('Failed to delete incomplete download:', e);
}
}
event.reply('music-download-complete', {
success: false,
error: error.message || '下载失败',
filename
});
}
}

42
src/main/modules/fonts.ts Normal file
View File

@@ -0,0 +1,42 @@
import { ipcMain } from 'electron';
import { getFonts } from 'font-list';
/**
* 清理字体名称
* @param fontName 原始字体名称
* @returns 清理后的字体名称
*/
function cleanFontName(fontName: string): string {
return fontName
.trim()
.replace(/^["']|["']$/g, '') // 移除首尾的引号
.replace(/\s+/g, ' '); // 将多个空格替换为单个空格
}
/**
* 获取系统字体列表
*/
async function getSystemFonts(): Promise<string[]> {
try {
// 使用 font-list 获取系统字体
const fonts = await getFonts();
// 清理字体名称并去重
const cleanedFonts = [...new Set(fonts.map(cleanFontName))];
// 添加系统默认字体并排序
return ['system-ui', ...cleanedFonts].sort();
} catch (error) {
console.error('获取系统字体失败:', error);
// 如果获取失败,至少返回系统默认字体
return ['system-ui'];
}
}
/**
* 初始化字体管理模块
*/
export function initializeFonts() {
// 添加获取系统字体的 IPC 处理
ipcMain.handle('get-system-fonts', async () => {
return await getSystemFonts();
});
}

View File

@@ -0,0 +1,88 @@
import { globalShortcut, ipcMain } from 'electron';
import { getStore } from './config';
// 添加获取平台信息的 IPC 处理程序
ipcMain.on('get-platform', (event) => {
event.returnValue = process.platform;
});
// 定义默认快捷键
export const defaultShortcuts = {
togglePlay: 'CommandOrControl+Alt+P',
prevPlay: 'CommandOrControl+Alt+Left',
nextPlay: 'CommandOrControl+Alt+Right',
volumeUp: 'CommandOrControl+Alt+Up',
volumeDown: 'CommandOrControl+Alt+Down',
toggleFavorite: 'CommandOrControl+Alt+L',
toggleWindow: 'CommandOrControl+Alt+Shift+M'
};
let mainWindowRef: Electron.BrowserWindow | null = null;
// 注册快捷键
export function registerShortcuts(mainWindow: Electron.BrowserWindow) {
mainWindowRef = mainWindow;
const store = getStore();
const shortcuts = store.get('shortcuts');
// 注销所有已注册的快捷键
globalShortcut.unregisterAll();
// 显示/隐藏主窗口
globalShortcut.register(shortcuts.toggleWindow, () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
// 播放/暂停
globalShortcut.register(shortcuts.togglePlay, () => {
mainWindow.webContents.send('global-shortcut', 'togglePlay');
});
// 上一首
globalShortcut.register(shortcuts.prevPlay, () => {
mainWindow.webContents.send('global-shortcut', 'prevPlay');
});
// 下一首
globalShortcut.register(shortcuts.nextPlay, () => {
mainWindow.webContents.send('global-shortcut', 'nextPlay');
});
// 音量增加
globalShortcut.register(shortcuts.volumeUp, () => {
mainWindow.webContents.send('global-shortcut', 'volumeUp');
});
// 音量减少
globalShortcut.register(shortcuts.volumeDown, () => {
mainWindow.webContents.send('global-shortcut', 'volumeDown');
});
// 收藏当前歌曲
globalShortcut.register(shortcuts.toggleFavorite, () => {
mainWindow.webContents.send('global-shortcut', 'toggleFavorite');
});
}
// 初始化快捷键
export function initializeShortcuts(mainWindow: Electron.BrowserWindow) {
mainWindowRef = mainWindow;
registerShortcuts(mainWindow);
// 监听禁用快捷键事件
ipcMain.on('disable-shortcuts', () => {
globalShortcut.unregisterAll();
});
// 监听启用快捷键事件
ipcMain.on('enable-shortcuts', () => {
if (mainWindowRef) {
registerShortcuts(mainWindowRef);
}
});
}

45
src/main/modules/tray.ts Normal file
View File

@@ -0,0 +1,45 @@
import { app, BrowserWindow, Menu, nativeImage, Tray } from 'electron';
import { join } from 'path';
let tray: Tray | null = null;
/**
* 初始化系统托盘
*/
export function initializeTray(iconPath: string, mainWindow: BrowserWindow) {
const trayIcon = nativeImage
.createFromPath(join(iconPath, 'icon_16x16.png'))
.resize({ width: 16, height: 16 });
tray = new Tray(trayIcon);
// 创建一个上下文菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '显示',
click: () => {
mainWindow.show();
}
},
{
label: '退出',
click: () => {
mainWindow.destroy();
app.quit();
}
}
]);
// 设置系统托盘图标的上下文菜单
tray.setContextMenu(contextMenu);
// 当系统托盘图标被点击时,切换窗口的显示/隐藏
tray.on('click', () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
return tray;
}

View File

@@ -0,0 +1,90 @@
import axios from 'axios';
import { exec } from 'child_process';
import { app, BrowserWindow, ipcMain } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
export function setupUpdateHandlers(_mainWindow: BrowserWindow) {
ipcMain.on('start-download', async (event, url: string) => {
try {
const response = await axios({
url,
method: 'GET',
responseType: 'stream',
onDownloadProgress: (progressEvent: { loaded: number; total?: number }) => {
if (!progressEvent.total) return;
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
const downloaded = (progressEvent.loaded / 1024 / 1024).toFixed(2);
const total = (progressEvent.total / 1024 / 1024).toFixed(2);
event.sender.send('download-progress', percent, `已下载 ${downloaded}MB / ${total}MB`);
}
});
const fileName = url.split('/').pop() || 'update.exe';
const downloadPath = path.join(app.getPath('downloads'), fileName);
// 创建写入流
const writer = fs.createWriteStream(downloadPath);
// 将响应流写入文件
response.data.pipe(writer);
// 处理写入完成
writer.on('finish', () => {
event.sender.send('download-complete', true, downloadPath);
});
// 处理写入错误
writer.on('error', (error) => {
console.error('Write file error:', error);
event.sender.send('download-complete', false, '');
});
} catch (error) {
console.error('Download failed:', error);
event.sender.send('download-complete', false, '');
}
});
ipcMain.on('install-update', (_event, filePath: string) => {
if (!fs.existsSync(filePath)) {
console.error('Installation file not found:', filePath);
return;
}
const { platform } = process;
// 关闭当前应用
app.quit();
// 根据不同平台执行安装
if (platform === 'win32') {
exec(`"${filePath}"`, (error) => {
if (error) {
console.error('Error starting installer:', error);
}
});
} else if (platform === 'darwin') {
// 挂载 DMG 文件
exec(`open "${filePath}"`, (error) => {
if (error) {
console.error('Error opening DMG:', error);
}
});
} else if (platform === 'linux') {
const ext = path.extname(filePath);
if (ext === '.AppImage') {
exec(`chmod +x "${filePath}" && "${filePath}"`, (error) => {
if (error) {
console.error('Error running AppImage:', error);
}
});
} else if (ext === '.deb') {
exec(`pkexec dpkg -i "${filePath}"`, (error) => {
if (error) {
console.error('Error installing deb package:', error);
}
});
}
}
});
}

119
src/main/modules/window.ts Normal file
View File

@@ -0,0 +1,119 @@
import { is } from '@electron-toolkit/utils';
import { app, BrowserWindow, ipcMain, session, shell } from 'electron';
import Store from 'electron-store';
import { join } from 'path';
const store = new Store();
/**
* 初始化代理设置
*/
function initializeProxy() {
const defaultConfig = {
enable: false,
protocol: 'http',
host: '127.0.0.1',
port: 7890
};
const proxyConfig = store.get('set.proxyConfig', defaultConfig) as {
enable: boolean;
protocol: string;
host: string;
port: number;
};
if (proxyConfig?.enable) {
const proxyRules = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`;
session.defaultSession.setProxy({ proxyRules });
} else {
session.defaultSession.setProxy({ proxyRules: '' });
}
}
/**
* 初始化窗口管理相关的IPC监听
*/
export function initializeWindowManager() {
// 初始化代理设置
initializeProxy();
ipcMain.on('minimize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.minimize();
}
});
ipcMain.on('maximize-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
if (win.isMaximized()) {
win.unmaximize();
} else {
win.maximize();
}
}
});
ipcMain.on('close-window', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.destroy();
app.quit();
}
});
ipcMain.on('mini-tray', (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
if (win) {
win.hide();
}
});
// 监听代理设置变化
store.onDidChange('set.proxyConfig', () => {
initializeProxy();
});
}
/**
* 创建主窗口
*/
export function createMainWindow(icon: Electron.NativeImage): BrowserWindow {
const mainWindow = new BrowserWindow({
width: 1200,
height: 780,
show: false,
frame: false,
autoHideMenuBar: true,
icon,
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true
}
});
mainWindow.setMinimumSize(1200, 780);
mainWindow.on('ready-to-show', () => {
mainWindow.show();
});
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: 'deny' };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
mainWindow.webContents.openDevTools({ mode: 'detach' });
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
return mainWindow;
}

30
src/main/server.ts Normal file
View File

@@ -0,0 +1,30 @@
import { ipcMain } from 'electron';
import Store from 'electron-store';
import fs from 'fs';
import server from 'netease-cloud-music-api-alger/server';
import os from 'os';
import path from 'path';
import { unblockMusic } from './unblockMusic';
const store = new Store();
if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {
fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');
}
// 处理解锁音乐请求
ipcMain.handle('unblock-music', async (_, id, data) => {
return unblockMusic(id, data);
});
async function startMusicApi(): Promise<void> {
console.log('MUSIC API STARTED');
const port = (store.get('set') as any).musicApiPort || 30488;
await server.serveNcmApi({
port
});
}
export { startMusicApi };

20
src/main/set.json Normal file
View File

@@ -0,0 +1,20 @@
{
"isProxy": false,
"proxyConfig": {
"enable": false,
"protocol": "http",
"host": "127.0.0.1",
"port": 7890
},
"enableRealIP": false,
"realIP": "",
"noAnimate": false,
"animationSpeed": 1,
"author": "Alger",
"authorUrl": "https://github.com/algerkong",
"musicApiPort": 30488,
"closeAction": "ask",
"musicQuality": "higher",
"fontFamily": "system-ui",
"fontScope": "global"
}

75
src/main/unblockMusic.ts Normal file
View File

@@ -0,0 +1,75 @@
import match from '@unblockneteasemusic/server';
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'youtube';
interface SongData {
name: string;
artists: Array<{ name: string }>;
album?: { name: string };
}
interface ResponseData {
url: string;
br: number;
size: number;
md5?: string;
platform?: Platform;
gain?: number;
}
interface UnblockResult {
data: {
data: ResponseData;
params: {
id: number;
type: 'song';
};
};
}
/**
* 音乐解析函数
* @param id 歌曲ID
* @param songData 歌曲信息
* @param retryCount 重试次数
* @returns Promise<UnblockResult>
*/
const unblockMusic = async (
id: number | string,
songData: SongData,
retryCount = 3
): Promise<UnblockResult> => {
// 所有可用平台
const platforms: Platform[] = ['migu', 'kugou', 'pyncmd', 'joox', 'kuwo', 'bilibili', 'youtube'];
const retry = async (attempt: number): Promise<UnblockResult> => {
try {
const data = await match(parseInt(String(id), 10), platforms, songData);
const result: UnblockResult = {
data: {
data,
params: {
id: parseInt(String(id), 10),
type: 'song'
}
}
};
return result;
} catch (err) {
if (attempt < retryCount) {
// 延迟重试,每次重试增加延迟时间
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
return retry(attempt + 1);
}
// 所有重试都失败后,抛出详细错误
throw new Error(
`音乐解析失败 (ID: ${id}): ${err instanceof Error ? err.message : '未知错误'}`
);
}
};
return retry(1);
};
export { type Platform, type ResponseData, type SongData, unblockMusic, type UnblockResult };

20
src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,20 @@
import { ElectronAPI } from '@electron-toolkit/preload';
declare global {
interface Window {
electron: ElectronAPI;
api: {
sendLyric: (data: string) => void;
openLyric: () => void;
minimize: () => void;
maximize: () => void;
close: () => void;
dragStart: (data: string) => void;
miniTray: () => void;
restart: () => void;
unblockMusic: (id: number, data: any) => Promise<any>;
invoke: (channel: string, ...args: any[]) => Promise<any>;
};
$message: any;
}
}

47
src/preload/index.ts Normal file
View File

@@ -0,0 +1,47 @@
import { electronAPI } from '@electron-toolkit/preload';
import { contextBridge, ipcRenderer } from 'electron';
// Custom APIs for renderer
const api = {
minimize: () => ipcRenderer.send('minimize-window'),
maximize: () => ipcRenderer.send('maximize-window'),
close: () => ipcRenderer.send('close-window'),
dragStart: (data) => ipcRenderer.send('drag-start', data),
miniTray: () => ipcRenderer.send('mini-tray'),
restart: () => ipcRenderer.send('restart'),
openLyric: () => ipcRenderer.send('open-lyric'),
sendLyric: (data) => ipcRenderer.send('send-lyric', data),
unblockMusic: (id) => ipcRenderer.invoke('unblock-music', id),
// 歌词缓存相关
invoke: (channel: string, ...args: any[]) => {
const validChannels = [
'get-lyrics',
'clear-lyrics-cache',
'get-system-fonts',
'get-cached-lyric',
'cache-lyric',
'clear-lyric-cache'
];
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, ...args);
}
return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`));
}
};
// 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);
contextBridge.exposeInMainWorld('api', api);
} catch (error) {
console.error(error);
}
} else {
// @ts-ignore (define in dts)
window.electron = electronAPI;
// @ts-ignore (define in dts)
window.api = api;
}

90
src/renderer/App.vue Normal file
View File

@@ -0,0 +1,90 @@
<template>
<div class="app-container" :class="{ mobile: isMobile, noElectron: !isElectron }">
<n-config-provider :theme="theme === 'dark' ? darkTheme : lightTheme">
<n-dialog-provider>
<n-message-provider>
<router-view></router-view>
</n-message-provider>
</n-dialog-provider>
</n-config-provider>
</div>
</template>
<script setup lang="ts">
import { darkTheme, lightTheme } from 'naive-ui';
import { computed, onMounted, watch } from 'vue';
import homeRouter from '@/router/home';
import store from '@/store';
import { isElectron } from '@/utils';
import { isMobile } from './utils';
const theme = computed(() => {
return store.state.theme;
});
// 监听字体变化并应用
watch(
() => [store.state.setData.fontFamily, store.state.setData.fontScope],
([newFont, fontScope]) => {
const appElement = document.body;
if (!appElement) return;
const defaultFonts =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
// 只有在全局模式下才应用字体
if (fontScope !== 'global') {
appElement.style.fontFamily = defaultFonts;
return;
}
if (newFont === 'system-ui') {
appElement.style.fontFamily = defaultFonts;
} else {
// 处理多个字体,确保每个字体名都被正确引用
const fontList = newFont.split(',').map((font) => {
const trimmedFont = font.trim();
// 如果字体名包含空格或特殊字符,添加引号(如果还没有引号的话)
return /[\s'"()]/.test(trimmedFont) && !/^['"].*['"]$/.test(trimmedFont)
? `"${trimmedFont}"`
: trimmedFont;
});
// 将选择的字体和默认字体组合
appElement.style.fontFamily = `${fontList.join(', ')}, ${defaultFonts}`;
}
},
{ immediate: true }
);
onMounted(() => {
store.dispatch('initializeSettings');
store.dispatch('initializeTheme');
store.dispatch('initializeSystemFonts');
if (isMobile.value) {
store.commit(
'setMenus',
homeRouter.filter((item) => item.meta.isMobile)
);
}
});
</script>
<style lang="scss">
.app-container {
@apply h-full w-full;
user-select: none;
}
.mobile {
.text-base {
font-size: 14px !important;
}
}
.html:has(.mobile) {
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,21 @@
import request from '@/utils/request';
// 获取歌手详情
export const getArtistDetail = (id) => {
return request.get('/artist/detail', { params: { id } });
};
// 获取歌手热门歌曲
export const getArtistTopSongs = (params) => {
return request.get('/artist/songs', {
params: {
...params,
order: 'hot'
}
});
};
// 获取歌手专辑
export const getArtistAlbums = (params) => {
return request.get('/artist/album', { params });
};

52
src/renderer/api/home.ts Normal file
View File

@@ -0,0 +1,52 @@
import { IData } from '@/type';
import { IAlbumNew } from '@/type/album';
import { IDayRecommend } from '@/type/day_recommend';
import { IRecommendMusic } from '@/type/music';
import { IPlayListSort } from '@/type/playlist';
import { IHotSearch, ISearchKeyword } from '@/type/search';
import { IHotSinger } from '@/type/singer';
import request from '@/utils/request';
interface IHotSingerParams {
offset: number;
limit: number;
}
interface IRecommendMusicParams {
limit: number;
}
// 获取热门歌手
export const getHotSinger = (params: IHotSingerParams) => {
return request.get<IHotSinger>('/top/artists', { params });
};
// 获取搜索推荐词
export const getSearchKeyword = () => {
return request.get<ISearchKeyword>('/search/default');
};
// 获取热门搜索
export const getHotSearch = () => {
return request.get<IHotSearch>('/search/hot/detail');
};
// 获取歌单分类
export const getPlaylistCategory = () => {
return request.get<IPlayListSort>('/playlist/catlist');
};
// 获取推荐音乐
export const getRecommendMusic = (params: IRecommendMusicParams) => {
return request.get<IRecommendMusic>('/personalized/newsong', { params });
};
// 获取每日推荐
export const getDayRecommend = () => {
return request.get<IData<IData<IDayRecommend>>>('/recommend/songs');
};
// 获取最新专辑推荐
export const getNewAlbum = () => {
return request.get<IAlbumNew>('/album/newest');
};

View File

@@ -1,6 +1,6 @@
import request from "@/utils/request";
import { IList } from "@/type/list";
import type { IListDetail } from "@/type/listDetail";
import { IList } from '@/type/list';
import type { IListDetail } from '@/type/listDetail';
import request from '@/utils/request';
interface IListByTagParams {
tag: string;
@@ -16,22 +16,27 @@ interface IListByCatParams {
// 根据tag 获取歌单列表
export function getListByTag(params: IListByTagParams) {
return request.get<IList>("/top/playlist/highquality", { params: params });
return request.get<IList>('/top/playlist/highquality', { params });
}
// 根据cat 获取歌单列表
export function getListByCat(params: IListByCatParams) {
return request.get("/top/playlist", {
params: params,
return request.get('/top/playlist', {
params
});
}
// 获取推荐歌单
export function getRecommendList(limit: number = 30) {
return request.get("/personalized", { params: { limit } });
return request.get('/personalized', { params: { limit } });
}
// 获取歌单详情
export function getListDetail(id: number | string) {
return request.get<IListDetail>("/playlist/detail", { params: { id } });
return request.get<IListDetail>('/playlist/detail', { params: { id } });
}
// 获取专辑内容
export function getAlbum(id: number | string) {
return request.get('/album', { params: { id } });
}

View File

@@ -1,46 +1,46 @@
import request from "@/utils/request";
import request from '@/utils/request';
// 创建二维码key
// /login/qr/key
export function getQrKey() {
return request.get("/login/qr/key");
return request.get('/login/qr/key');
}
// 创建二维码
// /login/qr/create
export function createQr(key: any) {
return request.get("/login/qr/create", { params: { key: key, qrimg: true } });
return request.get('/login/qr/create', { params: { key, qrimg: true } });
}
// 获取二维码状态
// /login/qr/check
export function checkQr(key: any) {
return request.get("/login/qr/check", { params: { key: key } });
return request.get('/login/qr/check', { params: { key } });
}
// 获取登录状态
// /login/status
export function getLoginStatus() {
return request.get("/login/status");
return request.get('/login/status');
}
// 获取用户信息
// /user/account
export function getUserDetail() {
return request.get("/user/account");
return request.get('/user/account');
}
// 退出登录
// /logout
export function logout() {
return request.get("/logout");
return request.get('/logout');
}
// 手机号登录
// /login/cellphone
export function loginByCellphone(phone: any, password: any) {
return request.post("/login/cellphone", {
phone: phone,
password: password,
export function loginByCellphone(phone: string, password: string) {
return request.post('/login/cellphone', {
phone,
password
});
}

85
src/renderer/api/music.ts Normal file
View File

@@ -0,0 +1,85 @@
import { musicDB } from '@/hooks/MusicHook';
import store from '@/store';
import type { ILyric } from '@/type/lyric';
import { isElectron } from '@/utils';
import request from '@/utils/request';
import requestMusic from '@/utils/request_music';
const { addData, getData, deleteData } = musicDB;
// 获取音乐音质详情
export const getMusicQualityDetail = (id: number) => {
return request.get('/song/music/detail', { params: { id } });
};
// 根据音乐Id获取音乐播放URl
export const getMusicUrl = async (id: number) => {
const res = await request.get('/song/download/url/v1', {
params: {
id,
level: store.state.setData.musicQuality || 'higher'
}
});
if (res.data.data.url) {
return { data: { data: [{ ...res.data.data }] } };
}
return await request.get('/song/url/v1', {
params: {
id,
level: store.state.setData.musicQuality || 'higher'
}
});
};
// 获取歌曲详情
export const getMusicDetail = (ids: Array<number>) => {
return request.get('/song/detail', { params: { ids: ids.join(',') } });
};
// 根据音乐Id获取音乐歌词
export const getMusicLrc = async (id: number) => {
const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000; // 10天的毫秒数
try {
// 尝试获取缓存的歌词
const cachedLyric = await getData('music_lyric', id);
if (cachedLyric?.createTime && Date.now() - cachedLyric.createTime < TEN_DAYS_MS) {
return { ...cachedLyric };
}
// 获取新的歌词数据
const res = await request.get<ILyric>('/lyric', { params: { id } });
// 只有在成功获取新数据后才删除旧缓存并添加新缓存
if (res?.data) {
if (cachedLyric) {
await deleteData('music_lyric', id);
}
addData('music_lyric', { id, data: res.data, createTime: Date.now() });
}
return res;
} catch (error) {
console.error('获取歌词失败:', error);
throw error; // 向上抛出错误,让调用者处理
}
};
export const getParsingMusicUrl = (id: number, data: any) => {
if (isElectron) {
return window.api.unblockMusic(id, data);
}
return requestMusic.get<any>('/music', { params: { id } });
};
// 收藏歌曲
export const likeSong = (id: number, like: boolean = true) => {
return request.get('/like', { params: { id, like } });
};
// 获取用户喜欢的音乐列表
export const getLikedList = () => {
return request.get('/likelist');
};

45
src/renderer/api/mv.ts Normal file
View File

@@ -0,0 +1,45 @@
import { IData } from '@/type';
import { IMvUrlData } from '@/type/mv';
import request from '@/utils/request';
interface MvParams {
limit?: number;
offset?: number;
area?: string;
}
// 获取 mv 排行
export const getTopMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params
});
};
// 获取所有mv
export const getAllMv = (params: MvParams) => {
return request({
url: '/mv/all',
method: 'get',
params
});
};
// 获取 mv 数据
export const getMvDetail = (mvid: string) => {
return request.get('/mv/detail', {
params: {
mvid
}
});
};
// 获取 mv 地址
export const getMvUrl = (id: Number) => {
return request.get<IData<IMvUrlData>>('/mv/url', {
params: {
id
}
});
};

View File

@@ -0,0 +1,14 @@
import request from '@/utils/request';
interface IParams {
keywords: string;
type: number;
limit?: number;
offset?: number;
}
// 搜索内容
export const getSearch = (params: IParams) => {
return request.get<any>('/cloudsearch', {
params
});
};

View File

@@ -1,17 +1,17 @@
import request from "@/utils/request";
import request from '@/utils/request';
// /user/detail
export function getUserDetail(uid: number) {
return request.get("/user/detail", { params: { uid } });
return request.get('/user/detail', { params: { uid } });
}
// /user/playlist
export function getUserPlaylist(uid: number) {
return request.get("/user/playlist", { params: { uid } });
return request.get('/user/playlist', { params: { uid } });
}
// 播放历史
// /user/record?uid=32953014&type=1
export function getUserRecord(uid: number, type: number = 0) {
return request.get("/user/record", { params: { uid, type } });
return request.get('/user/record', { params: { uid, type } });
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,15 @@
body {
/* background-color: #000; */
}
.n-popover:has(.music-play) {
border-radius: 1.5rem !important;
}
.n-popover {
border-radius: 0.5rem !important;
overflow: hidden !important;
}
.n-popover:has(.transparent-popover ) {
background-color: transparent !important;
padding: 0 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1,283 @@
@font-face {
font-family: 'iconfont'; /* Project id 2685283 */
src:
url('iconfont.woff2?t=1703643214551') format('woff2'),
url('iconfont.woff?t=1703643214551') format('woff'),
url('iconfont.ttf?t=1703643214551') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-list:before {
content: '\e603';
}
.icon-maxsize:before {
content: '\e692';
}
.icon-close:before {
content: '\e616';
}
.icon-minisize:before {
content: '\e602';
}
.icon-shuaxin:before {
content: '\e627';
}
.icon-icon_error:before {
content: '\e615';
}
.icon-a-3User:before {
content: '\e601';
}
.icon-Chat:before {
content: '\e605';
}
.icon-Category:before {
content: '\e606';
}
.icon-Document:before {
content: '\e607';
}
.icon-Heart:before {
content: '\e608';
}
.icon-Hide:before {
content: '\e609';
}
.icon-Home:before {
content: '\e60a';
}
.icon-a-Image2:before {
content: '\e60b';
}
.icon-Profile:before {
content: '\e60c';
}
.icon-Search:before {
content: '\e60d';
}
.icon-Paper:before {
content: '\e60e';
}
.icon-Play:before {
content: '\e60f';
}
.icon-Setting:before {
content: '\e610';
}
.icon-a-TicketStar:before {
content: '\e611';
}
.icon-a-VolumeOff:before {
content: '\e612';
}
.icon-a-VolumeUp:before {
content: '\e613';
}
.icon-a-VolumeDown:before {
content: '\e614';
}
.icon-stop:before {
content: '\e600';
}
.icon-next:before {
content: '\e6a9';
}
.icon-prev:before {
content: '\e6ac';
}
.icon-play:before {
content: '\e6aa';
}
.icon-xiasanjiaoxing:before {
content: '\e642';
}
.icon-videofill:before {
content: '\e7c7';
}
.icon-favorfill:before {
content: '\e64b';
}
.icon-favor:before {
content: '\e64c';
}
.icon-loading:before {
content: '\e64f';
}
.icon-search:before {
content: '\e65c';
}
.icon-likefill:before {
content: '\e668';
}
.icon-like:before {
content: '\e669';
}
.icon-notificationfill:before {
content: '\e66a';
}
.icon-notification:before {
content: '\e66b';
}
.icon-evaluate:before {
content: '\e672';
}
.icon-homefill:before {
content: '\e6bb';
}
.icon-link:before {
content: '\e6bf';
}
.icon-roundaddfill:before {
content: '\e6d8';
}
.icon-roundadd:before {
content: '\e6d9';
}
.icon-add:before {
content: '\e6da';
}
.icon-appreciatefill:before {
content: '\e6e3';
}
.icon-forwardfill:before {
content: '\e6ea';
}
.icon-voicefill:before {
content: '\e6f0';
}
.icon-wefill:before {
content: '\e6f4';
}
.icon-keyboard:before {
content: '\e71b';
}
.icon-picfill:before {
content: '\e72c';
}
.icon-markfill:before {
content: '\e730';
}
.icon-presentfill:before {
content: '\e732';
}
.icon-peoplefill:before {
content: '\e735';
}
.icon-read:before {
content: '\e742';
}
.icon-backwardfill:before {
content: '\e74d';
}
.icon-playfill:before {
content: '\e74f';
}
.icon-all:before {
content: '\e755';
}
.icon-hotfill:before {
content: '\e757';
}
.icon-recordfill:before {
content: '\e7a4';
}
.icon-full:before {
content: '\e7bc';
}
.icon-favor_fill_light:before {
content: '\e7ec';
}
.icon-round_favor_fill:before {
content: '\e80a';
}
.icon-round_location_fill:before {
content: '\e80b';
}
.icon-round_like_fill:before {
content: '\e80c';
}
.icon-round_people_fill:before {
content: '\e80d';
}
.icon-round_skin_fill:before {
content: '\e80e';
}
.icon-broadcast_fill:before {
content: '\e81d';
}
.icon-card_fill:before {
content: '\e81f';
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,478 @@
{
"id": "2685283",
"name": "music",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "1111849",
"name": "list",
"font_class": "list",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "1306794",
"name": "maxsize",
"font_class": "maxsize",
"unicode": "e692",
"unicode_decimal": 59026
},
{
"icon_id": "4437591",
"name": "close",
"font_class": "close",
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "5383753",
"name": "minisize",
"font_class": "minisize",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "13075017",
"name": "刷新",
"font_class": "shuaxin",
"unicode": "e627",
"unicode_decimal": 58919
},
{
"icon_id": "24457556",
"name": "icon_error",
"font_class": "icon_error",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "24492642",
"name": "3 User",
"font_class": "a-3User",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "24492643",
"name": "Chat",
"font_class": "Chat",
"unicode": "e605",
"unicode_decimal": 58885
},
{
"icon_id": "24492646",
"name": "Category",
"font_class": "Category",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "24492661",
"name": "Document",
"font_class": "Document",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "24492662",
"name": "Heart",
"font_class": "Heart",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "24492665",
"name": "Hide",
"font_class": "Hide",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "24492667",
"name": "Home",
"font_class": "Home",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "24492678",
"name": "Image 2",
"font_class": "a-Image2",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "24492684",
"name": "Profile",
"font_class": "Profile",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "24492685",
"name": "Search",
"font_class": "Search",
"unicode": "e60d",
"unicode_decimal": 58893
},
{
"icon_id": "24492687",
"name": "Paper",
"font_class": "Paper",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "24492690",
"name": "Play",
"font_class": "Play",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "24492698",
"name": "Setting",
"font_class": "Setting",
"unicode": "e610",
"unicode_decimal": 58896
},
{
"icon_id": "24492708",
"name": "Ticket Star",
"font_class": "a-TicketStar",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "24492712",
"name": "Volume Off",
"font_class": "a-VolumeOff",
"unicode": "e612",
"unicode_decimal": 58898
},
{
"icon_id": "24492713",
"name": "Volume Up",
"font_class": "a-VolumeUp",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "24492714",
"name": "Volume Down",
"font_class": "a-VolumeDown",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "18875422",
"name": "暂停 停止 灰色",
"font_class": "stop",
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "15262786",
"name": "1_music82",
"font_class": "next",
"unicode": "e6a9",
"unicode_decimal": 59049
},
{
"icon_id": "15262807",
"name": "1_music83",
"font_class": "prev",
"unicode": "e6ac",
"unicode_decimal": 59052
},
{
"icon_id": "15262830",
"name": "1_music81",
"font_class": "play",
"unicode": "e6aa",
"unicode_decimal": 59050
},
{
"icon_id": "15367",
"name": "下三角形",
"font_class": "xiasanjiaoxing",
"unicode": "e642",
"unicode_decimal": 58946
},
{
"icon_id": "1096518",
"name": "video_fill",
"font_class": "videofill",
"unicode": "e7c7",
"unicode_decimal": 59335
},
{
"icon_id": "29930",
"name": "favor_fill",
"font_class": "favorfill",
"unicode": "e64b",
"unicode_decimal": 58955
},
{
"icon_id": "29931",
"name": "favor",
"font_class": "favor",
"unicode": "e64c",
"unicode_decimal": 58956
},
{
"icon_id": "29934",
"name": "loading",
"font_class": "loading",
"unicode": "e64f",
"unicode_decimal": 58959
},
{
"icon_id": "29947",
"name": "search",
"font_class": "search",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "30417",
"name": "like_fill",
"font_class": "likefill",
"unicode": "e668",
"unicode_decimal": 58984
},
{
"icon_id": "30418",
"name": "like",
"font_class": "like",
"unicode": "e669",
"unicode_decimal": 58985
},
{
"icon_id": "30419",
"name": "notification_fill",
"font_class": "notificationfill",
"unicode": "e66a",
"unicode_decimal": 58986
},
{
"icon_id": "30420",
"name": "notification",
"font_class": "notification",
"unicode": "e66b",
"unicode_decimal": 58987
},
{
"icon_id": "30434",
"name": "evaluate",
"font_class": "evaluate",
"unicode": "e672",
"unicode_decimal": 58994
},
{
"icon_id": "33519",
"name": "home_fill",
"font_class": "homefill",
"unicode": "e6bb",
"unicode_decimal": 59067
},
{
"icon_id": "34922",
"name": "link",
"font_class": "link",
"unicode": "e6bf",
"unicode_decimal": 59071
},
{
"icon_id": "38744",
"name": "round_add_fill",
"font_class": "roundaddfill",
"unicode": "e6d8",
"unicode_decimal": 59096
},
{
"icon_id": "38746",
"name": "round_add",
"font_class": "roundadd",
"unicode": "e6d9",
"unicode_decimal": 59097
},
{
"icon_id": "38747",
"name": "add",
"font_class": "add",
"unicode": "e6da",
"unicode_decimal": 59098
},
{
"icon_id": "43903",
"name": "appreciate_fill",
"font_class": "appreciatefill",
"unicode": "e6e3",
"unicode_decimal": 59107
},
{
"icon_id": "52506",
"name": "forward_fill",
"font_class": "forwardfill",
"unicode": "e6ea",
"unicode_decimal": 59114
},
{
"icon_id": "55448",
"name": "voice_fill",
"font_class": "voicefill",
"unicode": "e6f0",
"unicode_decimal": 59120
},
{
"icon_id": "61146",
"name": "we_fill",
"font_class": "wefill",
"unicode": "e6f4",
"unicode_decimal": 59124
},
{
"icon_id": "90847",
"name": "keyboard",
"font_class": "keyboard",
"unicode": "e71b",
"unicode_decimal": 59163
},
{
"icon_id": "127305",
"name": "pic_fill",
"font_class": "picfill",
"unicode": "e72c",
"unicode_decimal": 59180
},
{
"icon_id": "143738",
"name": "mark_fill",
"font_class": "markfill",
"unicode": "e730",
"unicode_decimal": 59184
},
{
"icon_id": "143740",
"name": "present_fill",
"font_class": "presentfill",
"unicode": "e732",
"unicode_decimal": 59186
},
{
"icon_id": "158873",
"name": "people_fill",
"font_class": "peoplefill",
"unicode": "e735",
"unicode_decimal": 59189
},
{
"icon_id": "176313",
"name": "read",
"font_class": "read",
"unicode": "e742",
"unicode_decimal": 59202
},
{
"icon_id": "212324",
"name": "backward_fill",
"font_class": "backwardfill",
"unicode": "e74d",
"unicode_decimal": 59213
},
{
"icon_id": "212328",
"name": "play_fill",
"font_class": "playfill",
"unicode": "e74f",
"unicode_decimal": 59215
},
{
"icon_id": "240126",
"name": "all",
"font_class": "all",
"unicode": "e755",
"unicode_decimal": 59221
},
{
"icon_id": "240128",
"name": "hot_fill",
"font_class": "hotfill",
"unicode": "e757",
"unicode_decimal": 59223
},
{
"icon_id": "747747",
"name": "record_fill",
"font_class": "recordfill",
"unicode": "e7a4",
"unicode_decimal": 59300
},
{
"icon_id": "1005712",
"name": "full",
"font_class": "full",
"unicode": "e7bc",
"unicode_decimal": 59324
},
{
"icon_id": "1512759",
"name": "favor_fill_light",
"font_class": "favor_fill_light",
"unicode": "e7ec",
"unicode_decimal": 59372
},
{
"icon_id": "4110741",
"name": "round_favor_fill",
"font_class": "round_favor_fill",
"unicode": "e80a",
"unicode_decimal": 59402
},
{
"icon_id": "4110743",
"name": "round_location_fill",
"font_class": "round_location_fill",
"unicode": "e80b",
"unicode_decimal": 59403
},
{
"icon_id": "4110745",
"name": "round_like_fill",
"font_class": "round_like_fill",
"unicode": "e80c",
"unicode_decimal": 59404
},
{
"icon_id": "4110746",
"name": "round_people_fill",
"font_class": "round_people_fill",
"unicode": "e80d",
"unicode_decimal": 59405
},
{
"icon_id": "4110750",
"name": "round_skin_fill",
"font_class": "round_skin_fill",
"unicode": "e80e",
"unicode_decimal": 59406
},
{
"icon_id": "11778953",
"name": "broadcast_fill",
"font_class": "broadcast_fill",
"unicode": "e81d",
"unicode_decimal": 59421
},
{
"icon_id": "12625085",
"name": "card_fill",
"font_class": "card_fill",
"unicode": "e81f",
"unicode_decimal": 59423
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

75
src/renderer/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,75 @@
/* eslint-disable */
/* prettier-ignore */
// @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 onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
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 useId: typeof import('vue')['useId']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel']
const useNotification: typeof import('naive-ui')['useNotification']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

54
src/renderer/components.d.ts vendored Normal file
View File

@@ -0,0 +1,54 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
NAvatar: typeof import('naive-ui')['NAvatar']
NBadge: typeof import('naive-ui')['NBadge']
NButton: typeof import('naive-ui')['NButton']
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDrawer: typeof import('naive-ui')['NDrawer']
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
NDropdown: typeof import('naive-ui')['NDropdown']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInput: typeof import('naive-ui')['NInput']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NList: typeof import('naive-ui')['NList']
NListItem: typeof import('naive-ui')['NListItem']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NPopover: typeof import('naive-ui')['NPopover']
NProgress: typeof import('naive-ui')['NProgress']
NRadio: typeof import('naive-ui')['NRadio']
NRadioButton: typeof import('naive-ui')['NRadioButton']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSlider: typeof import('naive-ui')['NSlider']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NSwitch: typeof import('naive-ui')['NSwitch']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NTooltip: typeof import('naive-ui')['NTooltip']
NTransfer: typeof import('naive-ui')['NTransfer']
NVirtualList: typeof import('naive-ui')['NVirtualList']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

View File

@@ -0,0 +1,72 @@
<template>
<div class="relative inline-block">
<n-popover trigger="hover" placement="top" :show-arrow="true" :raw="true" :delay="100">
<template #trigger>
<slot>
<n-button
quaternary
class="inline-flex items-center gap-2 px-4 py-2 transition-all duration-300 hover:-translate-y-0.5"
>
请我喝咖啡
</n-button>
</slot>
</template>
<div class="p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800">
<div class="flex gap-10">
<div class="flex flex-col items-center gap-2">
<n-image
:src="alipayQR"
alt="支付宝收款码"
class="w-32 h-32 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">支付宝</span>
</div>
<div class="flex flex-col items-center gap-2">
<n-image
:src="wechatQR"
alt="微信收款码"
class="w-32 h-32 rounded-lg cursor-none"
preview-disabled
/>
<span class="text-sm text-gray-700 dark:text-gray-200">微信支付</span>
</div>
</div>
<div class="mt-4">
<p
class="text-sm text-gray-700 dark:text-gray-200 text-center cursor-pointer hover:text-green-500"
@click="copyQQ"
>
QQ群789288579
</p>
</div>
</div>
</n-popover>
</div>
</template>
<script setup>
import { NButton, NImage, NPopover } from 'naive-ui';
import alipay from '@/assets/alipay.png';
import wechat from '@/assets/wechat.png';
const message = useMessage();
const copyQQ = () => {
navigator.clipboard.writeText('789288579');
message.success('已复制到剪贴板');
};
defineProps({
alipayQR: {
type: String,
default: alipay
},
wechatQR: {
type: String,
default: wechat
}
});
</script>

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