Compare commits

...

140 Commits

Author SHA1 Message Date
lyswhut
f9f0e61c5b 更新版本号(2.12.6-beta.2) 2026-09-19 00:51:18 +08:00
lyswhut
d2f5485996 新增 Windows 旧版提示功能,提醒用户升级至新版系统以提高安全性和性能 2026-09-19 00:50:49 +08:00
lyswhut
e20b2844fb 优化透明窗口边框显示效果,修复全屏模式下的相关问题 2026-09-19 00:29:30 +08:00
lyswhut
e9bc6a572b 修复打开某些 kg 歌单时歌曲丢失的问题 2026-09-16 23:27:12 +08:00
lyswhut
bb8d07274f 优化 tx 推荐歌单列表 2026-09-16 21:18:54 +08:00
lyswhut
950029884c 更新版本号(2.12.6-beta.1) 2026-09-14 21:01:01 +08:00
lyswhut
7c78e557ed 修复 win7 无法启动的问题 2026-09-14 21:00:00 +08:00
lyswhut
78eb5c48d7 更新版本号(2.12.6-beta.0) 2026-09-14 20:14:29 +08:00
lyswhut
96fd33065a 修复 win7 无法启动的问题 2026-09-14 20:05:55 +08:00
lyswhut
abcbf5fa00 发布 v2.12.5 2026-09-14 01:07:30 +08:00
lyswhut
378174c277 fix: 降级 @simonwep/pickr 和 webpack 版本,修复依赖问题 2026-09-14 00:59:43 +08:00
lyswhut
39466f157c 发布 v2.12.4 2026-09-12 20:40:02 +08:00
lyswhut
d1fa51c2bb fix: 修复主题背景图片URL构建逻辑 2026-09-12 20:38:08 +08:00
lyswhut
a0e16fc4f9 发布 v2.12.3 2026-09-12 13:04:34 +08:00
lyswhut
f64da62aa8 更新依赖&版本号(2.12.3-beta.12) 2026-09-06 14:38:42 +08:00
3gf8jv4dv
75185cb50b chore: adjust item order of language options (#2946) 2026-08-25 13:39:41 +08:00
3gf8jv4dv
bf9b34b47f fix(l10n): correct window size option text in zh-tw (#2943)
resolves https://github.com/lyswhut/lx-music-desktop/issues/2932
2026-08-23 10:05:41 +08:00
lyswhut
6573a5cc8a fix: 修正armv7l架构映射以匹配正确的值 2026-08-21 20:38:31 +08:00
lyswhut
7c3ca2a0fc fix: 修复架构映射以正确复制库文件 2026-08-21 20:33:01 +08:00
lyswhut
e8214393e9 fix: 修复构建 2026-08-21 19:48:00 +08:00
lyswhut
f4f9496fda 更新依赖&版本号(2.12.3-beta.11) 2026-08-21 19:32:41 +08:00
jtcai
c68361719d 修正tx字段拼写错误 (#2911)
* 修正部分qq歌单返回字段

* 修正tx字段total_song_sum->total_song_num

---------

Co-authored-by: jtcai <jtcai@users.noreply.github.com>
2026-08-05 00:14:05 +08:00
lyswhut
936fcb79fa 更新依赖&版本号(2.12.3-beta.10) 2026-08-05 00:09:26 +08:00
lyswhut
65ff972d1a feat: 添加版本字符常量以优化歌曲匹配机制 2026-08-04 23:57:28 +08:00
moduvoice
9ff8ae2587 feat(i18n): add Korean (ko-kr) translation (#2902)
* feat(i18n): add Korean (ko-kr) translation

Add full Korean (ko-kr) locale for the app UI, translated from the
English (en-us) source with full key parity (746/746 keys) and all
placeholders preserved. Registers the new locale in languages.json
and src/lang/index.ts, mirroring how existing locales are wired up.

* feat(i18n): add Korean tray menu strings to tray.ts

Add ko-kr entry to the tray module's messages map, matching the
existing en-us/zh-cn/zh-tw pattern with full key parity (19/19 keys):
play/pause, next/prev song, collect/uncollect, show/hide main window,
show/hide/lock/unlock/top desktop lyric window, statusbar lyric
toggle, exit, and tooltip labels.

Addresses reviewer feedback on PR #2902.
2026-07-15 09:36:41 +08:00
3gf8jv4dv
c0dd64c71c fix: use IEC units for binary sizes (#2901) 2026-07-09 09:40:19 +08:00
lyswhut
f3fbbb3720 chore: 更新版本号至 2.12.3-beta.9 2026-07-07 22:23:02 +08:00
lyswhut
5279fbba56 chore: 更新版本号至 2.12.3-beta.8 2026-07-06 21:02:49 +08:00
lyswhut
bbbbfeda12 fix: 修复歌词解析中毫秒时间的处理逻辑 2026-07-06 20:56:58 +08:00
lyswhut
75ba99d9d1 修复歌词标签解析格式没有严格按照标准的问题(#2855) 2026-07-06 20:56:58 +08:00
jtcai
af4603d87b 修正部分qq歌单返回字段 (#2899)
Co-authored-by: jtcai <jtcai@users.noreply.github.com>
2026-07-06 20:56:44 +08:00
lyswhut
1cb9e2fd29 chore: 更新版本号至 2.12.3-beta.7 2026-07-02 12:43:43 +08:00
lyswhut
d5b1b3fd5f fix: 修复在某些情况下移动歌曲时可能导致保存的顺序不对的问题(#2842) 2026-07-02 12:42:54 +08:00
lyswhut
f0776becff 更新依赖&版本号(2.12.3-beta.6) 2026-06-21 13:11:44 +08:00
lyswhut
03fea45166 降级 electron 2026-06-21 13:08:23 +08:00
ikun0014
905b2c3c42 feat: kg 源搜索改用 AndroidFilter 接口并提供 albumAudioId (#2849)
将搜索接口 platform 从 WebFilter 切换到 AndroidFilter,新接口返回
MixSongID(即 album_audio_id),用于后续歌词精确搜索。

- songmid 各处仍保持原取值(Audioid / audio_id)不变,避免影响
  既有歌曲 id 与旧收藏/列表数据
- 所有歌曲列表产出点均新增 albumAudioId 字段:
    musicSearch(MixSongID)、songList 顶层(album_audio_id)、
    songList 详情/leaderboard/musicInfo(audio_group_id / album_audio_id)
- music.d.ts 的 MusicInfoMeta_kg 增加 albumAudioId 字段,
  tools.ts 新旧格式双向转换中保留,确保持久化后不丢失

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 10:31:46 +08:00
ikun0014
21619e72dc fix: 再次修复 tx 源搜索偶现风控 (#2848)
* fix: 修复 tx 源搜索偶现风控

将搜索接口从移动端 DoSearchForQQMusicMobile 切换到 PC 端
DoSearchForQQMusicDesktop,更新 comm 参数与 searchid 生成方式,
并兼容新旧返回结构(music.search.SearchCgiService / req)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: lyswhut <lyswhut@qq.com>
2026-06-21 10:30:06 +08:00
啊呜
33742bca6b Win文件名中存在#特殊字符时无法正常播放 (#2862)
(cherry picked from commit 890b6ea3da)
2026-06-14 20:34:42 +08:00
lyswhut
e0575fc179 fix: 移除请求函数中的不必要的类型注释 2026-06-14 20:14:01 +08:00
lyswhut
97fe5fa496 更新依赖&版本号(2.12.3-beta.5) 2026-06-14 19:41:56 +08:00
lyswhut
97533cd4aa fix: 修复在某些情况下添加列表、歌曲时可能导致保存的顺序不对的问题(#2842) 2026-06-10 09:24:34 +08:00
lyswhut
a7672168c6 修复旧版请求函数中的重定向选项 2026-06-07 11:45:59 +08:00
lyswhut
5c46ca648d 修复某些 tx 歌单打开失败的问题 2026-06-07 11:45:59 +08:00
lyswhut
38c7bc0d0d 修复 mg 图片、歌词获取 2026-06-07 11:45:59 +08:00
ikun0014
cb2798cba9 refactor: 将QRC解密替换为纯JS实现并简化kw歌词获取 (#2845)
* 简化kw歌词获取 && kw歌词解密逻辑移动

将酷我歌词解密从主进程 native/IPC 方案迁移到渲染进程纯 JS 实现,
移除 kw_decodeLyric 主进程入口与 handle_kw_decode_lyric IPC 通道。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* tx歌词解密改用js实现

将 QRC 歌词的 3DES 解密由主进程 native 插件(qrc_decode.node)按原 C++
算法 1:1 移植为渲染进程纯 JS 实现,移除 tx_decodeLyric 主进程入口、
handle_tx_decode_lyric IPC 通道,以及 build 配置中对 qrc_decode.node
的引用与各平台预构建二进制。

注意:QRC 使用的是非标准 DES 变体(S2[23]=15、S4[53]=10,标准为
14、1),新实现已逐字节对照原 native 二进制保持一致。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* update

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: lyswhut <lyswhut@qq.com>
2026-06-07 11:14:17 +08:00
lyswhut
f0342b852b 更新版本号(2.12.3-beta.4) 2026-06-04 21:43:22 +08:00
lyswhut
97346617a5 更新版本号(2.12.3-beta.3) 2026-06-04 21:22:23 +08:00
lyswhut
ecdc018fa1 Linux 点击托盘时将显示主界面(#2840) 2026-06-04 20:54:55 +08:00
lyswhut
cbcbe3e8c8 更新版本号(2.12.3-beta.2) 2026-05-23 11:01:51 +08:00
lyswhut
b12c5ed12b 同步服务在连接时允许URL重定向 2026-05-23 11:00:21 +08:00
3gf8jv4dv
96921b255f fix(l10n): update import songlist dialog text (#2797)
refs https://github.com/lyswhut/lx-music-doc/issues/64
refs https://github.com/lyswhut/lx-music-desktop/issues/2711
2026-05-17 14:42:49 +08:00
lyswhut
2df1077ee4 更新依赖&版本号(2.12.3-beta.1) 2026-05-15 12:36:43 +08:00
lyswhut
4937ddb09c 修复某些情况下开放 API 获取到的音量为0 的问题(#2790) 2026-05-15 12:15:04 +08:00
lyswhut
4b00a06a88 优化kw歌单列表数据显示 2026-05-15 12:13:13 +08:00
lyswhut
be48e78a6f 更新依赖&版本号(2.12.3-beta.0) 2026-05-05 15:31:34 +08:00
lyswhut
b7a3663078 修复 kg 搜索结果显示问题(#2782) 2026-05-05 14:50:34 +08:00
lyswhut
9c364b482e 更新依赖 2026-05-01 13:12:19 +08:00
lyswhut
355c5e584b 修复音调调节问题 2026-05-01 13:05:57 +08:00
lyswhut
f956fd93d5 将 actions/cache 版本从 v6 降级至 v5 2026-05-01 02:28:50 +08:00
lyswhut
69e285d3ca 升级 GitHub Actions 依赖至最新版本 2026-05-01 02:26:18 +08:00
lyswhut
728fed2ed3 发布 v2.12.2 2026-05-01 01:31:36 +08:00
lyswhut
bc0d2264a3 更新依赖 2026-05-01 01:30:51 +08:00
lyswhut
ba8a6bf76c 修复音乐名称和歌手信息格式化(#2733) 2026-04-23 18:08:44 +08:00
lyswhut
2cf84a796d 更新依赖&版本号(2.12.2-beta.1) 2026-04-10 12:16:30 +08:00
lyswhut
ba62ae27af 修复 tx 偶现搜索失败的问题 2026-04-10 09:11:29 +08:00
lyswhut
e35895d752 移除 babel 2026-04-10 09:10:58 +08:00
lyswhut
cc192cab44 fix: 修复 tx 搜索结果显示异常的问题(#2753) 2026-04-09 12:47:28 +08:00
lyswhut
e36792b76a 更新依赖&版本号(2.12.2-beta.0) 2026-03-31 18:14:35 +08:00
lyswhut
61231ab99e feat: 优化搜索功能,增强歌曲匹配逻辑 2026-03-31 18:11:31 +08:00
lyswhut
ae2ee0e61e 更新依赖 2026-03-23 09:29:30 +08:00
Stanislav Chernov
a339f0fd0c fix: 音源导入去重 (#2681)
* fix: 音源导入添加去重逻辑
修复音源导入时未对重复数据进行去重的问题。现在会根据url去重,避免导入重复的音源。
Fixes lyswhut/lx-music-desktop#2649

* fix: check script content instead of name for dedup

Allow duplicate source names (intentional behavior).
Only reject import when script content is identical to an existing
source, and report which source name conflicts.
2026-03-11 14:52:52 +08:00
lyswhut
b9c3c3506e 修复桌面歌词的鼠标悬停隐藏功能,优化鼠标事件处理逻辑 2026-03-07 14:11:44 +08:00
lyswhut
44d96264fb 修复某些情况下可能出现播放歌曲没有声音的问题 (#2693) 2026-03-07 14:06:12 +08:00
derr
19eab0418a fix(mac): 修复 Mac 睡眠导致音乐播放中断的问题 (#2692)
* fix(mac): prevent sleep on mac during playback

* Remove space between async and parentheses in functions

---------

Co-authored-by: ydm <ydm@49os.com>
Co-authored-by: lyswhut <lyswhut@qq.com>
2026-03-07 13:56:55 +08:00
Little_100
0695bbaa70 修复桌面歌词的 鼠标移入歌词区域时提高透明度 设置不稳定的问题 (#2679)
Co-authored-by: lyswhut <lyswhut@qq.com>
2026-03-07 13:52:35 +08:00
lyswhut
8d6b207833 发布 v2.12.1 2026-02-16 21:11:25 +08:00
lyswhut
3371a42822 更新依赖 2026-02-16 21:03:26 +08:00
lyswhut
fe712318b8 更新依赖&版本号(2.12.1-beta.3) 2026-01-31 12:44:16 +08:00
ikun0014
593d8327b2 fix: 网易云部分歌曲无法被搜索 (#2666)
* fix: 网易云部分歌曲无法被搜索

将搜索请求由原有 eapiRequest 改为通过 httpFetch 调用 /eapi/search/song/list/page 并使用 eapi 加密表单;调整导入(使用 eapi)、移除部分注释和冗余代码。更新结果解析以兼容新响应结构(使用 item.baseInfo.simpleSongData、result.data.resources 和 result.data.totalCount 等字段),并保留原有的分页与重试逻辑。

* update

* update

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2026-01-31 12:36:28 +08:00
lyswhut
d5c54b770f 更新依赖&版本号(2.12.1-beta.2) 2026-01-01 11:58:30 +08:00
lyswhut
32a9a21be1 优化托盘图标行为:在非 Windows 系统中,点击托盘图标时不再显示主窗口 2025-12-31 09:53:47 +08:00
lyswhut
e0735f8928 更新依赖&版本号(2.12.1-beta.1) 2025-12-21 13:46:31 +08:00
lyswhut
c6dc962134 修复某些情况下搜索框的搜索按钮布局错位的问题(#2622) 2025-12-21 13:45:40 +08:00
lyswhut
e068103e4d 修复构建 2025-12-12 12:52:31 +08:00
lyswhut
6eb5ade9ea 更新依赖&版本号(2.12.1-beta.0) 2025-12-12 12:37:16 +08:00
lyswhut
c40168cacd 更新构建流程,集成 undici 库并优化请求处理 2025-12-12 12:28:04 +08:00
lyswhut
a546b75986 修复音量条在调整音量时实际音量与显示的数值不一致的问题(#2606) 2025-12-07 14:07:12 +08:00
lyswhut
8686ecfa54 更新二进制库 2025-11-29 16:31:23 +08:00
lyswhut
270c48a1dd 发布 v2.12.0 2025-11-29 16:27:28 +08:00
lyswhut
fdbec2cbac 更新说明 2025-11-29 16:26:14 +08:00
lyswhut
46049fd15c 更新版本号(2.12.0-beta.15) 2025-11-28 12:06:21 +08:00
lyswhut
a81fe6ff7f 修复MG最新评论翻页 2025-11-28 12:06:17 +08:00
lyswhut
fd4e8ecb01 更新版本号(2.12.0-beta.14) 2025-11-26 13:41:57 +08:00
lyswhut
8142e5e79e 修复MG源评论加载失败 2025-11-26 13:41:22 +08:00
lyswhut
0c71772953 修复MG源歌单加载失败 2025-11-26 13:41:22 +08:00
lyswhut
d54bc21441 更新版本号(2.12.0-beta.13) 2025-11-23 10:21:40 +08:00
lyswhut
be4bca2c15 更新字体设置,支持最多设置两种字体,并调整选择组件宽度 2025-11-23 10:20:27 +08:00
lyswhut
2bd54673de 更新依赖&版本号(2.12.0-beta.12) 2025-11-21 09:09:01 +08:00
lyswhut
4a3359f773 更新依赖 2025-11-14 18:52:51 +08:00
lyswhut
c35095f90e 修复获取歌曲图片的函数,改为直接返回图片链接 2025-11-14 18:52:51 +08:00
lyswhut
32abccd8c2 更新版本号(2.12.0-beta.11) 2025-11-02 21:21:15 +08:00
Folltoshe
5d68ca3e18 修复TX源搜索失败 (#2575)
* 修复TX源搜索失败

* 清理音乐搜索模块中的多余空行

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2025-11-02 21:19:26 +08:00
lyswhut
8efa5335b0 更新版本号(2.12.0-beta.10) 2025-10-01 13:30:12 +08:00
lyswhut
4710bb16d8 下载列表菜单新增歌曲添加弹窗(#2537) 2025-10-01 13:29:35 +08:00
lyswhut
db4f3e4e1b 更新版本号(2.12.0-beta.9) 2025-09-29 12:48:33 +08:00
lyswhut
e18272a6d7 更新依赖 2025-09-29 12:43:24 +08:00
lyswhut
85aed9ce9d 修复隐藏状态下的透明度问题,增加鼠标悬停时恢复不透明度的效果 2025-09-29 12:43:12 +08:00
lyswhut
d03f93166f 新增完整歌词相关字段和API支持 2025-09-29 12:37:22 +08:00
lyswhut
a3b1a433c6 更新版本号(2.12.0-beta.8) 2025-09-19 23:54:44 +08:00
lyswhut
608f63bc9c 临时修复桌面歌词交互时背景异常的问题 2025-09-19 23:53:45 +08:00
lyswhut
a4c118648a mac下默认不使用透明窗口 2025-09-19 23:51:00 +08:00
lyswhut
42866b8e4f 修复构建 2025-09-07 12:14:31 +08:00
lyswhut
0b7455b74f 更新依赖&版本号(2.12.0-beta.7) 2025-09-07 11:53:50 +08:00
lyswhut
eab7bc3f5f 优化歌词解析 2025-08-11 13:49:26 +08:00
lyswhut
af2623d16c 更新版本号(2.12.0-beta.6) 2025-08-10 13:46:52 +08:00
lyswhut
e683034e77 新增 Any Listen 歌词标签数据嵌入、下载、读取播放(#2485) 2025-08-10 13:42:54 +08:00
lyswhut
ffa979ddc8 更新依赖版本,优化组件逻辑 2025-08-09 18:07:16 +08:00
3gf8jv4dv
8a8c8a5699 Revise text for lyric position switching feature (#2463)
- Fix incorrect i18n key
- Revise English translation
- Revise changelog descriptions to be more accurate
2025-08-06 12:24:03 +08:00
lyswhut
2aea956fd0 更新依赖&版本号(2.12.0-beta.5) 2025-08-06 12:22:20 +08:00
lyswhut
1803517943 优化 hidden 启动参数的行为 2025-08-06 12:21:47 +08:00
lyswhut
49c8081f68 尝试修复进度为0时仍然显示下载完成的问题(#2471) 2025-08-05 12:16:24 +08:00
lyswhut
3d64ca37e7 更新版本号(2.12.0-beta.4) 2025-07-19 14:29:32 +08:00
lyswhut
ccc88745a6 降级 electron 版本至 35.3.0 2025-07-19 14:28:47 +08:00
lyswhut
0bf6dbd402 更新依赖&版本号(2.12.0-beta.3) 2025-07-19 02:16:52 +08:00
lyswhut
b9abb4ce47 新增启动参数 -hidden,在启动时将软件最小化到系统托盘(#2459) 2025-07-18 22:24:15 +08:00
lyswhut
ad0c0f37c9 新增调换翻译歌词与罗马音歌词位置设置,优化歌词显示逻辑(#2451) 2025-07-09 08:58:34 +08:00
Folltoshe
4c6257c145 更新代理配置规则,现在不启用代理时,图片、音频加载将不再走系统代理 (#2382)
* 完善代理设置

* 更新

* update

---------

Co-authored-by: lyswhut <lyswhut@qq.com>
2025-05-10 10:43:58 +08:00
lyswhut
6c5231dbff 恢复更新日志 2025-05-10 10:07:45 +08:00
lyswhut
0454bb9b3c 更新依赖&版本号(2.12.0-beta.2) 2025-05-10 01:30:48 +08:00
lyswhut
021f89da29 新增播放器声音大小、静音、播放进度控制(#2386) 2025-05-10 00:58:19 +08:00
lyswhut
2ccca633ed 优化滚动事件监听,启用 passive 选项以提升性能 2025-05-09 10:02:37 +08:00
lyswhut
fe15e025aa 更新数据库备份逻辑 2025-05-09 10:02:37 +08:00
3gf8jv4dv
9e0c416db5 Revise chinese terminology for window and font size (#2384)
addresses https://github.com/lyswhut/lx-music-mobile/issues/769
2025-05-07 21:53:24 +08:00
3gf8jv4dv
fa9b84a9c9 Revise wording of main window style options - 2 (#2379)
- Not all operating systems use rounded corners. Like Windows 10.
- Further optimize wording.
- Forgot to add line breaks to CHANGELOG :(
2025-05-06 09:15:31 +08:00
lyswhut
b3b1bd7bc8 更新版本号(2.12.0-beta.1) 2025-05-03 11:23:53 +08:00
lyswhut
91e806a619 为数据库启用 WAL 模式 2025-05-03 11:22:35 +08:00
3gf8jv4dv
6df6eefd3d Revise wording of main window style options (#2372)
- Correct typos
- Edit i18n key name
- Avoid displaying long descriptions directly
2025-05-03 11:06:28 +08:00
lyswhut
ae35d05649 修正透明窗口设置逻辑 2025-05-02 13:02:01 +08:00
lyswhut
72939db2df 更新版本号(2.12.0-beta.0) 2025-05-02 12:36:08 +08:00
lyswhut
b6f5770dc2 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」设置 2025-05-02 12:28:16 +08:00
167 changed files with 7439 additions and 7100 deletions

View File

@@ -1,26 +0,0 @@
{
"presets": [
"@babel/preset-typescript",
[
"@babel/preset-env",
{
"corejs": "3",
"useBuiltIns": "usage"
}
]
// [
// "minify",
// {
// "builtIns": false,
// "evaluate": false,
// "mangle": false
// }
// ]
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-modules-umd",
"@babel/plugin-transform-runtime",
"@babel/plugin-proposal-class-properties"
]
}

View File

@@ -55,7 +55,6 @@ const vueRule = {
exports.base = {
extends: ['standard'],
rules: baseRule,
parser: '@babel/eslint-parser',
}
exports.html = {

View File

@@ -5,7 +5,7 @@ runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '22'
@@ -16,7 +16,7 @@ runs:
# https://docs.npmjs.com/cli/v10/configuring-npm/folders#cache
- name: Cache node modules
id: cache-npm
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ env.NPM_CACHE }}
key: ${{ runner.os }}-npm-cache-${{ hashFiles('package-lock.json') }}

View File

@@ -14,7 +14,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: Check out git repository
# uses: actions/checkout@v4
# uses: actions/checkout@v6
# - name: Install Node.js
# uses: actions/setup-node@v4
@@ -46,7 +46,7 @@ jobs:
# needs: CheckCode
steps:
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get npm cache directory
shell: pwsh
@@ -65,7 +65,7 @@ jobs:
- name: Build Package Setup x64
run: npm run pack:win:setup:x64
- name: Upload Artifact Setup x64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-x64-Setup
path: build/*-x64-Setup.exe
@@ -73,7 +73,7 @@ jobs:
- name: Build Package 7z x64
run: npm run pack:win:7z:x64
- name: Upload Artifact 7z x64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-win_x64-green
path: build/*win_x64-green.7z
@@ -81,7 +81,7 @@ jobs:
- name: Build Package Setup arm64
run: npm run pack:win:setup:arm64
- name: Upload Artifact Setup arm64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-arm64-Setup
path: build/*-arm64-Setup.exe
@@ -89,22 +89,56 @@ jobs:
- name: Build Package 7z arm64
run: npm run pack:win:7z:arm64
- name: Upload Artifact 7z arm64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-win_arm64-green
path: build/*win_arm64-green.7z
- name: Generate file MD5
run: |
cd build
Get-FileHash *.exe,*.7z -Algorithm MD5 | Format-List
- name: Install old electron
run: npm install electron@22
Windows_7:
name: Windows_7
runs-on: windows-latest
env:
BUILD_WIN7: true
# needs: CheckCode
steps:
- name: Check out git repository
uses: actions/checkout@v6
- name: Install python setuptools
run: pip.exe install setuptools
- name: Get npm cache directory
shell: pwsh
run: echo "NPM_CACHE=$(npm config get cache)" >> $env:GITHUB_ENV
- name: Setup Node Env
env:
NPM_CACHE: ${{ env.NPM_CACHE }}
uses: ./.github/actions/setup
- name: Prepare win7 undici env
run: |
npm install undici@5
Set-Content -Path .\src\common\utils\request.ts -Value "export * from './request_node16'"
- name: Build src code
env:
BUILD_WIN7: true
run: |
git status --porcelain
npm run build
- name: Prepare win7 electron env
run: |
npm install electron@22 better-sqlite3@12
pip.exe install setuptools
- name: Build Package win7 Setup x64
run: npm run pack:win7:setup:x64
- name: Upload Artifact win7 Setup x64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-win7_x64-Setup
path: build/*win7_x64-Setup.exe
@@ -112,7 +146,7 @@ jobs:
- name: Build Package win7 7z x64
run: npm run pack:win7:7z:x64
- name: Upload Artifact win7 7z x64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-win7_x64-green
path: build/*win7_x64-green.7z
@@ -120,7 +154,7 @@ jobs:
- name: Build Package win7 7z x86
run: npm run pack:win7:7z:x86
- name: Upload Artifact win7 7z x86
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-win7_x86-green
path: build/*win7_x86-green.7z
@@ -136,7 +170,7 @@ jobs:
# needs: CheckCode
steps:
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install python setuptools
run: brew install python-setuptools
@@ -161,14 +195,14 @@ jobs:
npm run pack:mac:dmg:arm64
- name: Upload Artifact dmg
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-mac-dmg
path: |
build/*.dmg
!build/*-arm64.dmg
- name: Upload Artifact dmg
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-mac-dmg-arm64
path: build/*-arm64.dmg
@@ -187,7 +221,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get npm cache directory
shell: bash
@@ -206,7 +240,7 @@ jobs:
- name: Build Package deb amd64
run: npm run pack:linux:deb:amd64
- name: Upload Artifact deb amd64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-deb-amd64
path: build/*_amd64.deb
@@ -214,7 +248,7 @@ jobs:
- name: Build Package deb arm64
run: npm run pack:linux:deb:arm64
- name: Upload Artifact deb arm64
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-deb-arm64
path: build/*_arm64.deb
@@ -222,7 +256,7 @@ jobs:
- name: Build Package deb armv7l
run: npm run pack:linux:deb:armv7l
- name: Upload Artifact deb armv7l
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-deb-armv7l
path: build/*_armv7l.deb
@@ -230,7 +264,7 @@ jobs:
- name: Build Package x64 appImage
run: npm run pack:linux:appImage
- name: Upload Artifact x64 appImage
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-x64-appImage
path: build/*_x64.AppImage
@@ -238,7 +272,7 @@ jobs:
- name: Build Package x64 rpm
run: npm run pack:linux:rpm
- name: Upload Artifact x64 rpm
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-x64-rpm
path: build/*.x64.rpm
@@ -246,7 +280,7 @@ jobs:
- name: Build Package x64 pacman
run: npm run pack:linux:pacman
- name: Upload Artifact x64 pacman
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lx-music-desktop-x64-pacman
path: build/*_x64.pacman

View File

@@ -11,10 +11,10 @@ jobs:
steps:
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 22

View File

@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v2
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.PAT }}
repository: lyswhut/lx-music-desktop-version-info

View File

@@ -14,7 +14,7 @@ jobs:
# runs-on: ubuntu-latest
# steps:
# - name: Check out git repository
# uses: actions/checkout@v4
# uses: actions/checkout@v6
# - name: Install Node.js
# uses: actions/setup-node@v4
@@ -46,7 +46,7 @@ jobs:
# needs: CheckCode
steps:
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get npm cache directory
shell: pwsh
@@ -87,7 +87,7 @@ jobs:
# needs: CheckCode
steps:
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get npm cache directory
shell: pwsh
@@ -98,14 +98,21 @@ jobs:
NPM_CACHE: ${{ env.NPM_CACHE }}
uses: ./.github/actions/setup
- name: Prepare win7 undici env
run: |
npm install undici@5
Set-Content -Path .\src\common\utils\request.ts -Value "export * from './request_node16'"
- name: Build src code
env:
BUILD_WIN7: true
run: |
git status --porcelain
npm run build
- name: Prepare win7 electron env
run: |
npm install electron@22
npm install electron@22 better-sqlite3@12
pip.exe install setuptools
- name: Release win7 package
@@ -128,7 +135,7 @@ jobs:
# needs: CheckCode
steps:
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install python3 setuptools
run: brew install python-setuptools
@@ -171,7 +178,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y rpm libarchive-tools
- name: Check out git repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Get npm cache directory
shell: bash

View File

@@ -6,6 +6,7 @@ module.exports = {
'del',
'comlink',
'vue',
'vue-router',
'image-size',
'message2call',
'@types/ws',
@@ -13,10 +14,17 @@ module.exports = {
'@types/node',
'electron-debug',
'eslint-webpack-plugin',
'typescript',
'undici',
'@types/better-sqlite3',
'changelog-parser',
'webpack-dev-server',
'eslint-plugin-vue',
'vue-eslint-parser',
// 'eslint-config-standard-with-typescript',
'webpack',
],
// target: 'newest',
@@ -27,7 +35,9 @@ module.exports = {
// target: 'patch',
// filter: [
// 'electron',
// 'vue',
// 'vue-router',
// ],
// target: 'minor',
@@ -36,9 +46,14 @@ module.exports = {
// 'eslint',
// 'eslint-webpack-plugin',
// 'electron-debug',
// 'typescript',
// '@types/node',
// 'undici',
// 'eslint-plugin-vue',
// 'vue-eslint-parser',
// 'better-sqlite3',
// '@types/better-sqlite3',
// 'webpack-dev-server',
// ],
}

View File

@@ -6,6 +6,121 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [2.12.5](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.4...v2.12.5) - 2026-09-14
目前新项目 Any Listen 的桌面版、Web 版已实现 LX Music 的大部分功能,并额外支持 WebDAV 歌曲播放、WebDAV 数据同步、独立播放列表等功能。
以后的开发精力将主要集中在新项目上,之前大家在 LX Music 提的功能我们也会考虑在新项目中添加。
对于日常使用 LX Music 的人可以试试迁移到 Any Listen若遇到任何问题可以发 issue 反馈。
Any Listen 的项目地址为 https://github.com/any-listen/any-listen
### 修复
- 修复自定义主题编辑器的颜色选择器默认值显示异常的问题(#2957
- 修复下载的 MP3 文件内嵌歌曲数据失败的问题(#2959
## [2.12.4](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.3...v2.12.4) - 2026-09-12
目前新项目 Any Listen 的桌面版、Web 版已实现 LX Music 的大部分功能,并额外支持 WebDAV 歌曲播放、WebDAV 数据同步、独立播放列表等功能。
以后的开发精力将主要集中在新项目上,之前大家在 LX Music 提的功能我们也会考虑在新项目中添加。
对于日常使用 LX Music 的人可以试试迁移到 Any Listen若遇到任何问题可以发 issue 反馈。
Any Listen 的项目地址为 https://github.com/any-listen/any-listen
### 修复
- 修复自定义主题背景图片无法显示的问题
## [2.12.3](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.2...v2.12.3) - 2026-09-12
目前新项目 Any Listen 的桌面版、Web 版已实现 LX Music 的大部分功能,并额外支持 WebDAV 歌曲播放、WebDAV 数据同步、独立播放列表等功能。
以后的开发精力将主要集中在新项目上,之前大家在 LX Music 提的功能我们也会考虑在新项目中添加。
对于日常使用 LX Music 的人可以试试迁移到 Any Listen若遇到任何问题可以发 issue 反馈。
Any Listen 的项目地址为 https://github.com/any-listen/any-listen
### 优化
- 优化kw歌单列表数据显示
- 同步服务在连接时允许URL重定向
- Linux 点击托盘时将显示主界面(#2840
- 优化自动换源歌曲匹配机制
### 修复
- 修复 kg 搜索结果显示问题 (#2782)
- 修复某些情况下开放 API 获取到的音量为 0 的问题 (#2790)
- 修复 mg 图片、歌词获取
- 修复某些 tx 歌单打开失败的问题 (#1060, @ght-000)
- 修复在某些情况下添加、移动歌曲时可能导致保存的歌曲顺序不对的问题(#2842
- 修复歌词标签解析格式没有严格按照标准的问题(#2855
- 修复 tx 歌曲搜索失败的问题(#2848, @ikun0014
## [2.12.2](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.1...v2.12.2) - 2026-05-01
我们很高兴地宣布新项目 Any Listen 的桌面版已发布目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能更多功能仍在积极开发中桌面版与Web版将同步更新。
对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。
### 优化
- 优化歌单内歌曲搜索结果排序 (#2734)
### 修复
- 修复桌面歌词的 鼠标移入歌词区域时提高透明度 设置不稳定的问题(#2679, @Little100
- 修复某些情况下可能播放没有声音的问题(#2693
- 修复 tx 搜索结果显示异常的问题(#2753
- 修复音乐名称和歌手信息格式化问题(#2733
### 其他
- 更新 Electron 到 40.8.3
## [2.12.1](https://github.com/lyswhut/lx-music-desktop/compare/v2.12.0...v2.12.1) - 2026-02-16
我们很高兴地宣布新项目 Any Listen 的桌面版已发布目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能更多功能仍在积极开发中桌面版与Web版将同步更新。
对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。
### 优化
- 优化托盘图标行为:在非 Windows 系统中,点击托盘图标时不再显示主窗口
### 修复
- 修复音量条在调整音量时实际音量与显示的数值不一致的问题(#2606
- 修复某些情况下搜索框的搜索按钮布局错位的问题(#2622
## [2.12.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.11.0...v2.12.0) - 2025-11-29
我们很高兴地宣布新项目 Any Listen 的桌面版已发布目前已支持列表跟随本地文件自动更新、加载并播放WebDAV上的歌曲等功能更多功能仍在积极开发中桌面版与Web版将同步更新。
对于有播放本地音乐或播放服务器上音乐需求的人可以试试,若遇到任何问题可以发 issue 反馈。
### 新增
- 新增「设置 → 其他设置 → 主窗口使用软件内置的圆角及阴影」选项 (#2360)
*默认启用,关闭后将使用系统原生的窗口样式,该选项重启软件后生效*
- 开放 API 新增播放器声音大小、静音、播放进度控制、完整歌词获取,详情看接入文档 (#2386)
- 新增「设置 → 播放设置 → 调换歌词翻译与歌词罗马音位置」选项,默认关闭 (#2451)
- 新增启动参数 `-hidden`,在启动时将软件最小化到系统托盘 (#2459)
- 新增 Any Listen 歌词(用于支持已下载歌曲的歌词逐字播放)标签数据读取与播放 (#2485)
- 新增 Any Listen 歌词(包含逐字歌词、翻译、罗马音歌词,如果有)嵌入与下载,默认启用
- 下载列表菜单新增歌曲添加弹窗,允许将所选歌曲的在线版本添加到收藏列表 (#2537)
### 修复
- 尝试修复进度为0时仍然显示下载完成的问题 (#2471)
- 修复TX源搜索失败 (#2575 @Folltoshe)
- 修复MG源歌单加载失败
- 修复MG源评论加载失败
### 变更
- 调换「歌词翻译」与「歌词罗马音」的位置,现在歌词罗马音在歌词翻译的上方展示
*若你想要恢复以前的行为,可以开启「调换歌词翻译与歌词罗马音位置」选项*
- 更新代理配置规则,现在不启用代理时,图片、音频加载将不再走系统代理 (#2382 @Folltoshe)
- 字体设置可以最多设置两种字体([any-listen#82](https://github.com/any-listen/any-listen/issues/82)
### 其他
- 更新 Electron 到 37.6.0
## [2.11.0](https://github.com/lyswhut/lx-music-desktop/compare/v2.10.0...v2.11.0) - 2025-05-01
### 新增

View File

@@ -47,6 +47,13 @@
*移动版项目地址https://github.com/lyswhut/lx-music-mobile*
> [!NOTE]
> 目前新项目 [Any Listen](https://github.com/any-listen/any-listen) 的桌面版、Web 版已实现 LX Music 的大部分功能,并额外支持 WebDAV 歌曲播放、WebDAV 数据同步、独立播放列表等功能。
> 我们以后的开发精力将主要集中在新项目上,之前大家在 LX Music 提的功能我们也会考虑在新项目中添加。
> 对于日常使用 LX Music 的人可以试试迁移到 Any Listen若遇到任何问题可以发 issue 反馈。
>
> 关于我们开发新项目的原因,可以参考:[LX Music 项目发展调整与新项目计划](https://github.com/lyswhut/lx-music-desktop/issues/1912)。
软件变化请查看[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)。
软件下载请查看 [GitHub Releases](https://github.com/lyswhut/lx-music-desktop/releases)。

View File

@@ -1,9 +1,12 @@
const { afterPack } = require('./deps')
const fs = require('fs').promises
// https://github.com/electron-userland/electron-builder/issues/4630
// https://github.com/electron-userland/electron-builder/issues/4630#issuecomment-782020139
module.exports = async(context) => {
await afterPack()
const { electronPlatformName, appOutDir } = context
if (electronPlatformName !== 'darwin') return
const {

View File

@@ -1,79 +1,60 @@
const fs = require('fs')
const fsPromises = require('fs').promises
const path = require('path')
// const fs = require('fs')
// const fsPromises = require('fs').promises
// const path = require('path')
const { Arch } = require('electron-builder')
const nodeAbi = require('node-abi')
// const nodeAbi = require('node-abi')
const { beforePack, copyLib } = require('./deps')
const better_sqlite3_fileNameMap = {
[Arch.x64]: 'linux-x64',
[Arch.arm64]: 'linux-arm64',
[Arch.armv7l]: 'linux-arm',
// const better_sqlite3_fileNameMap = {
// [Arch.x64]: 'linux-x64',
// [Arch.arm64]: 'linux-arm64',
// [Arch.armv7l]: 'linux-arm',
// }
// const replaceSqliteLib = async(arch) => {
// // console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))
// // if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return
// // https://github.com/lyswhut/lx-music-desktop/issues/1102
// // https://github.com/lyswhut/lx-music-desktop/issues/1161
// console.log('replace sqlite lib...')
// const filePath = path.join(__dirname, `./lib/better_sqlite3_${better_sqlite3_fileNameMap[arch]}.node`)
// console.log(filePath)
// const targetPath = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')
// await fsPromises.unlink(targetPath).catch(_ => _)
// await fsPromises.copyFile(filePath, targetPath)
// }
const archMap = {
[Arch.x64]: 'x64',
[Arch.ia32]: 'ia32',
[Arch.arm64]: 'arm64',
[Arch.armv7l]: 'arm',
}
const qrc_decode_fileNameMap = {
win32: {
[Arch.x64]: 'win32-x64',
[Arch.ia32]: 'win32-ia32',
[Arch.arm64]: 'win32-arm64',
},
linux: {
[Arch.x64]: 'linux-x64',
[Arch.arm64]: 'linux-arm64',
[Arch.armv7l]: 'linux-arm',
},
darwin: {
[Arch.x64]: 'darwin-x64',
[Arch.arm64]: 'darwin-arm64',
},
}
const replaceSqliteLib = async(electronNodeAbi, arch) => {
// console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))
// if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return
// https://github.com/lyswhut/lx-music-desktop/issues/1102
// https://github.com/lyswhut/lx-music-desktop/issues/1161
console.log('replace sqlite lib...')
const filePath = path.join(__dirname, `./lib/better_sqlite3_electron-v${electronNodeAbi}-${better_sqlite3_fileNameMap[arch]}.node`)
console.log(filePath)
const targetPath = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')
await fsPromises.unlink(targetPath).catch(_ => _)
await fsPromises.copyFile(filePath, targetPath)
}
const replaceQrcDecodeLib = async(electronNodeAbi, platform, arch) => {
console.log('replace qrc_decode lib...', platform, electronNodeAbi, qrc_decode_fileNameMap[platform][arch])
const filePath = path.join(__dirname, `./lib/qrc_decode_electron-v${electronNodeAbi}-${qrc_decode_fileNameMap[platform][arch]}.node`)
const targetPath = path.join(__dirname, '../build/Release/qrc_decode.node')
const targetDir = path.dirname(targetPath)
if (fs.existsSync(targetDir)) await fsPromises.unlink(targetPath).catch(_ => _)
else await fsPromises.mkdir(targetDir, { recursive: true })
await fsPromises.copyFile(filePath, targetPath)
}
module.exports = async(context) => {
const { electronPlatformName, arch } = context
await beforePack()
const { arch } = context
const electronVersion = context.packager?.info?._framework?.version ?? require('../package.json').devDependencies.electron.replace(/^[^\d]*?(\d+)/, '$1')
const electronNodeAbi = nodeAbi.getAbi(electronVersion, 'electron')
await replaceQrcDecodeLib(electronNodeAbi, electronPlatformName, arch)
if (electronPlatformName !== 'linux' || process.env.FORCE) return
const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')
const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')
switch (arch) {
case Arch.x64:
case Arch.arm64:
case Arch.armv7l:
if (fs.existsSync(bindingFilePath)) {
// console.log('rename binding file...')
await fsPromises.rename(bindingFilePath, bindingBakFilePath)
}
await replaceSqliteLib(electronNodeAbi, arch)
break
await copyLib(archMap[arch], parseInt(electronVersion) == 22)
// const electronNodeAbi = nodeAbi.getAbi(electronVersion, 'electron')
// if (electronPlatformName !== 'linux' || process.env.FORCE) return
// // const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')
// // const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')
// switch (arch) {
// case Arch.x64:
// case Arch.arm64:
// case Arch.armv7l:
// // if (fs.existsSync(bindingFilePath)) {
// // // console.log('rename binding file...')
// // await fsPromises.rename(bindingFilePath, bindingBakFilePath)
// // }
// await replaceSqliteLib(arch)
// break
default:
if (fs.existsSync(bindingFilePath)) return
// console.log('restore binding file...')
await fsPromises.rename(bindingBakFilePath, bindingFilePath)
break
}
// default:
// // if (fs.existsSync(bindingFilePath)) return
// // console.log('restore binding file...')
// // await fsPromises.rename(bindingBakFilePath, bindingFilePath)
// await copyLib(arch)
// break
// }
}

View File

@@ -35,7 +35,6 @@ const options = {
'node_modules/node-gyp-build',
'node_modules/bufferutil',
'node_modules/utf-8-validate',
'build/Release/qrc_decode.node',
'dist/**/*',
],
asar: {

View File

@@ -6,20 +6,28 @@ const path = require('node:path')
const rootPath = path.join(__dirname, '../')
const patchs = [
// [
// path.join(rootPath, './node_modules/ws/package.json'),
// '\n "browser": "./browser.js",',
// '',
// ],
// [
// path.join(rootPath, './node_modules/music-metadata/package.json'),
// '"default": "./lib/core.js"',
// '"default": "./lib/index.js"',
// ],
// [
// path.join(rootPath, './node_modules/strtok3/package.json'),
// '"default": "./lib/core.js"',
// '"default": "./lib/index.js"',
// ],
[
path.join(rootPath, './node_modules/ws/package.json'),
'\n "browser": "./browser.js",',
'',
],
[
path.join(rootPath, './node_modules/music-metadata/package.json'),
'"default": "./lib/core.js"',
'"default": "./lib/index.js"',
],
[
path.join(rootPath, './node_modules/strtok3/package.json'),
'"default": "./lib/core.js"',
'"default": "./lib/index.js"',
path.join(rootPath, './node_modules/better-sqlite3/package.json'),
`{
"build-release": "node-gyp clean && node-gyp rebuild --release --force_build=1",`,
`{
"install": "node -e \\"process.exit(require('fs').existsSync('build/Release/better_sqlite3.node') ? 0 : 1)\\" || node-gyp rebuild --release --force_build=1",
"build-release": "node-gyp clean && node-gyp rebuild --release --force_build=1",`,
],
]

55
build-config/deps.js Normal file
View File

@@ -0,0 +1,55 @@
const fs = require('fs')
const path = require('path')
const bindingFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp')
const bindingBakFilePath = path.join(__dirname, '../node_modules/better-sqlite3/binding.gyp.bak')
exports.beforePack = async() => {
if (!fs.existsSync(bindingFilePath)) return
fs.renameSync(bindingFilePath, bindingBakFilePath)
// try {
// fs.writeFileSync(
// bindingFilePath,
// fs.readFileSync(bindingFilePath, 'utf-8').replace('\'force_build%\': 0,', '\'force_build%\': 1,'),
// )
// } catch (error) {
// console.error(error)
// }
}
exports.afterPack = async() => {
if (fs.existsSync(bindingFilePath)) return
fs.renameSync(bindingBakFilePath, bindingFilePath)
// try {
// fs.writeFileSync(
// bindingFilePath,
// fs.readFileSync(bindingFilePath, 'utf-8').replace('\'force_build%\': 1,', '\'force_build%\': 0,'),
// )
// } catch (error) {
// console.error(error)
// }
}
const replaceSqliteLib = async(arch) => {
// console.log(await fs.readdir(path.join(context.appOutDir, './resources/')))
// if (context.electronPlatformName != 'linux' || context.arch != Arch.arm64) return
// https://github.com/lyswhut/lx-music-desktop/issues/1102
// https://github.com/lyswhut/lx-music-desktop/issues/1161
console.log('replace sqlite lib...')
const filePath = path.join(__dirname, `./lib/better_sqlite3_${process.platform}-${arch}.node`)
console.log(filePath)
const targetPath = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')
await fs.promises.unlink(targetPath).catch(_ => _)
await fs.promises.copyFile(filePath, targetPath)
}
exports.copyLib = async(arch = process.arch, replaceLocal = false) => {
if (process.platform === 'linux' || replaceLocal) {
await replaceSqliteLib(arch)
return
}
const libPath = path.join(__dirname, `../node_modules/better-sqlite3/prebuilds/${process.platform}-${arch}.node`)
if (!fs.existsSync(libPath)) {
console.error(`Better-sqlite3 prebuild not found for ${process.platform}-${arch}`)
return
}
const targetPath = path.join(__dirname, '../node_modules/better-sqlite3/build/Release/better_sqlite3.node')
await fs.promises.cp(libPath, targetPath, { recursive: true, force: true })
}

View File

@@ -9,7 +9,7 @@ const getGzipFiles = async() => {
// for (const name of names) {
// if (name.endsWith('.node')) await fs.promises.unlink(path.join(libDir, name))
// }
return names.filter(name => name.endsWith('.gz'))
return names.filter((name) => name.endsWith('.gz'))
}
const unzip = async(filePath) => {
@@ -18,23 +18,20 @@ const unzip = async(filePath) => {
await fs.promises.mkdir(targetDir)
await tar.x({
file: filePath,
strip: 1,
strip: 0,
C: targetDir,
})
return targetDir
}
const files = [
'qrc_decode',
'better_sqlite3',
]
const files = ['better_sqlite3']
const moveFile = async(filePath) => {
const name = 'electron-' + path.basename(filePath).split('-electron-')[1]
const name = `${path.basename(filePath).split('_v')[0].replace('_', '-')}`
for (const fileName of files) {
if (fileName == 'better_sqlite3' && !name.includes('linux')) continue
// if (fileName == 'better_sqlite3' && !name.includes('linux')) continue
const targetPath = path.join(libDir, `${fileName}_${name}.node`)
if (fs.existsSync(targetPath)) await fs.promises.unlink(targetPath)
await fs.promises.rename(path.join(filePath, 'Release', fileName + '.node'), targetPath)
await fs.promises.rename(path.join(filePath, `${fileName}.node`), targetPath)
}
await fs.promises.rm(filePath, { recursive: true })
}
@@ -50,4 +47,3 @@ const run = async() => {
}
run()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,8 +1,6 @@
const path = require('path')
const ESLintPlugin = require('eslint-webpack-plugin')
const isDev = process.env.NODE_ENV === 'development'
module.exports = {
target: 'electron-main',
output: {
@@ -18,7 +16,6 @@ module.exports = {
'electron-font-manager': 'electron-font-manager',
bufferutil: 'bufferutil',
'utf-8-validate': 'utf-8-validate',
'qrc_decode.node': isDev ? path.join(__dirname, '../../build/Release/qrc_decode.node') : path.join('../build/Release/qrc_decode.node'),
},
resolve: {
alias: {

View File

@@ -37,6 +37,7 @@ module.exports = merge(baseConfig, {
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',
BUILD_WIN7: `'${process.env.BUILD_WIN7}'`,
},
}),
],

View File

@@ -0,0 +1,3 @@
const { copyLib } = require('./deps')
copyLib()

View File

@@ -19,15 +19,6 @@ module.exports = merge(baseConfig, {
externals: [
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
],
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new webpack.DefinePlugin({
'process.env': {

View File

@@ -18,15 +18,6 @@ module.exports = merge(baseConfig, {
externals: [
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
],
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new webpack.DefinePlugin({
'process.env': {

View File

@@ -19,7 +19,13 @@ const gitInfo = {
}
try {
if (!execSync('git status --porcelain').toString().trim()) {
let isClean = !execSync('git status --porcelain').toString().trim()
if (process.env.BUILD_WIN7) {
console.warn('BUILD_WIN7 is set, skipping git status check.')
console.log('Workspace status:', execSync('git status --porcelain').toString().trim())
isClean = true
}
if (isClean) {
gitInfo.commit_id = execSync('git log -1 --pretty=format:"%H"').toString().trim()
gitInfo.commit_date = execSync('git log -1 --pretty=format:"%ad" --date=iso-strict').toString().trim()
} else if (process.env.IS_CI) {
@@ -33,15 +39,6 @@ module.exports = merge(baseConfig, {
externals: [
// ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)),
],
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new CopyWebpackPlugin({
patterns: [

View File

@@ -1,12 +1,12 @@
{
"compilerOptions": {
"baseUrl": "./",
"rootDir": "./",
"paths": {
"@main/*": ["src/main/*"],
"@renderer/*": ["src/renderer/*"],
"@lyric/*": ["src/renderer-lyric/*"],
"@static/*": ["src/static/*"],
"@common/*": ["src/common/*"],
"@main/*": ["./src/main/*"],
"@renderer/*": ["./src/renderer/*"],
"@lyric/*": ["./src/renderer-lyric/*"],
"@static/*": ["./src/static/*"],
"@common/*": ["./src/common/*"],
}
},
"vueCompilerOptions": {

9431
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "2.11.0",
"version": "2.12.6-beta.2",
"description": "一个免费的音乐查找助手",
"main": "./dist/main.js",
"scripts": {
@@ -63,7 +63,7 @@
"build:renderer-scripts": "cross-env NODE_ENV=production webpack --config build-config/renderer-scripts/webpack.config.prod.js --progress",
"lint": "eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly src",
"lint:fix": "eslint --ext .ts,.js,.vue -f node_modules/eslint-formatter-friendly --fix src",
"postinstall": "electron-builder install-app-deps",
"postinstall": "node build-config/postinstall.js",
"dp": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:2081 npm run pack",
"up": "cross-env ELECTRON_GET_USE_PROXY=true GLOBAL_AGENT_HTTPS_PROXY=http://127.0.0.1:2081 npm i"
},
@@ -107,97 +107,87 @@
},
"homepage": "https://github.com/lyswhut/lx-music-desktop#readme",
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/eslint-parser": "^7.27.1",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-modules-umd": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@babel/runtime": "^7.27.1",
"@tsconfig/recommended": "^1.0.8",
"@tsconfig/recommended": "^1.0.13",
"@types/better-sqlite3": "^7.6.13",
"@types/needle": "^3.3.0",
"@types/node": "^20.17.32",
"@types/node": "^20.19.43",
"@types/tunnel": "^0.0.7",
"@types/ws": "8.5.4",
"@volar/vue-language-plugin-pug": "^1.6.5",
"@vue/language-plugin-pug": "^2.2.10",
"babel-loader": "^10.0.0",
"browserslist": "^4.24.4",
"@vue/language-plugin-pug": "^3.3.11",
"browserslist": "^4.28.9",
"chalk": "^4.1.2",
"changelog-parser": "^3.0.1",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.42.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
"changelog-parser": "3.0.1",
"copy-webpack-plugin": "^14.0.0",
"core-js": "^3.50.0",
"cross-env": "^10.1.0",
"css-loader": "^7.1.5",
"css-minimizer-webpack-plugin": "^8.0.0",
"del": "^6.1.1",
"electron": "^35.2.2",
"electron-builder": "^26.0.14",
"electron": "42.11.3",
"electron-builder": "^26.15.7",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "github:lyswhut/electron-devtools-installer#64596d615c1fc891eefd8aef1dfcb2c87aaadf03",
"electron-to-chromium": "^1.5.148",
"electron-updater": "6.6.3",
"electron-to-chromium": "^1.5.427",
"electron-updater": "6.8.9",
"eslint": "^8.57.1",
"eslint-config-standard": "^17.1.0",
"eslint-config-standard-with-typescript": "^43.0.1",
"eslint-formatter-friendly": "github:lyswhut/eslint-friendly-formatter#2170d1320e2fad13615a9dcf229669f0bb473a53",
"eslint-plugin-html": "^8.1.2",
"eslint-plugin-html": "^8.2.0",
"eslint-plugin-vue": "^9.33.0",
"eslint-plugin-vue-pug": "^0.6.2",
"eslint-webpack-plugin": "^4.2.0",
"html-webpack-plugin": "^5.6.3",
"less": "^4.3.0",
"less-loader": "^12.2.0",
"mini-css-extract-plugin": "^2.9.2",
"html-webpack-plugin": "^5.6.8",
"less": "^4.9.1",
"less-loader": "^13.0.0",
"mini-css-extract-plugin": "^2.10.2",
"node-loader": "^2.1.0",
"postcss": "^8.5.3",
"postcss-loader": "^8.1.1",
"postcss": "^8.5.28",
"postcss-loader": "^8.2.1",
"postcss-pxtorem": "^6.1.0",
"pug": "^3.0.3",
"pug": "^3.0.4",
"pug-plain-loader": "^1.1.0",
"rimraf": "^6.0.1",
"rimraf": "^6.1.3",
"spinnies": "github:lyswhut/spinnies#233305c58694aa3b053e3ab9af9049993f918b9d",
"svg-sprite-loader": "^6.0.11",
"svg-transform-loader": "^2.0.13",
"svgo-loader": "^4.0.0",
"terser": "^5.39.0",
"terser-webpack-plugin": "^5.3.14",
"svgo-loader": "^5.0.0",
"terser": "^5.51.2",
"terser-webpack-plugin": "^5.6.1",
"tree-kill": "^1.2.2",
"ts-loader": "^9.5.2",
"typescript": "5.8.3",
"ts-loader": "^9.6.2",
"typescript": "5.9.3",
"vue-eslint-parser": "^9.4.3",
"vue-loader": "^17.4.2",
"webpack": "^5.99.7",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "5.2.1",
"webpack": "5.106.2",
"webpack-cli": "^7.2.3",
"webpack-dev-server": "5.2.6",
"webpack-hot-middleware": "github:lyswhut/webpack-hot-middleware#329c4375134b89d39da23a56a94db651247c74a1",
"webpack-merge": "^6.0.1"
},
"dependencies": {
"@simonwep/pickr": "^1.9.1",
"better-sqlite3": "^11.9.1",
"bufferutil": "^4.0.9",
"@simonwep/pickr": "1.9.1",
"better-sqlite3": "^13.0.3",
"bufferutil": "^4.1.0",
"comlink": "~4.3.1",
"crypto-js": "^4.2.0",
"electron-log": "^5.3.4",
"font-list": "^1.5.1",
"iconv-lite": "^0.6.3",
"electron-log": "^5.4.4",
"font-list": "^2.1.0",
"iconv-lite": "^0.7.3",
"image-size": "^1.1.0",
"jschardet": "^3.1.4",
"long": "^5.3.2",
"message2call": "^0.1.3",
"music-metadata": "^11.2.1",
"music-metadata": "^11.15.0",
"needle": "github:lyswhut/needle#93299ac841b7e9a9f82ca7279b88aaaeda404060",
"node-id3": "^0.2.9",
"sortablejs": "^1.15.6",
"sortablejs": "^1.15.7",
"tunnel": "^0.0.6",
"utf-8-validate": "^6.0.5",
"undici": "^7.29.1",
"utf-8-validate": "^6.0.6",
"vue": "~3.3.13",
"vue-router": "^4.5.1",
"ws": "^8.18.1"
"vue-router": "~4.5.1",
"ws": "^8.21.3"
},
"overrides": {
"got": "^11",
@@ -205,18 +195,29 @@
"node-abi": "latest",
"minimatch": "latest",
"semver": "latest",
"posthtml": "latest",
"node-gyp": "latest",
"sockjs": {
"uuid": "^12"
},
"svg-transform-loader": {
"postcss": "latest"
"postcss": "^8"
},
"svg-sprite-loader": {
"postcss": "latest"
"postcss": "^8"
},
"svg-baker": {
"postcss": "latest"
"postcss": "^8"
},
"braces": "latest",
"node-gyp-build": "latest",
"micromatch": "latest",
"http-cache-semantics": "latest"
},
"allowScripts": {
"bufferutil": false,
"core-js": false,
"electron-winstaller": false,
"utf-8-validate": false
}
}

View File

@@ -1,32 +1,15 @@
### 新增
- 新增「快进/快退5秒」自定义快捷键设置#2289
- 新增「设置 → 桌面歌词设置 → 暂停时提高歌词透明度」设置,默认启用(#2294
### 修复
- 修复 Windows 下桌面歌词最小高度与宽度设置问题(#2244
- 修复 Windows 下界面缩放后移动桌面歌词会改变歌词窗口大小的问题(#2244
- 修复 tx 歌单搜索名字、描述出现乱码的问题(#2250
- 修复本地 FLAC 文件内嵌歌词无法读取的问题
- 修复潜在播放暂停的问题
- 修复 kw 歌单详情出现打开失败的问题(#2317
- 修复 kg 热门评论无法获取的问题
- 修复桌面歌词被遮挡时会被暂停的问题(#2320
- 修复 kg 歌单打开失败的问题thanks @Folltoshe
目前新项目 Any Listen 的桌面版、Web 版已实现 LX Music 的大部分功能,并额外支持 WebDAV 歌曲播放、WebDAV 数据同步、独立播放列表等功能。
以后的开发精力将主要集中在新项目上,之前大家在 LX Music 提的功能我们也会考虑在新项目中添加。
对于日常使用 LX Music 的人可以试试迁移到 Any Listen若遇到任何问题可以发 issue 反馈。
Any Listen 的项目地址为 https://github.com/any-listen/any-listen
### 优化
- 允许更小的桌面歌词窗口宽度
- 允许拖动桌面歌词控制栏空白处移动歌词窗口(#2280
- 优化「自定义源管理」对话框在小窗口下的布局(#2247, @3gf8jv4dv
- 优化软件文案编排(#2259, #2266, #2269, #2296, @3gf8jv4dv
- 优化 tx 推荐歌单列表
- 优化禁用透明窗口的窗口边框显示效果
### 变更
### 修复
- 我的列表-歌曲菜单中的 歌曲换源 功能从之前的类似软连接的形式改成替换歌曲的形式,也就是说,现在该功能相当于快速在线搜索歌曲,确认换源后将自动将原来的歌曲删除再将选择的歌曲插入被删除歌曲的位置。
### 其他
- 更新项目文档(@3gf8jv4dv
- 更新 Electron 到 35.2.2
- 修复打开某些 kg 歌单时歌曲丢失的问题
- 修复 Windows 7 无法启动的问题
- 修复禁用透明窗口时按 F11 无法全屏的问题

View File

@@ -34,7 +34,7 @@ function fm(value) {
exports.sizeFormate = size => {
// https://gist.github.com/thomseddon/3511330
if (!size) return '0 b'
let units = ['b', 'kB', 'MB', 'GB', 'TB']
let units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
let number = Math.floor(Math.log(size) / Math.log(1024))
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
}

View File

@@ -0,0 +1,40 @@
/**
*
* @param {string} text
* @returns
*/
export const parseChangelog = async(text) => {
const versions = []
const lines = text.split(/\r\n|\r|\n/)
let currentVersion = null
let currentDate = null
let currentDesc = ''
for (const line of lines) {
const versionMatch = line.match(/^\s*##\s+\[?(\d+\.\d+\.\d+)\]?.*?-\s+(\d{4}-\d{2}-\d{2})$/)
if (versionMatch) {
if (currentVersion) {
versions.push({
version: currentVersion,
date: currentDate,
desc: currentDesc.trim(),
})
}
currentVersion = versionMatch[1]
currentDate = versionMatch[3]
currentDesc = ''
} else {
currentDesc += `${line}\n`
}
}
if (currentVersion) {
versions.push({
version: currentVersion,
date: currentDate,
desc: currentDesc.trim(),
})
}
return versions
}

View File

@@ -6,8 +6,8 @@ const version = require('../version.json')
const chalk = require('chalk')
const pkg_bak = JSON.stringify(pkg, null, 2)
const version_bak = JSON.stringify(version, null, 2)
const parseChangelog = require('changelog-parser')
const changelogPath = jp('../../CHANGELOG.md')
const { parseChangelog } = require('./parseChangelog')
// const md_renderer = markdownStr => new (require('markdown-it'))({
// html: true,
@@ -16,9 +16,9 @@ const changelogPath = jp('../../CHANGELOG.md')
// breaks: true,
// }).render(markdownStr)
const getPrevVer = () => parseChangelog(changelogPath).then(res => {
if (!res.versions.length) throw new Error('CHANGELOG 无法解析到版本号')
return res.versions[0].version
const getPrevVer = () => parseChangelog(fs.readFileSync(changelogPath, 'utf-8').toString()).then(versions => {
if (!versions.length) throw new Error('CHANGELOG 无法解析到版本号')
return versions[0].version
})
const updateChangeLog = async(newVerNum, newChangeLog) => {
@@ -32,11 +32,7 @@ const updateChangeLog = async(newVerNum, newChangeLog) => {
module.exports = async newVerNum => {
if (!newVerNum) {
let verArr = pkg.version.split('.')
verArr[verArr.length - 1] = parseInt(verArr[verArr.length - 1]) + 1
newVerNum = verArr.join('.')
}
if (!newVerNum) newVerNum = pkg.version
const newMDChangeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8')
// const newChangeLog = renderChangeLog(newMDChangeLog)
version.history.unshift({

File diff suppressed because one or more lines are too long

View File

@@ -19,6 +19,7 @@ const defaultSetting: LX.AppSetting = {
'common.isAgreePact': false,
'common.controlBtnPosition': isMac ? 'left' : 'right',
'common.playBarProgressStyle': 'mini',
'common.transparentWindow': !isMac,
'common.tryAutoUpdate': true,
'common.showChangeLog': true,
@@ -37,6 +38,7 @@ const defaultSetting: LX.AppSetting = {
'player.isMediaDeviceRemovedStopPlay': false,
'player.isShowLyricTranslation': false,
'player.isShowLyricRoma': false,
'player.isSwapLyricTranslationAndRoma': false,
'player.isS2t': false,
'player.isPlayLxlrc': !isMac,
'player.isSavePlayTime': false,
@@ -114,11 +116,13 @@ const defaultSetting: LX.AppSetting = {
'download.maxDownloadNum': 3,
'download.skipExistFile': true,
'download.isDownloadLrc': false,
'download.isDownloadLxLrc': true,
'download.isDownloadTLrc': false,
'download.isDownloadRLrc': false,
'download.lrcFormat': 'utf8',
'download.isEmbedPic': true,
'download.isEmbedLyric': false,
'download.isEmbedLyricLx': true,
'download.isEmbedLyricT': false,
'download.isEmbedLyricR': false,
'download.isUseOtherSource': false,

View File

@@ -78,10 +78,6 @@ const modules = {
restart_window: 'restart_window',
// lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
handle_tx_decode_lyric: 'handle_tx_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
set_config: 'set_config',
@@ -166,6 +162,7 @@ const modules = {
key_down: 'key_down',
request_main_window_channel: 'request_main_window_channel',
provide_main_window_channel: 'provide_main_window_channel',
mouse_enter_leave: 'mouse_enter_leave',
},
hotKey: {
enable: 'enable',

View File

@@ -5,7 +5,7 @@
"./types"
],
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@common/*": ["common/*"],
"@common/*": ["./*"],
},
},
}

View File

@@ -68,6 +68,11 @@ declare global {
*/
'common.playBarProgressStyle': 'mini' | 'full' | 'middle'
/**
* 启用透明窗口
*/
'common.transparentWindow': boolean
/**
* 尝试自动更新
*/
@@ -154,6 +159,11 @@ declare global {
*/
'player.isShowLyricRoma': boolean
/**
* 是否调换翻译歌词与罗马音歌词位置
*/
'player.isSwapLyricTranslationAndRoma': boolean
/**
* 是否将歌词从简体转换为繁体
*/
@@ -520,6 +530,11 @@ declare global {
*/
'download.isDownloadLrc': boolean
/**
* 是否在下载 lx 歌词
*/
'download.isDownloadLxLrc': boolean
/**
* 是否下载翻译歌词文件
*/
@@ -540,6 +555,11 @@ declare global {
*/
'download.isEmbedPic': boolean
/**
* 是否在音频文件中嵌入 lx 歌词
*/
'download.isEmbedLyricLx': boolean
/**
* 是否在音频文件中嵌入歌词
*/

View File

@@ -37,6 +37,11 @@ declare namespace LX {
*/
play?: string
/**
* 启动后最小化到系统托盘
*/
hidden?: boolean
[key: string]: boolean | number | string
}

View File

@@ -34,6 +34,7 @@ declare namespace LX {
'common.langId': LX.AppSetting['common.langId']
'player.isShowLyricTranslation': LX.AppSetting['player.isShowLyricTranslation']
'player.isShowLyricRoma': LX.AppSetting['player.isShowLyricRoma']
'player.isSwapLyricTranslationAndRoma': LX.AppSetting['player.isSwapLyricTranslationAndRoma']
'player.isPlayLxlrc': LX.AppSetting['player.isPlayLxlrc']
'player.playbackRate': LX.AppSetting['player.playbackRate']
}

View File

@@ -58,6 +58,7 @@ declare namespace LX {
qualitys: MusicQualityTypeKg[]
_qualitys: _MusicQualityTypeKg
hash: string // 歌曲hash
albumAudioId?: string // 专辑内歌曲ID(MixSongID),用于歌词精确搜索
}
interface MusicInfo_kg extends MusicInfoBase<'kg'> {
meta: MusicInfoMeta_kg

View File

@@ -11,6 +11,9 @@ declare namespace LX {
| 'pause'
| 'play'
| 'next'
| 'seek'
| 'volume'
| 'mute'
interface LyricInfo extends LX.Music.LyricInfo {
rawlrcInfo: LX.Music.LyricInfo
@@ -28,7 +31,12 @@ declare namespace LX {
lyricLineText: string
lyricLineAllText: string
lyric: string
tlyric: string
rlyric: string
lxlyric: string
collect: boolean
volume: number
mute: boolean
}
}
}

View File

@@ -1,5 +1,6 @@
// 非业务工具方法
import { pathToFileURL } from 'url'
/**
* 获取两个数之间的随机整数大于等于min小于max
* @param {*} min
@@ -11,7 +12,7 @@ export const getRandom = (min: number, max: number): number => Math.floor(Math.r
export const sizeFormate = (size: number): string => {
// https://gist.github.com/thomseddon/3511330
if (!size) return '0 B'
let units = ['B', 'KB', 'MB', 'GB', 'TB']
let units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
let number = Math.floor(Math.log(size) / Math.log(1024))
return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}`
}
@@ -188,7 +189,7 @@ export const sortInsert = <T>(arr: Array<{ num: number, data: T }>, data: { num:
}
export const encodePath = (path: string) => {
return encodeURI(path.replaceAll('\\', '/'))
return pathToFileURL(path).href
}

View File

@@ -230,6 +230,11 @@ class Task extends EventEmitter {
__handleComplete() {
if (this.status == STATUS.error) return
this.__clearTimeout()
if (this.progress.progress <= 0) {
this.status = STATUS.error
this.emit('error', new Error('Progress is 0, download failed.'))
return
}
void this.__closeWriteStream().then(() => {
if (this.progress.downloaded == this.progress.total) {
this.status = STATUS.completed

View File

@@ -1,4 +1,5 @@
import log from 'electron-log/node'
import os from 'node:os'
export const isLinux = process.platform == 'linux'
@@ -13,7 +14,12 @@ export const getPlatform = (platform: NodeJS.Platform = process.platform) => {
default: return 'linux'
}
}
export const getOSVersion = (): string => {
const release = os.release()
const versionMatch = /^(\d+)/.exec(release)
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
return versionMatch?.[1] || release
}
// https://stackoverflow.com/a/53387532
export function compareVer(currentVer: string, targetVer: string): -1 | 0 | 1 {

View File

@@ -2,6 +2,7 @@ import { getNow, TimeoutTools } from './utils'
const timeFieldExp = /^(?:\[[\d:.]+\])+/g
const timeExp = /\d{1,3}(:\d{1,3}){0,2}(?:\.\d{1,3})/g
const msTimeRxp = /\[\d{1,3}(:\d{1,3}){0,2}\.\d{3}]/
const tagRegMap = {
title: 'ti',
artist: 'ar',
@@ -82,6 +83,7 @@ export default class LinePlayer {
_initLines() {
this.lines = []
const isMsTime = msTimeRxp.test(this.lyric)
const lines = this.lyric.split(/\r\n|\r|\n/)
const linesMap = {}
for (let i = 0; i < lines.length; i++) {
@@ -103,9 +105,10 @@ export default class LinePlayer {
if (timeArr.length > 3) continue
else if (timeArr.length < 3) for (let i = 3 - timeArr.length; i--;) timeArr.unshift('0')
if (timeArr[2].indexOf('.') > -1) timeArr.splice(2, 1, ...timeArr[2].split('.'))
const msTime = timeArr[3] || '0'
linesMap[timeStr] = {
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(timeArr[3] || 0),
time: parseInt(timeArr[0]) * 60 * 60 * 1000 + parseInt(timeArr[1]) * 60 * 1000 + parseInt(timeArr[2]) * 1000 + parseInt(isMsTime ? msTime : msTime.padEnd(3, '0')),
text,
extendedLyrics: [],
}

View File

@@ -42,7 +42,7 @@ const parseLyric = str => {
let lxlyric = str.replace(/\[((\d+),\d+)\].*/g, str => {
let result = str.match(/\[((\d+),\d+)\].*/)
let time = parseInt(result[2])
let ms = time % 1000
let ms = (time % 1000).toString().padStart(3, '0')
time /= 1000
let m = parseInt(time / 60).toString().padStart(2, '0')
time %= 60

274
src/common/utils/request.ts Normal file
View File

@@ -0,0 +1,274 @@
import qs from 'node:querystring'
import {
FormData,
getGlobalDispatcher,
interceptors,
request as nodeRrequest,
ProxyAgent,
setGlobalDispatcher,
type Dispatcher,
} from 'undici'
const defaultOptions: Options = {
timeout: 15000,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
},
maxRedirect: 5,
} as const
const redirectDispatcher = interceptors.redirect({ maxRedirections: defaultOptions.maxRedirect })
const dispatchers = [
interceptors.retry({
maxRetries: 3,
minTimeout: 1000,
maxTimeout: 10000,
timeoutFactor: 2,
retryAfter: true,
}),
// interceptors.responseError(),
] as const
let proxyAgent: ProxyAgent | null = null
let globalDispatcher = getGlobalDispatcher()
const buildDispatcher = (redirectDispatcher: Dispatcher.DispatcherComposeInterceptor | null, retryNum = 3) => {
const otherInterceptors =
retryNum == 3
? dispatchers
: [
interceptors.retry({
maxRetries: retryNum,
minTimeout: 1000,
maxTimeout: 6000,
timeoutFactor: 2,
retryAfter: true,
}),
]
if (redirectDispatcher) {
return (proxyAgent ?? globalDispatcher).compose(redirectDispatcher, ...otherInterceptors)
}
return (proxyAgent ?? globalDispatcher).compose(...otherInterceptors)
}
setGlobalDispatcher(buildDispatcher(redirectDispatcher))
export const setProxy = (url?: string) => {
proxyAgent = url ? new ProxyAgent(url) : null
setGlobalDispatcher(buildDispatcher(redirectDispatcher))
}
export const setProxyByHost = (host?: string, port?: string) => {
setProxy(host ? `http://${host}:${port}` : undefined)
}
const CONTENT_TYPE = {
json: 'application/json',
form: 'application/x-www-form-urlencoded',
text: 'text/plain',
formdata: 'multipart/form-data',
xml: 'application/xml',
binary: 'application/octet-stream',
}
type ParamsData = Record<string, string | number | null | undefined | boolean>
export interface Options {
method?:
| 'GET'
| 'HEAD'
| 'POST'
| 'PUT'
| 'DELETE'
| 'OPTIONS'
| 'PATCH'
| 'PROPFIND'
| 'COPY'
| 'MOVE'
| 'MKCOL'
| 'PROPPATCH'
| 'QUOTA'
query?: ParamsData
headers?: Record<string, string | string[]>
timeout?: number
maxRedirect?: number
signal?: AbortController['signal']
json?: Record<string, unknown>
form?: ParamsData
binary?: Buffer | Uint8Array
text?: string
formdata?: Record<string, string | Buffer | Uint8Array>
xml?: string
needBody?: boolean
needRaw?: boolean
retryNum?: number
}
export interface Response<Res> {
headers: {
accept?: string | undefined
'accept-language'?: string | undefined
'accept-patch'?: string | undefined
'accept-ranges'?: string | undefined
'access-control-allow-credentials'?: string | undefined
'access-control-allow-headers'?: string | undefined
'access-control-allow-methods'?: string | undefined
'access-control-allow-origin'?: string | undefined
'access-control-expose-headers'?: string | undefined
'access-control-max-age'?: string | undefined
'access-control-request-headers'?: string | undefined
'access-control-request-method'?: string | undefined
age?: string | undefined
allow?: string | undefined
'alt-svc'?: string | undefined
authorization?: string | undefined
'cache-control'?: string | undefined
connection?: string | undefined
'content-disposition'?: string | undefined
'content-encoding'?: string | undefined
'content-language'?: string | undefined
'content-length'?: string | undefined
'content-location'?: string | undefined
'content-range'?: string | undefined
'content-type'?: string | undefined
cookie?: string | undefined
date?: string | undefined
etag?: string | undefined
expect?: string | undefined
expires?: string | undefined
forwarded?: string | undefined
from?: string | undefined
host?: string | undefined
'if-match'?: string | undefined
'if-modified-since'?: string | undefined
'if-none-match'?: string | undefined
'if-unmodified-since'?: string | undefined
'last-modified'?: string | undefined
location?: string | undefined
origin?: string | undefined
pragma?: string | undefined
'proxy-authenticate'?: string | undefined
'proxy-authorization'?: string | undefined
'public-key-pins'?: string | undefined
range?: string | undefined
referer?: string | undefined
'retry-after'?: string | undefined
'sec-websocket-accept'?: string | undefined
'sec-websocket-extensions'?: string | undefined
'sec-websocket-key'?: string | undefined
'sec-websocket-protocol'?: string | undefined
'sec-websocket-version'?: string | undefined
'set-cookie'?: string[] | undefined
'strict-transport-security'?: string | undefined
tk?: string | undefined
trailer?: string | undefined
'transfer-encoding'?: string | undefined
upgrade?: string | undefined
'user-agent'?: string | undefined
vary?: string | undefined
via?: string | undefined
warning?: string | undefined
'www-authenticate'?: string | undefined
}
body: Res
raw: Uint8Array
statusCode?: number
statusMessage?: string
}
const buildRequestBody = (options: Options) => {
let contentType: string | undefined
let body: string | Buffer | Uint8Array | FormData | undefined
if (options.json != null) {
contentType = CONTENT_TYPE.json
body = JSON.stringify(options.json)
} else if (options.form != null) {
contentType = CONTENT_TYPE.form
body = qs.stringify(options.form)
} else if (options.binary != null) {
contentType = CONTENT_TYPE.binary
body = options.binary
} else if (options.formdata != null) {
const formdata = new FormData()
// eslint-disable-next-line guard-for-in
for (const key in options.formdata) {
formdata.append(key, options.formdata[key])
}
contentType = CONTENT_TYPE.formdata
body = formdata
} else if (options.xml != null) {
contentType = CONTENT_TYPE.xml
body = options.xml
} else if (options.text != null) {
contentType = CONTENT_TYPE.text
body = options.text
}
const headers = options.headers ?? {}
if (headers['Content-Type']) {
contentType = headers['Content-Type'] as string
}
const finalHeaders: NonNullable<Options['headers']> = { ...defaultOptions.headers, ...headers }
if (contentType) finalHeaders['Content-Type'] = contentType
return [finalHeaders, body] as const
}
const buildRequestDispatcher = (options: Options) => {
let dispatcher: Dispatcher.ComposedDispatcher | undefined
if (options.maxRedirect != null) {
if (options.maxRedirect != defaultOptions.maxRedirect) {
if (options.maxRedirect) {
dispatcher = buildDispatcher(interceptors.redirect({ maxRedirections: options.maxRedirect }), options.retryNum)
} else {
dispatcher = buildDispatcher(null, options.retryNum)
}
}
}
return dispatcher
}
export const request = async <T = unknown>(url: string, options: Options = {}): Promise<Response<T>> => {
const method = options.method?.toUpperCase() ?? 'GET'
const timeout = options.timeout ?? defaultOptions.timeout
const [headers, body] = buildRequestBody(options)
// console.log(url, {
// method,
// bodyTimeout: timeout,
// headersTimeout: timeout,
// headers,
// query: options.query,
// body,
// signal: options.signal,
// dispatcher: buildRequestDispatcher(options),
// })
return nodeRrequest(url, {
method,
bodyTimeout: timeout,
headersTimeout: timeout,
headers,
query: options.query,
body,
signal: options.signal,
dispatcher: buildRequestDispatcher(options),
}).then(async(response) => {
if (options.needBody) {
return {
headers: response.headers,
statusCode: response.statusCode,
body: response.body as unknown as T,
} satisfies Omit<Response<T>, 'raw'> as Response<T>
}
if (options.needRaw) {
return {
headers: response.headers,
statusCode: response.statusCode,
raw: await response.body.bytes(),
} satisfies Omit<Response<T>, 'body'> as Response<T>
}
// console.log(response)
let body = (await response.body.text()) as T
if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) {
try {
body = JSON.parse(body as string) as T
} catch {}
}
return {
body,
headers: response.headers,
statusCode: response.statusCode,
} satisfies Omit<Response<T>, 'raw'> as Response<T>
})
}

View File

@@ -0,0 +1,238 @@
import qs from 'node:querystring'
import {
FormData,
getGlobalDispatcher,
request as nodeRrequest,
ProxyAgent,
setGlobalDispatcher,
type Dispatcher,
} from 'undici'
const defaultOptions: Options = {
timeout: 15000,
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
},
maxRedirect: 5,
} as const
let proxyAgent: ProxyAgent | null = null
let globalDispatcher = getGlobalDispatcher()
const buildDispatcher = () => {
return proxyAgent ?? globalDispatcher
}
setGlobalDispatcher(buildDispatcher())
export const setProxy = (url?: string) => {
proxyAgent = url ? new ProxyAgent(url) : null
setGlobalDispatcher(buildDispatcher())
}
export const setProxyByHost = (host?: string, port?: string) => {
console.log(host)
setProxy(host ? `http://${host}:${port}` : undefined)
}
const CONTENT_TYPE = {
json: 'application/json',
form: 'application/x-www-form-urlencoded',
text: 'text/plain',
formdata: 'multipart/form-data',
xml: 'application/xml',
binary: 'application/octet-stream',
}
type ParamsData = Record<string, string | number | null | undefined | boolean>
export interface Options {
method?:
| 'GET'
| 'HEAD'
| 'POST'
| 'PUT'
| 'DELETE'
| 'OPTIONS'
| 'PATCH'
| 'PROPFIND'
| 'COPY'
| 'MOVE'
| 'MKCOL'
| 'PROPPATCH'
| 'QUOTA'
query?: ParamsData
headers?: Record<string, string | string[]>
timeout?: number
maxRedirect?: number
signal?: AbortController['signal']
json?: Record<string, unknown>
form?: ParamsData
binary?: Buffer | Uint8Array
text?: string
formdata?: Record<string, string | Buffer | Uint8Array>
xml?: string
needBody?: boolean
needRaw?: boolean
retryNum?: number
}
export interface Response<Res> {
headers: {
accept?: string | undefined
'accept-language'?: string | undefined
'accept-patch'?: string | undefined
'accept-ranges'?: string | undefined
'access-control-allow-credentials'?: string | undefined
'access-control-allow-headers'?: string | undefined
'access-control-allow-methods'?: string | undefined
'access-control-allow-origin'?: string | undefined
'access-control-expose-headers'?: string | undefined
'access-control-max-age'?: string | undefined
'access-control-request-headers'?: string | undefined
'access-control-request-method'?: string | undefined
age?: string | undefined
allow?: string | undefined
'alt-svc'?: string | undefined
authorization?: string | undefined
'cache-control'?: string | undefined
connection?: string | undefined
'content-disposition'?: string | undefined
'content-encoding'?: string | undefined
'content-language'?: string | undefined
'content-length'?: string | undefined
'content-location'?: string | undefined
'content-range'?: string | undefined
'content-type'?: string | undefined
cookie?: string | undefined
date?: string | undefined
etag?: string | undefined
expect?: string | undefined
expires?: string | undefined
forwarded?: string | undefined
from?: string | undefined
host?: string | undefined
'if-match'?: string | undefined
'if-modified-since'?: string | undefined
'if-none-match'?: string | undefined
'if-unmodified-since'?: string | undefined
'last-modified'?: string | undefined
location?: string | undefined
origin?: string | undefined
pragma?: string | undefined
'proxy-authenticate'?: string | undefined
'proxy-authorization'?: string | undefined
'public-key-pins'?: string | undefined
range?: string | undefined
referer?: string | undefined
'retry-after'?: string | undefined
'sec-websocket-accept'?: string | undefined
'sec-websocket-extensions'?: string | undefined
'sec-websocket-key'?: string | undefined
'sec-websocket-protocol'?: string | undefined
'sec-websocket-version'?: string | undefined
'set-cookie'?: string[] | undefined
'strict-transport-security'?: string | undefined
tk?: string | undefined
trailer?: string | undefined
'transfer-encoding'?: string | undefined
upgrade?: string | undefined
'user-agent'?: string | undefined
vary?: string | undefined
via?: string | undefined
warning?: string | undefined
'www-authenticate'?: string | undefined
}
body: Res
raw: Uint8Array
statusCode?: number
statusMessage?: string
}
const buildRequestBody = (options: Options) => {
let contentType: string | undefined
let body: string | Buffer | Uint8Array | FormData | undefined
if (options.json != null) {
contentType = CONTENT_TYPE.json
body = JSON.stringify(options.json)
} else if (options.form != null) {
contentType = CONTENT_TYPE.form
body = qs.stringify(options.form)
} else if (options.binary != null) {
contentType = CONTENT_TYPE.binary
body = options.binary
} else if (options.formdata != null) {
const formdata = new FormData()
// eslint-disable-next-line guard-for-in
for (const key in options.formdata) {
formdata.append(key, options.formdata[key])
}
contentType = CONTENT_TYPE.formdata
body = formdata
} else if (options.xml != null) {
contentType = CONTENT_TYPE.xml
body = options.xml
} else if (options.text != null) {
contentType = CONTENT_TYPE.text
body = options.text
}
const headers = options.headers ?? {}
if (headers['Content-Type']) {
contentType = headers['Content-Type'] as string
}
const finalHeaders: NonNullable<Options['headers']> = { ...defaultOptions.headers, ...headers }
if (contentType) finalHeaders['Content-Type'] = contentType
return [finalHeaders, body] as const
}
const buildRequestDispatcher = (options: Options) => {
return buildDispatcher()
}
export const request = async <T = unknown>(url: string, options: Options = {}): Promise<Response<T>> => {
const method = (options.method?.toUpperCase() ?? 'GET') as Dispatcher.RequestOptions['method']
const timeout = options.timeout ?? defaultOptions.timeout
const [headers, body] = buildRequestBody(options)
// console.log(url, {
// method,
// bodyTimeout: timeout,
// headersTimeout: timeout,
// headers,
// query: options.query,
// body,
// signal: options.signal,
// dispatcher: buildRequestDispatcher(options),
// })
return nodeRrequest(url, {
method,
bodyTimeout: timeout,
headersTimeout: timeout,
headers,
query: options.query,
body,
signal: options.signal,
dispatcher: buildRequestDispatcher(options),
maxRedirections: options.maxRedirect ?? defaultOptions.maxRedirect,
}).then(async(response) => {
if (options.needBody) {
return {
headers: response.headers,
statusCode: response.statusCode,
body: response.body as unknown as T,
} satisfies Omit<Response<T>, 'raw'> as Response<T>
}
if (options.needRaw) {
return {
headers: response.headers,
statusCode: response.statusCode,
raw: new Uint8Array(await response.body.arrayBuffer()),
} satisfies Omit<Response<T>, 'body'> as unknown as Response<T>
}
// console.log(response)
let body = (await response.body.text()) as T
if (!headers['Content-Type'] || headers['Content-Type'].includes(CONTENT_TYPE.json)) {
try {
body = JSON.parse(body as string) as T
} catch {}
}
return {
body,
headers: response.headers,
statusCode: response.statusCode,
} satisfies Omit<Response<T>, 'raw'> as Response<T>
})
}

View File

@@ -35,6 +35,7 @@ export const toNewMusicInfo = (oldMusicInfo: any): LX.Music.MusicInfo => {
switch (oldMusicInfo.source) {
case 'kg':
meta.hash = oldMusicInfo.hash
meta.albumAudioId = oldMusicInfo.albumAudioId
newInfo.id = oldMusicInfo.songmid + '_' + oldMusicInfo.hash
break
case 'tx':
@@ -79,6 +80,7 @@ export const toOldMusicInfo = (minfo: LX.Music.MusicInfo) => {
switch (minfo.source) {
case 'kg':
oInfo.hash = minfo.meta.hash
oInfo.albumAudioId = minfo.meta.albumAudioId
break
case 'tx':
oInfo.strMediaMid = minfo.meta.strMediaMid
@@ -148,3 +150,7 @@ export const clipFileNameLength = (name: string) => {
return name.length > MAX_FILE_NAME_LENGTH ? name.substring(0, MAX_FILE_NAME_LENGTH) : name
}
export const formatMusicName = (format: string, name: string, singer: string) => {
// return format.replace(/歌名|歌手/g, match => match === '歌名' ? name : singer)
return format.replace('歌手', singer).replace('歌名', name)
}

View File

@@ -407,11 +407,13 @@
"setting__dislike_list_title": "Disliked Song Rule List",
"setting__download": "Download",
"setting__download_data_embed": "Embed the Following into Audio File",
"setting__download_embed_lxlyric": "Embed LX Music lyrics at the same time (if any, verbatim lyrics can be supported when playing with LX Music)",
"setting__download_embed_lyric": "Embed lyrics",
"setting__download_embed_pic": "Embed cover",
"setting__download_embed_rlyric": "Also embed romanized lyrics if available",
"setting__download_embed_tlyric": "Also embed translated lyrics if available",
"setting__download_enable": "Enable Download",
"setting__download_lxlyric": "Also write LX Music lyrics into the lyrics file (if any, verbatim lyrics can be supported when playing with LX Music)",
"setting__download_lyric": "Lyric Download",
"setting__download_lyric_format": "Encoding Format of Downloaded Lyric Files",
"setting__download_lyric_format_gbk": "GBK",
@@ -512,6 +514,8 @@
"setting__other_resource_cache_label": "The app has used cache size: ",
"setting__other_resource_cache_tip": "Includes pictures, audios, and other caches. After cleaning, these resources will need to be downloaded again. It is not recommended to clean up. The app will dynamically manage the cache size according to the disk space.",
"setting__other_resource_cache_tip_confirm": "Involving the cache of pictures, audios, etc., these resources will need to be downloaded again after cleaning. It is not recommended to clean up. The app will dynamically manage the cache size according to the disk space. Do you still need to clean up?",
"setting__other_transparent_window": "Use built-in rounded corners and shadows for the main window",
"setting__other_transparent_window_tip": "Turning off this option will use the system's native window style. Requires a restart of the software for the change to take effect.",
"setting__other_tray_theme": "Tray Icon Style",
"setting__other_tray_theme_auto": "Follow System",
"setting__other_tray_theme_black": "Black",
@@ -553,6 +557,7 @@
"setting__play_task_bar": "Show playback progress on the taskbar",
"setting__play_timeout": "Timed Pause",
"setting__player_audio_visualization_tip": "The custom audio output device feature conflicts with the audio visualization feature. After the audio visualization is enabled, the audio output device will be reset to the default. At present, this problem cannot be resolved. Do you still want to enable it?",
"setting__player_swap_lyric_trans_roma": "Swap the position of the translated and romanized lyrics",
"setting__search": "Search",
"setting__search_focus_search_box": "Automatically focus search box on startup",
"setting__search_history": "Enable Search History",
@@ -608,7 +613,7 @@
"songlist__import_input_tip_1": "Cross-service playlists are not supported. Please confirm whether the playlist to be opened corresponds to the current chosen service",
"songlist__import_input_tip_2": "If you encounter a playlist link that cannot be opened. Please send us your feedback",
"songlist__import_input_tip_3": "Unable to open Kugou playlist with playlist ID or link from lite edition, but support to open it with Kugou code or link from main edition",
"songlist__import_input_tip_4": "NetEase Cloud Music's \"I Like\" playlist requires a token to open. For details, see ",
"songlist__import_input_tip_4": "NetEases “Private Radar” playlists require a token to access. Tencents personal playlists require your profile be public to access. For details, see ",
"songlist__import_input_title": "Open shared playlist",
"songlist__open_list": "Open \"{name}\" playlist",
"songlist__tag_info_hot_tag": "Top Tags",

View File

@@ -1,14 +1,28 @@
import zh_cn from './zh-cn.json'
import zh_tw from './zh-tw.json'
import en_us from './en-us.json'
import ko_kr from './ko-kr.json'
type Message = Record<keyof typeof zh_cn, string>
| Record<keyof typeof zh_tw, string>
| Record<keyof typeof en_us, string>
| Record<keyof typeof ko_kr, string>
type Messages = Record<(typeof langs)[number]['locale'], Message>
const langs = [
{
name: 'English',
locale: 'en-us',
country: 'us',
message: en_us,
},
{
name: '한국어',
locale: 'ko-kr',
country: 'kr',
message: ko_kr,
},
{
name: '简体中文',
locale: 'zh-cn',
@@ -24,12 +38,6 @@ const langs = [
country: 'cn',
message: zh_tw,
},
{
name: 'English',
locale: 'en-us',
country: 'us',
message: en_us,
},
] as const
const langList: Array<{

748
src/lang/ko-kr.json Normal file
View File

@@ -0,0 +1,748 @@
{
"action": "동작",
"agree": "동의",
"alert_button_text": "확인",
"audio_visualization": "오디오 시각화 (실험적 기능)",
"back": "뒤로",
"btn_cancel": "취소",
"btn_close": "닫기",
"btn_confirm": "확인",
"btn_save": "저장",
"cancel_button_text": "취소",
"cancel_button_text_2": "아니요, 잘못 눌렀어요",
"close": "닫기",
"comment__hot_load_error": "인기 댓글을 불러오지 못했습니다. 클릭하여 다시 시도하세요.",
"comment__hot_loading": "인기 댓글을 불러오는 중...",
"comment__hot_title": "인기 댓글",
"comment__location": "{location}에서",
"comment__new_load_error": "최신 댓글을 불러오지 못했습니다. 클릭하여 다시 시도하세요.",
"comment__new_loading": "최신 댓글을 불러오는 중...",
"comment__new_title": "최신 댓글",
"comment__no_content": "아직 댓글이 없습니다",
"comment__refresh": "댓글 새로고침",
"comment__show": "노래 댓글",
"comment__title": "\"{name}\"의 댓글",
"comment__unavailable": "이 노래의 댓글을 가져올 수 없습니다.",
"confirm_button_text": "예",
"copy_tip": " (클릭하여 복사)",
"date_format_hour": "{num}시간 전",
"date_format_minute": "{num}분 전",
"date_format_second": "{num}초 전",
"deep_link__handle_error_tip": "호출 실패: {message}",
"default": "기본값",
"default_list": "기본 목록",
"desktop_lyric__back": "뒤로",
"desktop_lyric__close": "닫기",
"desktop_lyric__font_decrease": "글자 크기 줄이기",
"desktop_lyric__font_increase": "글자 크기 늘리기",
"desktop_lyric__lock": "잠금",
"desktop_lyric__lrc_active_zoom_off": "재생 중인 가사 확대 해제",
"desktop_lyric__lrc_active_zoom_on": "재생 중인 가사 확대",
"desktop_lyric__opacity_decrease": "투명도 높이기\n(우클릭하여 세밀 조정)",
"desktop_lyric__opacity_increase": "투명도 낮추기\n(우클릭하여 세밀 조정)",
"desktop_lyric__theme": "테마 색상",
"desktop_lyric__unlock": "잠금 해제",
"desktop_lyric__win_top_off": "항상 위 유지",
"desktop_lyric__win_top_on": "항상 위 해제",
"download": "다운로드",
"download___status_completed": "다운로드 완료",
"download___status_error": "오류",
"download___status_paused": "일시정지됨",
"download___status_running": "다운로드 중",
"download___status_waiting": "다운로드 대기 중",
"download__all": "전체 작업",
"download__error": "오류",
"download__finished": "완료",
"download__high_quality": "고음질",
"download__lossless": "무손실",
"download__multiple_tip": "{len}곡 선택됨",
"download__multiple_tip2": "원하는 다운로드 음질을 선택하세요",
"download__normal": "일반",
"download__not_available_tip": "해당 음질을 사용할 수 없습니다.",
"download__paused": "일시정지됨",
"download__progress": "진행률",
"download__quality": "음질",
"download__running": "다운로드 중",
"download__status": "상태",
"download_status_error_check_path": "다운로드 경로 확인 중 오류가 발생했습니다. 설정한 다운로드 폴더가 정상인지 확인해 주세요",
"download_status_error_check_path_exist": "동일한 이름의 파일이 존재하여 다운로드를 건너뛰었습니다",
"download_status_error_refresh_url": "링크가 만료되어 새로고침 중...",
"download_status_error_response": "다운로드 실패: ",
"download_status_error_url_failed": "노래 링크를 가져오지 못했습니다",
"download_status_error_write": "다운로드 위치가 사용 중이거나 쓰기 권한이 없습니다. 다운로드 폴더를 변경하거나 앱/기기를 재시작해 보세요. 오류 상세: ",
"download_status_start": "다운로드 시작",
"download_status_url_getting": "노래 링크를 가져오는 중...",
"download_status_write_queue": "데이터 쓰는 중 ({num})",
"duplicate_list_tip": "이미 \"{name}\" 목록을 담은 적이 있습니다. 목록의 곡을 업데이트하시겠습니까?",
"export": "내보내기",
"fullscreen_exit": "전체화면 종료",
"history_clear": "기록 지우기",
"history_remove": "우클릭하여 이 항목 삭제",
"history_search": "검색 기록",
"import": "가져오기",
"leaderboard": "차트",
"list__add_to": "추가할 위치...",
"list__collect": "담기",
"list__copy_name": "이름 복사",
"list__dislike": "싫어요",
"list__download": "다운로드",
"list__export_part_desc": "목록 파일을 저장할 위치를 선택하세요",
"list__file": "파일 위치 열기",
"list__import_part_button_cancel": "취소",
"list__import_part_button_confirm": "덮어쓰기",
"list__import_part_confirm": "가져온 목록({importName})의 ID가 로컬 목록({localName})과 동일합니다. 로컬 목록을 덮어쓰시겠습니까?",
"list__import_part_desc": "목록 파일을 선택하세요",
"list__load_failed": "이런, 불러오기에 실패했습니다 😭",
"list__loading": "목록을 불러오는 중...⏳",
"list__move_to": "이동할 위치...",
"list__movedown": "아래로 이동",
"list__moveup": "위로 이동",
"list__name_default": "기본 목록",
"list__name_love": "좋아요",
"list__new_list_btn": "새 목록",
"list__new_list_input": "새 목록...",
"list__pause": "정지",
"list__play": "재생",
"list__play_later": "다음에 재생",
"list__remove": "삭제",
"list__remove_tip": "\"{name}\"을(를) 정말 삭제하시겠습니까?",
"list__remove_tip_button": "예, 맞습니다",
"list__rename": "이름 변경",
"list__search": "검색",
"list__sort": "순서 조정",
"list__source_detail": "노래 상세정보",
"list__start": "재개",
"list__sync": "업데이트",
"list__toggle_source": "소스 변경",
"list_add__btn_title": "\"{name}\"에 곡 추가",
"list_add__multiple_btn_title": "이 곡들을 \"{name}\"에 추가",
"list_add__multiple_title_add": "선택한 {num}곡을 다음 위치에 추가...",
"list_add__multiple_title_move": "선택한 {num}곡을 다음 위치로 이동...",
"list_add__title_first_add": "추가",
"list_add__title_first_move": "이동",
"list_add__title_last": "위치로...",
"list_duplicate_tip": "동일한 이름의 목록이 이미 존재합니다. 계속 생성하시겠습니까?",
"list_import_tip__alldata": "가져오기에 실패했습니다. 이 파일은 \"전체 데이터\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n설정 → 백업 및 복원 → 전체 데이터 → 가져오기",
"list_import_tip__playlist": "가져오기에 실패했습니다. 이 파일은 \"목록\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n설정 → 백업 및 복원 → 일부 데이터 → 목록 가져오기",
"list_import_tip__playlist_part": "가져오기에 실패했습니다. 이 파일은 \"목록 전용\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n내 서재 → 아무 목록 이름에서 우클릭 → 메뉴에서 '가져오기' 선택",
"list_import_tip__setting": "가져오기에 실패했습니다. 이 파일은 \"설정\" 백업 파일입니다. 다음 위치에서 가져와야 합니다: \n\n설정 → 백업 및 복원 → 일부 데이터 → 설정 가져오기",
"list_import_tip__unknown": "가져오기에 실패했습니다. 알 수 없는 파일 형식입니다. 앱을 최신 버전으로 업데이트한 후 다시 시도해 주세요.",
"list_sort_modal_by_album": "앨범",
"list_sort_modal_by_down": "내림차순",
"list_sort_modal_by_field": "정렬 기준",
"list_sort_modal_by_name": "제목",
"list_sort_modal_by_random": "무작위",
"list_sort_modal_by_singer": "아티스트",
"list_sort_modal_by_source": "음악 서비스",
"list_sort_modal_by_time": "길이",
"list_sort_modal_by_type": "정렬 항목",
"list_sort_modal_by_up": "오름차순",
"list_sort_modal_tip_confirm": "정말로 실행하시겠습니까?",
"list_update_modal__auto_update": "자동 업데이트",
"list_update_modal__tips": "💡 \"자동 업데이트\"가 체크된 목록은 앱을 시작할 때마다 자동으로 업데이트됩니다.",
"list_update_modal__title": "목록 업데이트 관리",
"list_update_modal__update": "동기화",
"lists__add_local_file_desc": "노래 파일을 선택하세요",
"lists__dislike_music_singer_tip": "{singer}의 \"{name}\"을(를) 정말 싫어요 처리하시겠습니까?",
"lists__dislike_music_tip": "\"{name}\"을(를) 정말 싫어요 처리하시겠습니까?",
"lists__duplicate": "중복된 곡",
"lists__export": "내보내기",
"lists__export_part_desc": "목록 파일을 저장할 위치를 선택하세요",
"lists__import": "가져오기",
"lists__import_part_button_cancel": "아니요",
"lists__import_part_button_confirm": "덮어쓰기",
"lists__import_part_confirm": "가져온 목록({importName})의 ID가 로컬 목록({localName})과 동일합니다. 로컬 목록을 덮어쓰시겠습니까?",
"lists__import_part_desc": "목록 파일을 선택하세요",
"lists__new_list_btn": "목록 만들기",
"lists__new_list_input": "새 목록...",
"lists__remove": "삭제",
"lists__remove_music_tip": "선택한 {len}곡을 정말 삭제하시겠습니까?",
"lists__remove_tip": "\"{name}\"을(를) 정말 삭제하시겠습니까?",
"lists__remove_tip_button": "예, 맞습니다",
"lists__rename": "이름 변경",
"lists__select_local_file": "로컬 곡 추가",
"lists__sort_list": "곡 정렬",
"lists__source_detail": "재생목록 페이지",
"lists__sync": "업데이트",
"lists__sync_confirm_tip": "\"{name}\"의 곡을 온라인 목록의 곡으로 교체합니다. 업데이트하시겠습니까?",
"load_list_file_error_detail": "이전 목록 파일을 {path}에 백업해 두었습니다.\nJSON 형식으로 저장되어 있으며, 수동으로 복구를 시도할 수 있습니다.\n\n오류 상세: {detail}",
"load_list_file_error_title": "재생목록 데이터 로드 오류",
"loading": "불러오는 중...",
"love_list": "즐겨찾기",
"lyric__load_error": "가사를 가져오지 못했습니다",
"lyric__select": "가사 텍스트 선택 허용",
"lyric_menu__align": "가사 정렬",
"lyric_menu__align_center": "가운데",
"lyric_menu__align_left": "왼쪽",
"lyric_menu__lrc_size": "글자 크기 [{size}]",
"lyric_menu__offset": "오프셋 [{offset}ms]",
"lyric_menu__offset_add_10": "10ms 늘리기",
"lyric_menu__offset_add_100": "100ms 늘리기",
"lyric_menu__offset_dec_10": "10ms 줄이기",
"lyric_menu__offset_dec_100": "100ms 줄이기",
"lyric_menu__offset_reset": "초기화",
"lyric_menu__size_add": "글자 크기 늘리기\n(우클릭하여 세밀 조정)",
"lyric_menu__size_dec": "글자 크기 줄이기\n(우클릭하여 세밀 조정)",
"lyric_menu__size_reset": "초기화",
"media_device__empty_device_tip": "오디오 출력 장치가 비어 있습니다. 재생이 되지 않는다면 사운드 카드 드라이버가 설치되어 있는지 확인해 주세요. \n또는 소프트웨어를 제거한 후 재설치할 때 \"나만 사용\"을 선택하고 기본 설치 경로를 유지해 보세요.",
"min": "최소화",
"music_album": "앨범",
"music_duplicate": "중복된 곡",
"music_name": "제목",
"music_singer": "아티스트",
"music_sort__input_tip": "이동할 위치를 입력하세요",
"music_sort__title": "\"{name}\"의 위치를 다음으로 조정: ",
"music_sort__title_multiple": "선택한 {num}곡의 위치를 다음으로 조정: ",
"music_time": "길이",
"music_toggle_clean": "변경 취소",
"music_toggle_confirm": "확인",
"music_toggle_duplicate_tip": "목록에 동일한 곡이 이미 존재합니다. 삭제하고 계속할까요?",
"my_list": "내 서재",
"no_item": "아무것도 없습니다...",
"not_agree": "거부",
"ok": "확인",
"pagination__next": "다음 페이지",
"pagination__page": "{num} 페이지",
"pagination__prev": "이전 페이지",
"play_timeout": "타이머 정지",
"play_timeout_close": "닫기",
"play_timeout_confirm": "확인",
"play_timeout_end": "곡이 끝난 후 정지",
"play_timeout_stop": "타이머 취소",
"play_timeout_tip": "{time} 후 정지",
"play_timeout_unit": "분",
"play_timeout_update": "시간 업데이트",
"player__add_music_to": "현재 곡을 다음 위치에 추가...",
"player__buffering": "버퍼링 중...",
"player__desktop_lyric_lock": "우클릭하여 가사창 잠금",
"player__desktop_lyric_off": "가사창 숨기기",
"player__desktop_lyric_on": "가사창 표시",
"player__desktop_lyric_unlock": "우클릭하여 가사창 잠금 해제",
"player__end": "정지됨",
"player__error": "음악을 불러오는 중 오류가 발생했습니다. 5초 후 다음 곡으로 전환합니다",
"player__getting_url": "음악 링크를 가져오는 중...",
"player__getting_url_delay_retry": "서버가 혼잡합니다. {time}초 후 다시 시도합니다...",
"player__hide_detail_tip": "상세 페이지 숨기기\n(앱 창에서 우클릭하면 상세 페이지를 빠르게 숨길 수 있습니다)",
"player__loading": "음악을 불러오는 중...",
"player__music_album": "앨범: ",
"player__music_name": "제목: ",
"player__music_singer": "아티스트: ",
"player__next": "다음 곡",
"player__pause": "일시정지",
"player__pic_tip": "재생 상세 페이지\n(우클릭하면 \"내 서재\"에서 현재 재생 중인 곡을 찾을 수 있습니다)",
"player__play": "재생",
"player__play_toggle_mode_list": "순서대로",
"player__play_toggle_mode_list_loop": "목록 반복",
"player__play_toggle_mode_off": "사용 안 함",
"player__play_toggle_mode_random": "무작위 재생",
"player__play_toggle_mode_single_loop": "한 곡 반복",
"player__playback_preserves_pitch": "음정 보정",
"player__playback_rate": "현재 재생 속도: ",
"player__playback_rate_reset_btn": "초기화",
"player__playing": "재생 중...",
"player__prev": "이전 곡",
"player__refresh_url": "음악 URL이 만료되어 새로고침 중...",
"player__sound_effect": "음향 옵션 (실험적 기능)",
"player__sound_effect_biquad_filter": "이퀄라이저",
"player__sound_effect_biquad_filter_preset_classical": "클래식",
"player__sound_effect_biquad_filter_preset_dance": "댄스",
"player__sound_effect_biquad_filter_preset_electronic": "일렉트로닉",
"player__sound_effect_biquad_filter_preset_pop": "팝",
"player__sound_effect_biquad_filter_preset_rock": "록",
"player__sound_effect_biquad_filter_preset_slow": "슬로우",
"player__sound_effect_biquad_filter_preset_soft": "소프트",
"player__sound_effect_biquad_filter_preset_subwoofer": "서브우퍼",
"player__sound_effect_biquad_filter_preset_vocal": "보컬",
"player__sound_effect_biquad_filter_reset_btn": "초기화",
"player__sound_effect_biquad_filter_save_btn": "프리셋 다른 이름으로 저장",
"player__sound_effect_biquad_filter_save_input": "새 프리셋...",
"player__sound_effect_convolution": "환경 리버브 음향 효과",
"player__sound_effect_convolution_file_bright_hall": "홀",
"player__sound_effect_convolution_file_cardiod_35_10_spread": "하트형 확산",
"player__sound_effect_convolution_file_cinema_diningroom": "영화관",
"player__sound_effect_convolution_file_dining_living_true_stereo": "식당",
"player__sound_effect_convolution_file_feedback_spring": "피드백 스프링",
"player__sound_effect_convolution_file_living_bedroom_leveled": "욕실",
"player__sound_effect_convolution_file_matrix_1": "매트릭스 (1)",
"player__sound_effect_convolution_file_matrix_2": "매트릭스 (2)",
"player__sound_effect_convolution_file_s2_r4_bd": "교회",
"player__sound_effect_convolution_file_s3_r1_bd": "스테레오",
"player__sound_effect_convolution_file_spreader50_65ms": "실내",
"player__sound_effect_convolution_file_telephone": "전화기",
"player__sound_effect_convolution_file_tim_omni_35_10_magnetic": "마그네틱 스테레오",
"player__sound_effect_convolution_main_gain": "원음 게인",
"player__sound_effect_convolution_send_gain": "환경 음향 효과 게인",
"player__sound_effect_features_tip": "안내: 음향 옵션은 사용자 지정 오디오 출력 장치와 충돌합니다. 음향 옵션을 활성화하면 오디오 출력 장치가 기본 출력 장치로 재설정됩니다. 현재로서는 이 문제를 해결할 수 없습니다.",
"player__sound_effect_panner": "3D 입체 서라운드 (헤드폰 사용 필요)",
"player__sound_effect_panner_enabled": "사용",
"player__sound_effect_panner_sound_r": "음원 거리",
"player__sound_effect_panner_sound_speed": "서라운드 속도",
"player__sound_effect_pitch_shifter": "음정 조정",
"player__sound_effect_pitch_shifter_preset_semitones": "{num}반음",
"player__sound_effect_pitch_shifter_reset_btn": "초기화",
"player__sound_effect_pitch_shifter_tip": "음정을 높이거나 낮추려면 오디오 데이터를 실시간으로 처리해야 하므로 CPU 사용량이 추가로 발생합니다.\n\n알려진 문제:\nCPU 자원이 부족하면 처리 작업이 쌓여 소리에 이상이 발생할 수 있습니다.\n이 경우 재생을 잠시 멈추고 쌓인 작업이 처리될 때까지 기다린 후 재생해야 합니다.",
"player__stop": "일시정지됨",
"player__volume": "볼륨: ",
"player__volume_mute_label": "음소거",
"player__volume_muted": "음소거됨",
"search": "검색",
"search__hot_search": "인기 검색어",
"search__type_music": "노래",
"search__type_songlist": "재생목록",
"search__welcome": "원하는 것을 검색해보세요~~ 😉",
"setting": "설정",
"setting__about": "LX Music 정보",
"setting__backup": "백업 및 복원",
"setting__backup_all": "전체 데이터 (\"목록\" 데이터와 \"설정\" 데이터)",
"setting__backup_all_export": "내보내기",
"setting__backup_all_export_desc": "백업을 저장할 위치...",
"setting__backup_all_import": "가져오기",
"setting__backup_all_import_desc": "백업 파일을 선택하세요",
"setting__backup_other": "기타 백업 형식 (해당 백업 파일의 복원은 현재 지원하지 않습니다)",
"setting__backup_other_export_dir": "파일을 저장할 위치를 선택하세요",
"setting__backup_other_export_list_csv": "CSV 형식으로 목록 내보내기",
"setting__backup_other_export_list_text": "TXT 형식으로 목록 내보내기",
"setting__backup_other_export_list_text_confirm": "모든 목록을 하나의 파일로 합치시겠습니까?",
"setting__backup_part": "일부 데이터 (\"목록\" 데이터에는 \"기본\", \"좋아요\" 및 사용자가 만든 목록이 포함됩니다. \"설정\" 데이터에는 \"단축키\" 설정이 포함되지 않습니다)",
"setting__backup_part_export_list": "목록 내보내기",
"setting__backup_part_export_list_desc": "목록을 저장할 위치...",
"setting__backup_part_export_setting": "설정 내보내기",
"setting__backup_part_export_setting_desc": "설정을 저장할 위치...",
"setting__backup_part_import_list": "목록 가져오기",
"setting__backup_part_import_list_confirm": "백업 파일의 목록이 기존 목록과 동일한 ID를 가지고 있으면 기존 목록의 곡이 덮어써집니다. 계속하시겠습니까?",
"setting__backup_part_import_list_desc": "목록 백업 파일을 선택하세요",
"setting__backup_part_import_setting": "설정 가져오기",
"setting__backup_part_import_setting_desc": "설정 백업 파일을 선택하세요",
"setting__basic": "일반",
"setting__basic_animation": "팝업 애니메이션 무작위 적용",
"setting__basic_control_btn_position": "컨트롤 버튼 위치",
"setting__basic_control_btn_position_left": "왼쪽",
"setting__basic_control_btn_position_right": "오른쪽",
"setting__basic_font": "글꼴",
"setting__basic_font_size": "글자 크기",
"setting__basic_font_size_14px": "더 작게",
"setting__basic_font_size_15px": "작게",
"setting__basic_font_size_16px": "표준",
"setting__basic_font_size_17px": "크게",
"setting__basic_font_size_18px": "더 크게",
"setting__basic_font_size_19px": "매우 크게",
"setting__basic_lang": "언어",
"setting__basic_lang_title": "앱에 표시되는 언어",
"setting__basic_playbar_progress_style": "재생바 진행바 스타일",
"setting__basic_playbar_progress_style_full": "전체 너비",
"setting__basic_playbar_progress_style_middle": "중간",
"setting__basic_playbar_progress_style_mini": "미니",
"setting__basic_show_animation": "애니메이션 표시",
"setting__basic_source": "음악 API",
"setting__basic_source_status_failed": "초기화 실패",
"setting__basic_source_status_initing": "초기화 중...",
"setting__basic_source_status_success": "초기화 성공",
"setting__basic_source_temp": "임시 API (일부 기능 사용 불가. 테스트 API를 사용할 수 없을 때의 대안)",
"setting__basic_source_test": "테스트 API (대부분의 앱 기능 사용 가능)",
"setting__basic_source_user_api_btn": "음악 API 관리",
"setting__basic_sourcename": "음악 스트리밍 서비스 이름",
"setting__basic_sourcename_alias": "별칭",
"setting__basic_sourcename_real": "원래 이름",
"setting__basic_sourcename_title": "음악 스트리밍 서비스 이름 표시 방식을 선택하세요",
"setting__basic_start_in_fullscreen": "전체화면 모드로 시작",
"setting__basic_theme": "테마",
"setting__basic_theme_auto_tip": "이것은 동적 테마입니다. 라이트 테마와 다크 테마를 미리 설정해 두면 시스템의 라이트/다크 모드에 따라 자동으로 해당 테마로 전환됩니다.\n\n참고: 이 테마 항목을 우클릭하면 라이트/다크 테마 설정 창이 열립니다.",
"setting__basic_to_tray": "앱 창을 닫을 때 시스템 트레이로 최소화",
"setting__basic_window_size": "창 크기",
"setting__basic_window_size_big": "크게",
"setting__basic_window_size_huge": "아주 크게",
"setting__basic_window_size_larger": "더 크게",
"setting__basic_window_size_medium": "중간",
"setting__basic_window_size_oversized": "매우 크게",
"setting__basic_window_size_small": "작게",
"setting__basic_window_size_smaller": "더 작게",
"setting__basic_window_size_title": "창 크기를 설정하세요",
"setting__click_copy": "클릭하여 복사",
"setting__click_open": "클릭하여 열기",
"setting__desktop_lyric": "데스크톱 가사",
"setting__desktop_lyric_align": "가사 정렬",
"setting__desktop_lyric_align_center": "가운데",
"setting__desktop_lyric_align_left": "왼쪽",
"setting__desktop_lyric_align_right": "오른쪽",
"setting__desktop_lyric_always_on_top": "항상 위 유지",
"setting__desktop_lyric_always_on_top_loop": "\"항상 위 유지\"가 활성화된 경우 가사창을 반복적으로 새로고침",
"setting__desktop_lyric_always_on_top_loop_tip": "다른 프로그램에 가사창이 계속 가려질 경우 이 옵션을 활성화해 보세요.",
"setting__desktop_lyric_audio_visualization": "오디오 시각화 활성화 (실험적 기능)",
"setting__desktop_lyric_color": "가사 글자 색상",
"setting__desktop_lyric_color_reset": "색상 초기화",
"setting__desktop_lyric_delay_scroll": "가사 스크롤 지연",
"setting__desktop_lyric_direction": "가사 표시 방향",
"setting__desktop_lyric_direction_horizontal": "가로",
"setting__desktop_lyric_direction_vertical": "세로",
"setting__desktop_lyric_ellipsis": "가사 줄바꿈 안 함",
"setting__desktop_lyric_enable": "가사창 표시",
"setting__desktop_lyric_font": "가사 글꼴",
"setting__desktop_lyric_font_default": "기본값",
"setting__desktop_lyric_font_weight": "굵은 글꼴",
"setting__desktop_lyric_font_weight_extended": "번역 및 로마자 가사",
"setting__desktop_lyric_font_weight_font": "글자 단위 가사",
"setting__desktop_lyric_font_weight_line": "줄 단위 가사",
"setting__desktop_lyric_fullscreen_hide": "전체화면일 때 가사창 숨기기",
"setting__desktop_lyric_hover_hide": "마우스가 가사창 위로 이동하면 가사창 투명도 높이기",
"setting__desktop_lyric_hover_hide_tip": "이 기능은 플랫폼 호환성 문제가 있을 수 있습니다.",
"setting__desktop_lyric_line_gap": "가사 간격 ({num})",
"setting__desktop_lyric_line_gap_add": "간격 늘리기",
"setting__desktop_lyric_line_gap_dec": "간격 줄이기",
"setting__desktop_lyric_lock": "가사창 잠금",
"setting__desktop_lyric_lock_screen": "가사창을 메인 화면 밖으로 드래그하지 못하도록 제한",
"setting__desktop_lyric_pause_hide": "음악을 일시정지하면 가사창 투명도 높이기",
"setting__desktop_lyric_played_color": "재생된 부분",
"setting__desktop_lyric_reset": "초기화",
"setting__desktop_lyric_reset_window": "데스크톱 가사 옵션 초기화",
"setting__desktop_lyric_scroll_align": "재생 중 가사 스크롤 위치",
"setting__desktop_lyric_scroll_align_center": "가운데",
"setting__desktop_lyric_scroll_align_top": "위쪽",
"setting__desktop_lyric_shadow_color": "그림자",
"setting__desktop_lyric_show_taskbar": "작업 표시줄에 가사창 프로세스 표시",
"setting__desktop_lyric_show_taskbar_tip": "화면 녹화 프로그램이 가사창을 캡처하지 못할 때 대안으로 사용하는 옵션입니다.",
"setting__desktop_lyric_unplay_color": "재생 안 된 부분",
"setting__dislike_list_input_tip": "곡명@아티스트명\n곡명\n@아티스트명",
"setting__dislike_list_save_btn": "저장",
"setting__dislike_list_tips": "1. 한 줄에 하나의 항목을 입력합니다. 곡명이나 아티스트명에 \"@\" 기호가 있으면 \"#\"로 대체해야 합니다\n2. 특정 아티스트의 곡 지정: 곡명@아티스트명\n3. 곡 지정: 곡명\n4. 아티스트 지정: @아티스트명",
"setting__dislike_list_title": "싫어요 곡 규칙 목록",
"setting__download": "다운로드",
"setting__download_data_embed": "다음 항목을 오디오 파일에 삽입",
"setting__download_embed_lxlyric": "LX Music 가사도 함께 삽입 (있는 경우, LX Music으로 재생 시 글자 단위 가사를 지원합니다)",
"setting__download_embed_lyric": "가사 삽입",
"setting__download_embed_pic": "커버 이미지 삽입",
"setting__download_embed_rlyric": "가능한 경우 로마자 가사도 함께 삽입",
"setting__download_embed_tlyric": "가능한 경우 번역 가사도 함께 삽입",
"setting__download_enable": "다운로드 활성화",
"setting__download_lxlyric": "LX Music 가사도 가사 파일에 함께 기록 (있는 경우, LX Music으로 재생 시 글자 단위 가사를 지원합니다)",
"setting__download_lyric": "가사 다운로드",
"setting__download_lyric_format": "다운로드한 가사 파일의 인코딩 형식",
"setting__download_lyric_format_gbk": "GBK",
"setting__download_lyric_format_tip": "일부 기기에서 중국어가 깨져 보인다면 GBK 형식을 선택해 보세요.",
"setting__download_lyric_format_utf8": "UTF-8",
"setting__download_lyric_title": "가사 파일 다운로드 여부를 선택하세요",
"setting__download_max_num": "동시 다운로드 개수",
"setting__download_max_num_tip": "동시 다운로드 개수가 너무 많으면 음악 API에서 IP가 차단될 수 있습니다. 변경하시겠습니까?",
"setting__download_max_num_tooltip": "값을 너무 높게 설정하면 음악 API에 따라 IP가 차단될 수 있습니다.",
"setting__download_name": "음악 파일 이름 규칙",
"setting__download_name1": "\"제목 - 아티스트\"",
"setting__download_name2": "\"아티스트 - 제목\"",
"setting__download_name3": "제목만",
"setting__download_name_title": "다운로드 시 음악 파일 이름 형식을 선택하세요",
"setting__download_path": "다운로드 경로",
"setting__download_path_change_btn": "변경",
"setting__download_path_label": "현재: ",
"setting__download_path_open_label": "클릭하여 이 경로 열기",
"setting__download_path_title": "다운로드 경로를 선택하세요",
"setting__download_rlyric": "가능한 경우 로마자 가사도 가사 파일에 함께 기록",
"setting__download_select_save_path": "저장 경로를 선택하세요",
"setting__download_skip_exist_file": "다운로드 폴더에 동일한 이름의 파일이 있으면 다운로드 작업 건너뛰기",
"setting__download_tlyric": "가능한 경우 번역 가사도 가사 파일에 함께 기록",
"setting__download_use_other_source": "다운로드 시 자동 소스 전환",
"setting__download_use_other_source_tip": "원래 소스에서 곡을 다운로드할 수 없을 때 다른 소스로 전환하여 다운로드를 시도합니다.\n참고: 소스를 변경한 후의 곡 버전이 원래 버전과 100% 일치한다고 보장하지는 않습니다.",
"setting__hot_key": "단축키",
"setting__hot_key_common_focus_search_input": "검색창 포커스",
"setting__hot_key_common_min": "앱 최소화",
"setting__hot_key_common_toggle_close": "앱 종료",
"setting__hot_key_common_toggle_hide": "앱 표시/숨기기",
"setting__hot_key_common_toggle_min": "앱 최소화/복원",
"setting__hot_key_desktop_lyric_toggle_always_top": "데스크톱 가사 항상 위/해제",
"setting__hot_key_desktop_lyric_toggle_lock": "데스크톱 가사 잠금/잠금 해제",
"setting__hot_key_desktop_lyric_toggle_visible": "데스크톱 가사 표시/숨기기",
"setting__hot_key_global_title": "전역 단축키",
"setting__hot_key_local_title": "앱 내 단축키",
"setting__hot_key_player_music_dislike": "곡 싫어요",
"setting__hot_key_player_music_love": "곡 좋아요",
"setting__hot_key_player_music_unlove": "곡 좋아요 취소",
"setting__hot_key_player_next": "다음 곡",
"setting__hot_key_player_prev": "이전 곡",
"setting__hot_key_player_seekbackward": "5초 되감기",
"setting__hot_key_player_seekforward": "5초 빨리감기",
"setting__hot_key_player_toggle_play": "재생/일시정지",
"setting__hot_key_player_volume_down": "볼륨 낮추기",
"setting__hot_key_player_volume_mute": "음소거 전환",
"setting__hot_key_player_volume_up": "볼륨 높이기",
"setting__hot_key_tip_input": "새 키를 입력하세요",
"setting__hot_key_unset_input": "설정 안 됨",
"setting__is_enable": "사용",
"setting__is_show": "표시",
"setting__list": "목록",
"setting__list_action_btn": "목록 작업 버튼 표시",
"setting__list_add_music_location_type": "목록에 곡 추가 시 위치",
"setting__list_add_music_location_type_bottom": "맨 아래",
"setting__list_add_music_location_type_top": "맨 위",
"setting__list_click_action": "목록에서 곡을 더블클릭하면 자동으로 현재 목록으로 전환 (\"재생목록\" 및 \"차트\" 페이지에서만 적용)",
"setting__list_scroll": "재생목록 스크롤 위치 기억 (\"내 서재\" 페이지에서만 적용)",
"setting__list_source": "곡이 어느 음악 스트리밍 서비스에서 왔는지 표시 (\"내 서재\" 페이지에서만 적용)",
"setting__network": "네트워크",
"setting__network_proxy_host": "호스트",
"setting__network_proxy_password": "비밀번호",
"setting__network_proxy_port": "포트",
"setting__network_proxy_title": "HTTP 프록시 (설정이 잘못되면 앱이 네트워크에 연결되지 않습니다)",
"setting__network_proxy_username": "사용자 이름",
"setting__odc": "자동 비우기",
"setting__odc_clear_search_input": "검색하지 않을 때 검색창 비우기",
"setting__odc_clear_search_list": "검색하지 않을 때 검색 목록 비우기",
"setting__open_api": "오픈 API",
"setting__open_api_address": "서비스 주소: ",
"setting__open_api_bind_lan": "LAN에서 접속 허용",
"setting__open_api_enable": "오픈 API 서비스 활성화",
"setting__open_api_port": "서비스 포트",
"setting__open_api_port_tip": "오픈 API 서비스 포트를 입력하세요",
"setting__open_api_tip": "이 기능은 타사 프로그램이 LX Music을 호출할 수 있도록 제공합니다. 현재 사용 가능한 기능을 확인하세요: ",
"setting__open_api_tip_link": "문서 보기",
"setting__other": "기타",
"setting__other_dislike_list": "싫어요 곡 규칙",
"setting__other_dislike_list_label": "규칙 개수: ",
"setting__other_dislike_list_show_btn": "규칙 편집",
"setting__other_listdata": "목록 데이터 정리",
"setting__other_listdata_clear_btn": "\"내 서재\" 데이터 지우기",
"setting__other_listdata_clear_tip_confirm": "생성한 모든 목록과 좋아요한 모든 곡이 지워집니다. 정말 계속하시겠습니까?",
"setting__other_lyric_edited_cache": "오프셋 조정된 가사 관리",
"setting__other_lyric_edited_clear_btn": "시간 조정된 가사 지우기",
"setting__other_lyric_edited_clear_tip_confirm": "이전에 오프셋을 조정한 모든 가사가 지워집니다. 지우시겠습니까? \n(손 떨림으로 확인 🤪)",
"setting__other_lyric_edited_label": "가사 개수: ",
"setting__other_lyric_raw_clear_btn": "가사 캐시 지우기",
"setting__other_lyric_raw_label": "가사 개수: ",
"setting__other_music_url_clear_btn": "곡 URL 캐시 지우기",
"setting__other_music_url_label": "곡 URL 개수: ",
"setting__other_other_cache": "기타 캐시 관리",
"setting__other_other_source_clear_btn": "소스 변경된 곡 캐시 지우기",
"setting__other_other_source_label": "소스가 변경된 곡 정보 개수: ",
"setting__other_resource_cache": "리소스 캐시 관리",
"setting__other_resource_cache_clear_btn": "리소스 캐시 지우기",
"setting__other_resource_cache_confirm": "지우겠습니다",
"setting__other_resource_cache_label": "앱이 사용 중인 캐시 크기: ",
"setting__other_resource_cache_tip": "이미지, 오디오 등의 캐시를 포함합니다. 정리 후에는 해당 리소스를 다시 다운로드해야 합니다. 정리하지 않는 것을 권장하며, 앱이 디스크 공간에 따라 캐시 크기를 동적으로 관리합니다.",
"setting__other_resource_cache_tip_confirm": "이미지, 오디오 등의 캐시를 포함하며, 정리 후에는 해당 리소스를 다시 다운로드해야 합니다. 정리하지 않는 것을 권장하며, 앱이 디스크 공간에 따라 캐시 크기를 동적으로 관리합니다. 그래도 정리하시겠습니까?",
"setting__other_transparent_window": "메인 창에 내장된 둥근 모서리와 그림자 사용",
"setting__other_transparent_window_tip": "이 옵션을 끄면 시스템 기본 창 스타일을 사용합니다. 변경 사항을 적용하려면 소프트웨어를 재시작해야 합니다.",
"setting__other_tray_theme": "트레이 아이콘 스타일",
"setting__other_tray_theme_auto": "시스템 따르기",
"setting__other_tray_theme_black": "검정",
"setting__other_tray_theme_native": "흰색",
"setting__other_tray_theme_origin": "기본값",
"setting__play": "재생",
"setting__play_advanced_audio_features_tip": "사용자 지정 오디오 출력 장치는 이 기능과 충돌합니다. 이 기능을 활성화하면 오디오 출력 장치가 기본 출력 장치로 재설정됩니다. 현재로서는 이 문제를 해결할 수 없습니다. 그래도 활성화하시겠습니까?",
"setting__play_auto_clean_played_list": "현재 재생목록과 동일한 목록을 재생할 때 재생 완료 목록 비우기",
"setting__play_auto_clean_played_list_tip": "무작위 재생 모드에서 목록의 모든 곡이 다시 무작위 대상에 포함됩니다.",
"setting__play_auto_skip_on_error": "재생 오류 발생 시 자동으로 곡 전환",
"setting__play_detail": "재생 상세 페이지",
"setting__play_detail_align": "가사 정렬",
"setting__play_detail_align_center": "가운데",
"setting__play_detail_align_left": "왼쪽",
"setting__play_detail_align_right": "오른쪽",
"setting__play_detail_font_size": "가사 글자 크기 (재생 상세 페이지에서 키보드 \"+\"와 \"-\"로 글자 크기를 조정할 수 있습니다)",
"setting__play_detail_font_size_current": "현재 글자 크기: {size}",
"setting__play_detail_font_size_reset": "초기화",
"setting__play_detail_font_zoom": "재생 중인 가사 확대",
"setting__play_detail_lyric_delay_scroll": "가사 스크롤 지연",
"setting__play_detail_lyric_progress": "가사를 드래그하여 재생 진행 위치 조정 허용",
"setting__play_lyric_lxlrc": "가능한 경우 노래방 스타일 가사로 재생",
"setting__play_lyric_lxlrc_tip": "이 기능은 높은 성능을 요구하므로 저사양 기기에서는 사용을 권장하지 않습니다!",
"setting__play_lyric_roma": "가능한 경우 로마자 가사 표시",
"setting__play_lyric_s2t": "재생 및 다운로드 중인 중국어 가사를 번체자로 변환",
"setting__play_lyric_transition": "가능한 경우 번역 가사 표시",
"setting__play_max_output_channel_count": "기기가 지원하는 최대 채널 수로 오디오 출력",
"setting__play_mediaDevice": "오디오 출력",
"setting__play_mediaDevice_remove_stop_play": "현재 오디오 출력 장치가 변경되면 재생 일시정지",
"setting__play_mediaDevice_title": "오디오 출력에 사용할 미디어 장치를 선택하세요",
"setting__play_media_device_error_tip": "이 기능은 고급 오디오 기능(오디오 시각화, 음향 옵션, 기기 최대 채널 수 오디오 출력)과 충돌합니다. 이번 앱 실행 시 해당 기능이 활성화되어 있어 이 옵션을 일시적으로 사용할 수 없습니다. 해당 기능을 끄고 앱을 재시작한 후 다시 변경해 주세요.",
"setting__play_media_device_tip": "이 기능은 오디오 시각화 기능과 충돌하여 동시에 사용할 수 없습니다. 오디오 시각화를 끄고 선택한 오디오 출력 옵션을 적용하시겠습니까?",
"setting__play_playQuality": "가능한 경우 재생 시 음질 우선",
"setting__play_power_save_blocker": "곡 재생 중 기기 절전 모드 방지",
"setting__play_save_play_time": "재생 진행 위치 기억",
"setting__play_startup_auto_play": "시작 시 자동으로 음악 재생",
"setting__play_statusbar_lyric": "상태 메뉴에 가사 표시",
"setting__play_statusbar_lyric_tip": "설정 → 일반에서 \"앱 창을 닫을 때 시스템 트레이로 최소화\"를 활성화해야 합니다",
"setting__play_task_bar": "작업 표시줄에 재생 진행률 표시",
"setting__play_timeout": "타이머 정지",
"setting__player_audio_visualization_tip": "사용자 지정 오디오 출력 장치 기능은 오디오 시각화 기능과 충돌합니다. 오디오 시각화를 활성화하면 오디오 출력 장치가 기본값으로 재설정됩니다. 현재로서는 이 문제를 해결할 수 없습니다. 그래도 활성화하시겠습니까?",
"setting__player_swap_lyric_trans_roma": "번역 가사와 로마자 가사의 위치 바꾸기",
"setting__search": "검색",
"setting__search_focus_search_box": "시작 시 검색창 자동 포커스",
"setting__search_history": "검색 기록 활성화",
"setting__search_hot": "인기 검색어 활성화",
"setting__sync": "데이터 동기화",
"setting__sync_client_address": "현재 기기 주소: {address}",
"setting__sync_client_host": "동기화 서비스 주소",
"setting__sync_client_host_tip": "http://<호스트>:<포트>",
"setting__sync_client_mode": "클라이언트 모드",
"setting__sync_client_status": "상태: {status}",
"setting__sync_code_blocked_ip": "현재 기기의 IP가 서버에 의해 차단되었습니다!",
"setting__sync_code_fail": "유효하지 않은 연결 코드",
"setting__sync_enable": "동기화 활성화",
"setting__sync_mode": "동기화 모드",
"setting__sync_mode_client": "클라이언트",
"setting__sync_mode_server": "서버",
"setting__sync_server_address": "동기화 서비스 주소: {address}",
"setting__sync_server_auth_code": "연결 코드: {code}",
"setting__sync_server_device": "연결된 기기: {devices}",
"setting__sync_server_device_list_btn_remove": "삭제",
"setting__sync_server_device_list_noitem": "아무것도 없습니다 ┗( ▔, ▔ )┛",
"setting__sync_server_device_list_time": "마지막 연결 시간: {time}",
"setting__sync_server_device_list_tips": "💡 기기를 삭제한 후 다시 연결하려면 연결 코드를 다시 입력해야 합니다",
"setting__sync_server_device_list_title": "인증된 기기",
"setting__sync_server_mode": "서버 모드 (데이터가 평문으로 전송되므로 신뢰할 수 있는 네트워크에서 사용하세요)",
"setting__sync_server_port": "동기화 포트",
"setting__sync_server_port_tip": "포트 번호를 입력하세요",
"setting__sync_server_refresh_code": "연결 코드 새로고침",
"setting__sync_server_show_device_list": "인증된 기기 목록",
"setting__sync_tip": "사용 방법은 FAQ의 \"동기화 기능\" 항목을 참고하세요",
"setting__update": "업데이트",
"setting__update_checking": "업데이트 확인 중...",
"setting__update_commit_date": "커밋 날짜: ",
"setting__update_commit_id": "커밋 해시: ",
"setting__update_current_label": "현재 버전: ",
"setting__update_downloading": "업데이트를 찾아 다운로드하는 중...⏳",
"setting__update_init": "업데이트 중...",
"setting__update_latest": "앱이 최신 버전입니다. 즐거운 시간 되세요!🥂",
"setting__update_latest_label": "최신 버전: ",
"setting__update_new_version": "새 버전을 찾았습니다. 어서 업데이트하세요~🚀🚀",
"setting__update_open_version_modal_btn": "업데이트 창 열기",
"setting__update_progress": "상태: ",
"setting__update_show_change_log": "업데이트 후 첫 실행 시 변경 로그 표시",
"setting__update_try_auto_update": "새 버전을 찾으면 자동으로 업데이트 다운로드 시도",
"setting__update_unknown": "알 수 없음",
"setting__update_unknown_tip": "❓ 최신 버전 정보를 가져오지 못했습니다. 정보 페이지에서 프로젝트 릴리스 주소를 열어 현재 버전이 최신인지 확인하는 것을 권장합니다",
"setting_download_save_group_list_name": "해당 목록 이름의 하위 폴더에 파일 저장",
"setting_sync_status_enabled": "연결됨",
"song_list": "재생목록",
"songlist__import_input_btn_confirm": "열기",
"songlist__import_input_show_btn": "재생목록 열기",
"songlist__import_input_tip": "재생목록 링크/ID를 입력하세요",
"songlist__import_input_tip_1": "다른 서비스 간 재생목록은 지원되지 않습니다. 열려는 재생목록이 현재 선택한 서비스에 해당하는지 확인해 주세요",
"songlist__import_input_tip_2": "열리지 않는 재생목록 링크가 있다면 피드백을 보내주세요",
"songlist__import_input_tip_3": "간편판의 재생목록 ID나 링크로는 쿠거우 재생목록을 열 수 없지만, 정식판의 쿠거우 코드나 링크로는 열 수 있습니다",
"songlist__import_input_tip_4": "넷이즈의 \"프라이빗 레이더\" 재생목록은 접근하려면 토큰이 필요합니다. 텐센트의 개인 재생목록은 프로필을 공개해야 접근할 수 있습니다. 자세한 내용은 다음을 참고하세요: ",
"songlist__import_input_title": "공유된 재생목록 열기",
"songlist__open_list": "\"{name}\" 재생목록 열기",
"songlist__tag_info_hot_tag": "인기 태그",
"source_alias_all": "통합",
"source_alias_bd": "BD 뮤직",
"source_alias_kg": "KG 뮤직",
"source_alias_kw": "KW 뮤직",
"source_alias_mg": "MG 뮤직",
"source_alias_tx": "TX 뮤직",
"source_alias_wy": "WY 뮤직",
"source_alias_xm": "XM 뮤직",
"source_all": "통합",
"source_bd": "바이두",
"source_kg": "쿠거우",
"source_kw": "쿠워",
"source_mg": "미구",
"source_tx": "텐센트",
"source_wy": "넷이즈",
"source_xm": "샤미",
"sync__auth_code_input_tip": "연결 코드를 입력하세요",
"sync__auth_code_title": "연결 코드 입력이 필요합니다",
"sync__dislike_merge_tip_desc": "두 목록의 내용을 병합하고 중복 항목을 제거합니다.",
"sync__dislike_other_tip_desc": "\"동기화 취소\"를 선택하면 \"싫어요 곡\" 목록을 동기화하지 않습니다.",
"sync__dislike_overwrite_tip_desc": "덮어써지는 쪽의 목록이 덮어쓰는 쪽의 목록으로 교체됩니다.",
"sync__dislike_title": "{name}의 \"싫어요 곡\" 목록과 동기화할 방법을 선택하세요",
"sync__list_merge_tip_desc": "두 목록을 병합합니다. 동일한 곡은 제거되고(병합 대상의 곡이 제거됨), 다른 곡은 추가됩니다.",
"sync__list_other_tip_desc": "\"동기화 취소\"를 선택하면 목록을 동기화하지 않습니다.",
"sync__list_overwrite_tip_desc": "덮어써지는 목록과 동일한 ID를 가진 목록은 삭제되고 덮어쓰는 쪽의 목록으로 교체됩니다(ID가 다른 목록은 병합됩니다). \"전체 덮어쓰기\"가 선택된 경우, 덮어써지는 쪽의 모든 목록이 제거되고 덮어쓰는 쪽의 목록으로 교체됩니다.",
"sync__list_title": "\"{name}\"과(와) 목록을 동기화할 방법을 선택하세요",
"sync__merge_btn_local_remote": "\"로컬 목록\"을 \"원격 목록\"과 병합",
"sync__merge_btn_remote_local": "\"원격 목록\"을 \"로컬 목록\"과 병합",
"sync__merge_label": "병합",
"sync__merge_tip": "병합: ",
"sync__other_label": "기타",
"sync__other_tip": "기타: ",
"sync__overwrite": "전체 덮어쓰기",
"sync__overwrite_btn_cancel": "동기화 취소",
"sync__overwrite_btn_local_remote": "\"로컬 목록\"으로 \"원격 목록\" 덮어쓰기",
"sync__overwrite_btn_none": "실시간 동기화만 사용",
"sync__overwrite_btn_remote_local": "\"원격 목록\"으로 \"로컬 목록\" 덮어쓰기",
"sync__overwrite_label": "덮어쓰기",
"sync__overwrite_tip": "덮어쓰기: ",
"sync_status_disabled": "연결 안 됨",
"tag__high_quality": "HQ",
"tag__lossless": "SQ",
"tag__lossless_24bit": "24bit",
"theme_add": "테마 추가",
"theme_auto": "자동",
"theme_auto_tip": "우클릭하여 라이트/다크 테마 옵션 창 열기",
"theme_black": "블랙",
"theme_blue": "블루",
"theme_blue2": "퍼플 블루",
"theme_blue_plus": "블루 플러스",
"theme_china_ink": "수묵",
"theme_edit_modal__app_bg": "앱 배경 색상",
"theme_edit_modal__aside_color": "사이드바 버튼 색상",
"theme_edit_modal__badge": "라벨 색상",
"theme_edit_modal__badge_primary": "메인 색상",
"theme_edit_modal__badge_secondary": "보조 색상",
"theme_edit_modal__badge_tertiary": "세 번째 색상",
"theme_edit_modal__bg_image": "배경 이미지",
"theme_edit_modal__bg_image_add": "배경 이미지 추가",
"theme_edit_modal__bg_image_change": "배경 이미지 변경",
"theme_edit_modal__bg_image_remove": "배경 이미지 제거",
"theme_edit_modal__close_btn": "닫기",
"theme_edit_modal__control_btn": "왼쪽 컨트롤 버튼 색상",
"theme_edit_modal__copy": "테마 복사",
"theme_edit_modal__dark": "다크 테마",
"theme_edit_modal__dark_font": "다크 글꼴",
"theme_edit_modal__font": "글꼴 색상",
"theme_edit_modal__hide_btn": "상세 페이지 숨기기",
"theme_edit_modal__main_bg": "콘텐츠 영역 배경 색상",
"theme_edit_modal__min_btn": "최소화",
"theme_edit_modal__pick_cancel": "초기화",
"theme_edit_modal__pick_color": "색상 선택",
"theme_edit_modal__pick_last_color": "이전 색상 사용",
"theme_edit_modal__pick_save": "확인",
"theme_edit_modal__preview": "미리보기",
"theme_edit_modal__primary": "테마 색상",
"theme_edit_modal__remove": "삭제",
"theme_edit_modal__remove_tip": "이 테마를 정말 삭제하시겠습니까?",
"theme_edit_modal__save_new": "복사",
"theme_edit_modal__select_bg_file": "배경 이미지를 선택하세요",
"theme_edit_modal__title_add": "테마 추가",
"theme_edit_modal__title_edit": "테마 편집",
"theme_green": "그린",
"theme_grey": "그레이",
"theme_happy_new_year": "새해",
"theme_max_tip": "테마는 최대 10개까지 추가할 수 있습니다. 일부를 삭제하고 다시 추가해 보세요 😜",
"theme_mid_autumn": "추석",
"theme_ming": "밍",
"theme_more_btn_show": "더 많은 테마",
"theme_naruto": "나루토",
"theme_orange": "오렌지",
"theme_pink": "핑크",
"theme_purple": "퍼플",
"theme_red": "레드",
"theme_selector_modal__dark_title": "다크 테마",
"theme_selector_modal__light_title": "라이트 테마",
"theme_selector_modal__theme_name": "테마 이름",
"theme_selector_modal__title": "시스템 테마 따르기 옵션",
"theme_selector_modal__title_tip": "참고: 라이트 테마와 다크 테마를 미리 설정해 두면 시스템의 라이트/다크 모드에 따라 자동으로 미리 설정한 테마로 전환됩니다.",
"toggle_source_failed": "소스 변경에 실패했습니다. 검색 페이지에서 다른 서비스를 지정하여 직접 곡을 검색해 보세요.",
"toggle_source_try": "다른 소스로 전환을 시도하는 중...",
"update__downgrade_tip": "버전을 다운그레이드({ver})하신 것을 확인했습니다. 새 버전을 사용하다가 문제가 발생하면 먼저 FAQ를 확인해 주세요. FAQ에 문서화되지 않았거나 해결되지 않는 문제는 문서에 안내된 피드백 채널을 통해 알려주세요😘!\n\n참고: 새 버전에서 이전 버전으로 다운그레이드할 때는 먼저 재생목록을 백업하는 것을 권장합니다. 문제가 발생하면 데이터를 정리하여 해결할 수 있습니다. 데이터 폴더 경로는 문서에서 확인할 수 있습니다.",
"update__error_top": "새 버전 다운로드에 실패했습니다. 다시 다운로드를 시도하거나 GitHub에서 직접 다운로드해 보세요.\n\n새 버전의 주소는 업데이트 팝업창에 포함되어 있습니다. 새 버전을 다운로드하여 바로 설치하세요. 설치에 실패하면 FAQ를 참고하세요.\n\n참고: 현재는 Windows Setup 버전만 자동 업데이트가 가능합니다. Linux의 AppImage 및 deb 버전도 동작하는 것으로 보이나 테스트되지 않았습니다. 다른 버전으로 설치한 사용자는 수동으로 다운로드하여 업데이트해야 합니다.",
"update__ignore_cancel": "업데이트하지 않겠습니다 🤨",
"update__ignore_confirm": "네, 업데이트할게요 ❤️",
"update__ignore_confirm_tip": "현재는 Windows Setup 버전만 자동 업데이트가 가능합니다. Linux의 AppImage 및 deb 버전도 동작하는 것으로 보이나 테스트되지 않았습니다. 다른 버전으로 설치한 사용자는 수동으로 다운로드하여 업데이트해야 합니다.\n\n새 버전의 주소는 업데이트 팝업창에 포함되어 있습니다. 새 버전을 다운로드하여 바로 설치하세요. 설치에 실패하면 FAQ를 참고하세요.",
"update__ignore_confirm_tip_confirm": "네, 이해했습니다",
"update__ignore_tip": "현재 사용 중인 버전이 최신 버전보다 {num}개 버전 뒤처져 있습니다🤪. 더 나은 사용 경험을 위해 최신 버전으로 업데이트하는 것을 권장합니다~!\n참고: 새 버전을 사용하다가 문제가 발생하면 먼저 FAQ를 확인해 주세요. FAQ에 문서화되지 않았거나 해결되지 않는 문제는 문서에 안내된 피드백 채널을 통해 알려주세요😘!",
"update__timeout_top": "다운로드 시간 초과 안내\n\n현재 GitHub 접속 속도가 느려 새 버전을 한 시간째 다운로드하고 있지만 아직 완료되지 않았습니다😳. 계속 기다릴 수도 있지만, 수동으로 업데이트하는 것을 강력히 권장합니다!",
"user_api__allow_show_update_alert": "업데이트 팝업 표시 허용",
"user_api__btn_export": "내보내기",
"user_api__btn_import": "로컬 파일에서 가져오기",
"user_api__btn_import_online": "네트워크에서 가져오기",
"user_api__btn_remove": "삭제",
"user_api__import_file": "음악 API 스크립트 파일을 선택하세요",
"user_api__init_failed_alert": "음악 API \"{name}\" 초기화 실패: ",
"user_api__max_tip": "동시에 최대 20개의 API만 사용할 수 있습니다🤪.\n\n계속 가져오려면 불필요한 API를 삭제하여 공간을 확보해 주세요.",
"user_api__noitem": "아무것도 없습니다...😲",
"user_api__note": "안내: API 스크립트의 실행 환경을 최대한 격리했지만, 악의적인 동작을 포함한 API 스크립트를 가져오면 시스템에 영향을 줄 수 있습니다. 신중하게 가져와 주세요.",
"user_api__readme": "API 작성 안내: ",
"user_api__title": "음악 API 관리",
"user_api__update_alert": "음악 API \"{name}\"의 새 버전을 찾았습니다: ",
"user_api__update_alert_open_url": "업데이트 주소 열기",
"user_api_import__failed": "음악 API 가져오기에 실패했습니다:\n\n{message}",
"user_api_import_online__input_confirm": "가져오기",
"user_api_import_online__input_loading": "가져오는 중...",
"user_api_import_online__input_tip": "HTTP 링크를 입력하세요",
"user_api_import_online__title": "네트워크에서 음악 API 가져오기"
}

View File

@@ -1,4 +1,14 @@
[
{
"name": "English",
"locale": "en-us",
"country": "us"
},
{
"name": "한국어",
"locale": "ko-kr",
"country": "kr"
},
{
"name": "简体中文",
"locale": "zh-cn",
@@ -11,10 +21,5 @@
"locale": "zh-tw",
"alternate": "zh-hk",
"country": "cn"
},
{
"name": "English",
"locale": "en-us",
"country": "us"
}
]

View File

@@ -320,11 +320,11 @@
"setting__basic_control_btn_position_right": "右边",
"setting__basic_font": "字体",
"setting__basic_font_size": "字体大小",
"setting__basic_font_size_14px": "小",
"setting__basic_font_size_14px": "小",
"setting__basic_font_size_15px": "小",
"setting__basic_font_size_16px": "标准",
"setting__basic_font_size_17px": "大",
"setting__basic_font_size_18px": "大",
"setting__basic_font_size_18px": "大",
"setting__basic_font_size_19px": "非常大",
"setting__basic_lang": "语言",
"setting__basic_lang_title": "软件显示的语言",
@@ -351,11 +351,11 @@
"setting__basic_window_size": "窗口尺寸",
"setting__basic_window_size_big": "大",
"setting__basic_window_size_huge": "巨大",
"setting__basic_window_size_larger": "大",
"setting__basic_window_size_larger": "大",
"setting__basic_window_size_medium": "中",
"setting__basic_window_size_oversized": "超大",
"setting__basic_window_size_small": "小",
"setting__basic_window_size_smaller": "小",
"setting__basic_window_size_smaller": "小",
"setting__basic_window_size_title": "设置软件窗口尺寸",
"setting__click_copy": "点击复制",
"setting__click_open": "点击打开",
@@ -407,11 +407,13 @@
"setting__dislike_list_title": "「不喜欢的歌曲」规则列表",
"setting__download": "下载设置",
"setting__download_data_embed": "嵌入到音频文件中的内容",
"setting__download_embed_lxlyric": "同时嵌入 LX Music 歌词(如果有,使用 LX Music 播放时可支持逐字歌词)",
"setting__download_embed_lyric": "嵌入歌词",
"setting__download_embed_pic": "嵌入封面",
"setting__download_embed_rlyric": "同时嵌入罗马音歌词(如果有)",
"setting__download_embed_tlyric": "同时嵌入翻译歌词(如果有)",
"setting__download_enable": "启用下载功能",
"setting__download_lxlyric": "同时将 LX Music 歌词写入歌词文件中(如果有,使用 LX Music 播放时可支持逐字歌词)",
"setting__download_lyric": "歌词下载",
"setting__download_lyric_format": "下载的歌词文件编码格式",
"setting__download_lyric_format_gbk": "GBK",
@@ -512,6 +514,8 @@
"setting__other_resource_cache_label": "软件已使用缓存大小:",
"setting__other_resource_cache_tip": "包括图片、音频等缓存,清理后这些资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小",
"setting__other_resource_cache_tip_confirm": "涉及图片、音频等缓存,清理后这些资源将需要重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小,是否仍要清理?",
"setting__other_transparent_window": "主窗口使用软件内置的圆角及阴影",
"setting__other_transparent_window_tip": "关闭后将使用系统原生窗口样式。更改后重启软件生效。",
"setting__other_tray_theme": "托盘图标样式",
"setting__other_tray_theme_auto": "跟随系统亮暗模式",
"setting__other_tray_theme_black": "黑色",
@@ -553,6 +557,7 @@
"setting__play_task_bar": "在任务栏上显示当前歌曲播放进度",
"setting__play_timeout": "定时暂停",
"setting__player_audio_visualization_tip": "自定义音频输出设备与音频可视化功能冲突,启用「音频可视化」后音频输出设备将会被重置为默认,目前此问题暂无法解决。是否仍要开启?",
"setting__player_swap_lyric_trans_roma": "调换歌词翻译与歌词罗马音位置",
"setting__search": "搜索设置",
"setting__search_focus_search_box": "启动时自动聚焦搜索框",
"setting__search_history": "显示历史搜索记录",
@@ -608,7 +613,7 @@
"songlist__import_input_tip_1": "不支持跨源打开歌单,请确认要打开的歌单与当前选择的歌单来源是否对应",
"songlist__import_input_tip_2": "若遇到无法打开的歌单链接,欢迎反馈",
"songlist__import_input_tip_3": "酷狗源歌单不支持用歌单 ID 或概念版链接打开,但支持用普通版链接或酷狗码打开",
"songlist__import_input_tip_4": "网易源的「我喜欢」歌单需要 Token 才能打开,详情看 ",
"songlist__import_input_tip_4": "网易源「私人雷达」歌单需要 Token 才能打开,企鹅源个人歌单需要将个人主页公开才能打开,请参阅 ",
"songlist__import_input_title": "打开分享的歌单",
"songlist__open_list": "打开「{name}」歌单",
"songlist__tag_info_hot_tag": "热门标签",

View File

@@ -320,11 +320,11 @@
"setting__basic_control_btn_position_right": "右邊",
"setting__basic_font": "字體",
"setting__basic_font_size": "字體大小",
"setting__basic_font_size_14px": "小",
"setting__basic_font_size_14px": "小",
"setting__basic_font_size_15px": "小",
"setting__basic_font_size_16px": "標準",
"setting__basic_font_size_17px": "大",
"setting__basic_font_size_18px": "大",
"setting__basic_font_size_18px": "大",
"setting__basic_font_size_19px": "非常大",
"setting__basic_lang": "語言",
"setting__basic_lang_title": "軟體顯示的語言",
@@ -351,11 +351,11 @@
"setting__basic_window_size": "視窗尺寸",
"setting__basic_window_size_big": "大",
"setting__basic_window_size_huge": "巨大",
"setting__basic_window_size_larger": "大",
"setting__basic_window_size_larger": "大",
"setting__basic_window_size_medium": "中",
"setting__basic_window_size_oversized": "超大",
"setting__basic_window_size_small": "小",
"setting__basic_window_size_smaller": "小",
"setting__basic_window_size_smaller": "小",
"setting__basic_window_size_title": "設定軟體視窗尺寸",
"setting__click_copy": "點擊複製",
"setting__click_open": "點擊開啟",
@@ -407,11 +407,13 @@
"setting__dislike_list_title": "「不喜歡的歌曲」規則清單",
"setting__download": "下載設定",
"setting__download_data_embed": "嵌入到音訊檔案中的內容",
"setting__download_embed_lxlyric": "同時嵌入 LX Music 歌詞(如果有,使用 LX Music 播放時可支持逐字歌詞)",
"setting__download_embed_lyric": "嵌入歌詞",
"setting__download_embed_pic": "嵌入封面",
"setting__download_embed_rlyric": "同時嵌入羅馬音歌詞(如果有)",
"setting__download_embed_tlyric": "同時嵌入翻譯歌詞(如果有)",
"setting__download_enable": "啟用下載功能",
"setting__download_lxlyric": "同時將 LX Music 歌詞寫入歌詞文件中(如果有,使用 LX Music 播放時可支持逐字歌詞)",
"setting__download_lyric": "歌詞下載",
"setting__download_lyric_format": "下載的歌詞檔案編碼格式",
"setting__download_lyric_format_gbk": "GBK",
@@ -512,6 +514,8 @@
"setting__other_resource_cache_label": "軟體已使用快取大小:",
"setting__other_resource_cache_tip": "包括圖片、音訊等快取,清理後這些資源將需要重新下載,不建議清理,軟體會根據磁碟空間動態管理快取大小",
"setting__other_resource_cache_tip_confirm": "涉及圖片、音訊等快取,清理後這些資源將需要重新下載,不建議清理,軟體會根據磁碟空間動態管理快取大小,是否仍要清理?",
"setting__other_transparent_window": "主視窗使用軟體內建的圓角及陰影",
"setting__other_transparent_window_tip": "關閉後將使用系統原生視窗樣式。更改後重啟軟體生效。",
"setting__other_tray_theme": "系統匣圖示樣式",
"setting__other_tray_theme_auto": "跟隨系統亮暗模式",
"setting__other_tray_theme_black": "黑色",
@@ -553,6 +557,7 @@
"setting__play_task_bar": "在工具列上顯示目前歌曲播放進度",
"setting__play_timeout": "定時暫停",
"setting__player_audio_visualization_tip": "自訂音訊輸出裝置與音訊可視化功能衝突,啟用「音訊可視化」後音訊輸出裝置將會被重設為預設,目前此問題暫無法解決。是否仍要開啟?",
"setting__player_swap_lyric_trans_roma": "調換歌詞翻譯與歌詞羅馬音位置",
"setting__search": "搜尋設定",
"setting__search_focus_search_box": "啟動時自動聚焦搜尋框",
"setting__search_history": "顯示歷史搜尋記錄",
@@ -608,7 +613,7 @@
"songlist__import_input_tip_1": "不支援跨平台開啟歌單,請確認要開啟的歌單與目前歌單的來源平台是否對應",
"songlist__import_input_tip_2": "若遇到無法開啟的歌單連結,歡迎回報",
"songlist__import_input_tip_3": "酷狗音樂歌單不支援用歌單 ID 與概念版連結開啟,但支援用普通版連結與酷狗碼開啟",
"songlist__import_input_tip_4": "網易雲音樂的「我喜歡」歌單需要 Token 才能開啟,詳情看 ",
"songlist__import_input_tip_4": "網易雲音樂「私人雷達」歌單需要 Token 才能開啟,企鵝音樂個人歌單需要將個人資料頁公開才能開啟,詳情看 ",
"songlist__import_input_title": "開啟分享的歌單",
"songlist__open_list": "開啟「{name}」歌單",
"songlist__tag_info_hot_tag": "熱門標籤",

View File

@@ -2,7 +2,7 @@ import path from 'node:path'
import { existsSync, mkdirSync, renameSync } from 'fs'
import { app, shell, screen, nativeTheme, dialog } from 'electron'
import { URL_SCHEME_RXP } from '@common/constants'
import { getTheme, initHotKey, initSetting, parseEnvParams } from './utils'
import { getProxy, getTheme, initHotKey, initSetting, parseEnvParams } from './utils'
import { navigationUrlWhiteList } from '@common/config'
import defaultSetting from '@common/defaultSetting'
import { isExistWindow as isExistMainWindow, showWindow as showMainWindow } from './modules/winMain'
@@ -11,10 +11,11 @@ import { isMac, log } from '@common/utils'
import createWorkers from './worker'
import { migrateDBData } from './utils/migrate'
import { openDirInExplorer } from '@common/utils/electron'
import { setProxyByHost } from '@common/utils/request'
export const initGlobalData = () => {
const envParams = parseEnvParams()
envParams.cmdParams.dt = !!envParams.cmdParams.dt
// envParams.cmdParams.dt = !!envParams.cmdParams.dt
global.envParams = {
cmdParams: envParams.cmdParams,
@@ -64,7 +65,12 @@ export const initGlobalData = () => {
lyricLineText: '',
lyricLineAllText: '',
lyric: '',
tlyric: '',
rlyric: '',
lxlyric: '',
collect: false,
volume: 0,
mute: false,
},
}
@@ -82,16 +88,16 @@ export const initSingleInstanceHandle = () => {
}
app.on('second-instance', (event, argv, cwd) => {
for (const param of argv) {
if (URL_SCHEME_RXP.test(param)) {
global.envParams.deeplink = param
break
}
}
if (isExistMainWindow()) {
if (global.envParams.deeplink) global.lx.event_app.deeplink(global.envParams.deeplink)
else showMainWindow()
const envParams = parseEnvParams(argv)
if (envParams.deeplink) {
global.envParams.deeplink = envParams.deeplink
global.lx.event_app.deeplink(global.envParams.deeplink)
return
}
if (envParams.cmdParams.hidden !== true) {
showMainWindow()
}
} else {
app.quit()
}
@@ -230,6 +236,30 @@ export const listenerAppEvent = (startApp: () => void) => {
global.lx.theme.shouldUseDarkColors = shouldUseDarkColors
global.lx?.event_app.system_theme_change(shouldUseDarkColors)
})
const setProxy = () => {
const proxy = getProxy()
if (proxy) {
setProxyByHost(proxy.host, proxy.port ? String(proxy.port) : undefined)
} else setProxyByHost()
}
global.lx.event_app.on('updated_config', (keys, setting) => {
if (keys.includes('network.proxy.enable') || (global.lx.appSetting['network.proxy.enable'] && keys.some(k => k.includes('network.proxy.')))) {
setProxy()
}
if (keys.includes('player.volume')) {
global.lx.event_app.player_status({ volume: Math.trunc(setting['player.volume']! * 100) })
}
if (keys.includes('player.isMute')) {
global.lx.event_app.player_status({ mute: setting['player.isMute'] })
}
})
global.lx.event_app.on('app_inited', () => {
setProxy()
global.lx.event_app.player_status({ volume: Math.trunc(global.lx.appSetting['player.volume'] * 100) })
global.lx.event_app.player_status({ mute: global.lx.appSetting['player.isMute'] })
})
}
const initTheme = () => {
@@ -256,6 +286,20 @@ const initTheme = () => {
})
}
const backupDB = (backupPath: string) => {
const dbPath = path.join(global.lxDataPath, 'lx.data.db')
try {
renameSync(dbPath, backupPath)
} catch {}
try {
renameSync(`${dbPath}-wal`, `${backupPath}-wal`)
} catch {}
try {
renameSync(`${dbPath}-shm`, `${backupPath}-shm`)
} catch {}
openDirInExplorer(backupPath)
}
let isInitialized = false
export const initAppSetting = async() => {
if (!global.lx.inited) {
@@ -268,19 +312,19 @@ export const initAppSetting = async() => {
if (!isInitialized) {
let dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)
if (dbFileExists === null) {
const backPath = path.join(global.lxDataPath, `lx.data.db.${Date.now()}.bak`)
const backupPath = path.join(global.lxDataPath, `lx.data.db.${Date.now()}.bak`)
dialog.showMessageBoxSync({
type: 'warning',
message: 'Database verify failed',
detail: `数据库表结构校验失败,我们将把有问题的数据库备份到:${backPath}\n若此问题导致你的数据丢失你可以尝试从备份文件找回它们。\n\nThe database table structure verification failed, we will back up the problematic database to: ${backPath}\nIf this problem causes your data to be lost, you can try to retrieve them from the backup file.`,
detail: `数据库表结构校验失败,我们将把有问题的数据库备份到:${backupPath}\n若此问题导致你的数据丢失你可以尝试从备份文件找回它们。\n\nThe database table structure verification failed, we will back up the problematic database to: ${backupPath}\nIf this problem causes your data to be lost, you can try to retrieve them from the backup file.`,
})
renameSync(path.join(global.lxDataPath, 'lx.data.db'), backPath)
openDirInExplorer(backPath)
backupDB(backupPath)
dbFileExists = await global.lx.worker.dbService.init(global.lxDataPath)
}
global.lx.appSetting = (await initSetting()).setting
if (!dbFileExists) await migrateDBData().catch(err => { log.error(err) })
initTheme()
if (envParams.cmdParams.dt == null) envParams.cmdParams.dt = !global.lx.appSetting['common.transparentWindow']
}
// global.lx.theme = getTheme()

View File

@@ -16,6 +16,7 @@ import registerModules from '@main/modules'
// 初始化应用
const init = () => {
console.log('init')
if (process.env.BUILD_WIN7 == 'true') import('./utils/winLegacy')
void initAppSetting().then(() => {
registerModules()
global.lx.event_app.app_inited()

View File

@@ -52,6 +52,15 @@ const handleSendStatus = (res: http.ServerResponse<http.IncomingMessage>, query?
for (const k of keys) resp[k] = global.lx.player_status[k]
sendResponse(res, 200, resp, 'application/json; charset=utf-8')
}
const handleSendAllLyric = (res: http.ServerResponse<http.IncomingMessage>) => {
const resp: Partial<Record<SubscribeKeys, any>> = {
lyric: global.lx.player_status.lyric,
tlyric: global.lx.player_status.tlyric,
rlyric: global.lx.player_status.rlyric,
lxlyric: global.lx.player_status.lxlyric,
}
sendResponse(res, 200, resp, 'application/json; charset=utf-8')
}
const handleSubscribePlayerStatus = (req: http.IncomingMessage, res: http.ServerResponse<http.IncomingMessage>, query?: string) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
@@ -130,6 +139,9 @@ const handleStartServer = async(port: number, ip: string) => new Promise<void>((
case '/lyric':
msg = global.lx.player_status.lyric
break
case '/lyric-all':
handleSendAllLyric(res)
return
case '/play':
sendTaskbarButtonClick('play')
break
@@ -142,12 +154,44 @@ const handleStartServer = async(port: number, ip: string) => new Promise<void>((
case '/skip-prev':
sendTaskbarButtonClick('prev')
break
case '/seek': {
const offset = parseFloat(querystring.parse(query ?? '').offset as string)
if (Number.isNaN(offset) || offset < 0 || offset > global.lx.player_status.duration) {
code = 400
msg = 'Invalid offset'
} else {
sendTaskbarButtonClick('seek', parseFloat(offset.toFixed(3)))
}
break
}
case '/collect':
sendTaskbarButtonClick('collect')
break
case '/uncollect':
sendTaskbarButtonClick('unCollect')
break
case '/volume': {
const volume = parseInt(querystring.parse(query ?? '').volume as string)
if (Number.isNaN(volume) || volume < 0 || volume > 100) {
code = 400
msg = 'Invalid volume'
} else {
sendTaskbarButtonClick('volume', volume / 100)
}
break
}
case '/mute': {
const mute = querystring.parse(query ?? '').mute
if (mute == 'true') {
sendTaskbarButtonClick('mute', true)
} else if (mute == 'false') {
sendTaskbarButtonClick('mute', false)
} else {
code = 400
msg = 'Invalid mute value'
}
break
}
case '/subscribe-player-status':
try {
handleSubscribePlayerStatus(req, res, query)

View File

@@ -151,6 +151,7 @@ let client: LX.Sync.Client.Socket | null
// let listSyncPromise: Promise<void>
export const connect = (urlInfo: LX.Sync.Client.UrlInfo, keyInfo: LX.Sync.ClientKeyInfo) => {
client = new WebSocket(`${urlInfo.wsProtocol}//${urlInfo.hostPath}/socket?i=${encodeURIComponent(keyInfo.clientId)}&t=${encodeURIComponent(aesEncrypt(SYNC_CODE.msgConnect, keyInfo.key))}`, {
followRedirects: true,
}) as LX.Sync.Client.Socket
client.data = {
keyInfo,

View File

@@ -3,10 +3,9 @@ import { httpFetch, type RequestOptions } from '@main/utils/request'
import { decodeData, encodeData } from '../utils'
export const request = async(url: string, options: RequestOptions = { }) => {
return httpFetch(url, {
return httpFetch<string>(url, {
...options,
timeout: options.timeout ?? 10000,
follow_max: 5,
}).then(response => {
return {
text: response.body,

View File

@@ -116,6 +116,27 @@ const messages = {
music_name: '標題: ',
music_singer: '演出者: ',
},
'ko-kr': {
collect: '즐겨찾기 추가',
uncollect: '즐겨찾기 해제',
play: '재생',
pause: '일시정지',
next: '다음 곡',
prev: '이전 곡',
hide_win_main: '메인 창 숨기기',
show_win_main: '메인 창 표시',
hide_win_lyric: '가사 창 닫기',
show_win_lyric: '가사 창 열기',
lock_win_lyric: '가사 창 잠금',
unlock_win_lyric: '가사 창 잠금 해제',
top_win_lyric: '가사 항상 위에 표시',
untop_win_lyric: '항상 위에 표시 해제',
show_statusbar_lyric: '상태 표시줄에 가사 표시',
hide_statusbar_lyric: '상태 표시줄 가사 숨기기',
exit: '종료',
music_name: '제목: ',
music_singer: '아티스트: ',
},
} as const
type Messages = typeof messages
type Langs = keyof Messages
@@ -150,9 +171,11 @@ export const createTray = () => {
// tray.setToolTip('LX Music')
// createMenu()
tray.setIgnoreDoubleClickEvents(true)
tray.on('click', () => {
showMainWindow()
})
if (!isMac) {
tray.on('click', () => {
showMainWindow()
})
}
}
export const destroyTray = () => {

View File

@@ -109,14 +109,20 @@ const inflateScript = async(script: string) => new Promise<string>((resolve, rej
})
export const importApi = async(scriptRaw: string): Promise<LX.UserApi.UserApiInfo> => {
let scriptInfo = parseScriptInfo(scriptRaw)
const script = await deflateScript(scriptRaw)
userApis ??= []
for (const api of userApis) {
const existingScript = scripts.get(api.id)
if (existingScript === script) {
throw new Error(`导入失败,脚本内容与已有的源「${api.name}」相同`)
}
}
const apiInfo = {
id: `user_api_${Math.random().toString().substring(2, 5)}_${Date.now()}`,
...scriptInfo,
allowShowUpdateAlert: true,
}
userApis ??= []
userApis.push(apiInfo)
const script = await deflateScript(scriptRaw)
scripts.set(apiInfo.id, script)
saveData()
return apiInfo

View File

@@ -1,7 +1,8 @@
import { isLinux } from '@common/utils'
import { closeWindow, createWindow, getBounds, isExistWindow, alwaysOnTopTools, setBounds, setIgnoreMouseEvents, setSkipTaskbar } from './main'
import { sendConfigChange } from './rendererEvent'
import { sendConfigChange, sendMouseLeave } from './rendererEvent'
import { buildLyricConfig, getLyricWindowBounds, initWindowSize, watchConfigKeys } from './utils'
import { mouseCheckTools } from './mouseCheckTools'
let isLock: boolean
let isEnable: boolean
@@ -21,14 +22,21 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
isLock = global.lx.appSetting['desktopLyric.isLock']
if (global.lx.appSetting['desktopLyric.isLock']) {
setIgnoreMouseEvents(true, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })
mouseCheckTools.runCheck(sendMouseLeave)
} else {
setIgnoreMouseEvents(false, { forward: !isLinux && global.lx.appSetting['desktopLyric.isHoverHide'] })
mouseCheckTools.cacnelCheck()
}
}
if (keys.includes('desktopLyric.isHoverHide') && isHoverHide != global.lx.appSetting['desktopLyric.isHoverHide']) {
isHoverHide = global.lx.appSetting['desktopLyric.isHoverHide']
if (!isLinux) {
setIgnoreMouseEvents(global.lx.appSetting['desktopLyric.isLock'], { forward: global.lx.appSetting['desktopLyric.isHoverHide'] })
setIgnoreMouseEvents(global.lx.appSetting['desktopLyric.isLock'], { forward: isHoverHide })
if (isHoverHide) {
mouseCheckTools.runCheck(sendMouseLeave)
} else {
mouseCheckTools.cacnelCheck()
}
}
}
if (keys.includes('desktopLyric.isAlwaysOnTop') && isAlwaysOnTop != global.lx.appSetting['desktopLyric.isAlwaysOnTop']) {
@@ -54,7 +62,7 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
if (keys.includes('desktopLyric.isLockScreen') && isLockScreen != global.lx.appSetting['desktopLyric.isLockScreen']) {
isLockScreen = global.lx.appSetting['desktopLyric.isLockScreen']
if (global.lx.appSetting['desktopLyric.isLockScreen']) {
setBounds(getLyricWindowBounds(getBounds(), {
setBounds(getLyricWindowBounds(getBounds()!, {
x: 0,
y: 0,
w: global.lx.appSetting['desktopLyric.width'],
@@ -77,6 +85,7 @@ export const setLrcConfig = (keys: Array<keyof LX.AppSetting>, setting: Partial<
createWindow()
} else {
alwaysOnTopTools.clearLoop()
mouseCheckTools.cacnelCheck()
closeWindow()
}
}

View File

@@ -172,8 +172,8 @@ export const sendEvent = <T = any>(name: string, params?: T) => {
mainSend(browserWindow, name, params)
}
export const getBounds = (): Electron.Rectangle => {
if (!browserWindow) throw new Error('window is not available')
export const getBounds = (): Electron.Rectangle | null => {
if (!browserWindow) return null
return browserWindow.getBounds()
}

View File

@@ -0,0 +1,57 @@
import { screen } from 'electron'
import { getBounds } from './main'
import { isLinux } from '@common/utils'
export const mouseCheckTools = {
timer: null as NodeJS.Timeout | null,
isMouseInWindow: false,
sendMouseLeave: null as (() => void) | null,
onInBoundsChange(isInBounds: boolean) {
if (isInBounds) return
this.stop()
this.sendMouseLeave?.()
},
checkMousePosition() {
const bounds = getBounds()
if (!bounds) return
const point = screen.getCursorScreenPoint()
const isInBounds = point.x >= bounds.x && point.x <= bounds.x + bounds.width &&
point.y >= bounds.y && point.y <= bounds.y + bounds.height
if (isInBounds !== this.isMouseInWindow) {
this.isMouseInWindow = isInBounds
this.onInBoundsChange(isInBounds)
}
},
start() {
this.timer = setTimeout(() => {
this.timer = null
this.checkMousePosition()
this.start()
}, 500)
},
stop() {
if (!this.timer) return
clearTimeout(this.timer)
this.timer = null
// console.log('stop check')
},
setMouseInWindow(isInWindow: boolean) {
this.isMouseInWindow = isInWindow
},
runCheck(sendMouseLeave: () => void) {
if (isLinux || !global.lx.appSetting['desktopLyric.isLock'] || !global.lx.appSetting['desktopLyric.isHoverHide'] || !this.isMouseInWindow) return
// console.log('run check')
this.sendMouseLeave = sendMouseLeave
this.stop()
this.start()
},
cacnelCheck() {
this.stop()
},
}

View File

@@ -5,6 +5,7 @@ import { buildLyricConfig, getLyricWindowBounds } from './utils'
import { sendNewDesktopLyricClient } from '@main/modules/winMain'
import { getBounds, getMainFrame, sendEvent, setBounds, setResizeable } from './main'
import { MessageChannelMain } from 'electron'
import { mouseCheckTools } from './mouseCheckTools'
export default () => {
@@ -26,7 +27,7 @@ export default () => {
})
mainOn<LX.DesktopLyric.NewBounds>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_bounds, ({ params: options }) => {
setBounds(getLyricWindowBounds(getBounds(), options))
setBounds(getLyricWindowBounds(getBounds()!, options))
})
mainOn<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.set_win_resizeable, ({ params: resizable }) => {
@@ -45,6 +46,16 @@ export default () => {
// without going through the main process!
console.log('request_main_window_channel')
})
mainOn<boolean>(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, ({ params: isEnter }) => {
if (isEnter) {
mouseCheckTools.setMouseInWindow(true)
mouseCheckTools.runCheck(sendMouseLeave)
} else {
mouseCheckTools.setMouseInWindow(false)
mouseCheckTools.cacnelCheck()
}
})
}
export const sendConfigChange = (setting: Partial<LX.DesktopLyric.Config>) => {
@@ -55,3 +66,6 @@ export const sendMainWindowInitedEvent = () => {
sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.main_window_inited)
}
export const sendMouseLeave = () => {
sendEvent(WIN_LYRIC_RENDERER_EVENT_NAME.mouse_enter_leave, false)
}

View File

@@ -81,6 +81,7 @@ export const watchConfigKeys = [
'common.langId',
'player.isShowLyricTranslation',
'player.isShowLyricRoma',
'player.isSwapLyricTranslationAndRoma',
'player.isPlayLxlrc',
'player.playbackRate',
] satisfies Array<keyof LX.AppSetting>

View File

@@ -1,7 +1,7 @@
import { BrowserWindow, dialog, session } from 'electron'
import path from 'node:path'
import { createTaskBarButtons, getWindowSizeInfo } from './utils'
import { getPlatform, isLinux, isWin } from '@common/utils'
import { getOSVersion, getPlatform, isLinux, isMac, isWin } from '@common/utils'
import { getProxy, openDevTools as handleOpenDevTools } from '@main/utils'
import { mainSend } from '@common/mainIpc'
import { sendFocus, sendTaskbarButtonClick } from './rendererEvent'
@@ -40,10 +40,25 @@ const winEvent = () => {
browserWindow.on('blur', () => {
global.lx.event_app.main_window_blur()
})
browserWindow.on('enter-full-screen', () => {
global.lx.event_app.main_window_fullscreen(true)
})
browserWindow.on('leave-full-screen', () => {
global.lx.event_app.main_window_fullscreen(false)
// macOS needs here to set resizable to false after exiting full screen
if (isMac) {
if (browserWindow?.resizable) {
browserWindow.setResizable(false)
}
}
})
browserWindow.once('ready-to-show', () => {
showWindow()
setThumbarButtons()
if (!global.envParams.cmdParams.hidden) {
showWindow()
setThumbarButtons()
}
global.lx.event_app.main_window_ready_to_show()
})
@@ -66,11 +81,7 @@ export const createWindow = () => {
const { shouldUseDarkColors, theme } = global.lx.theme
const ses = session.fromPartition('persist:win-main')
const proxy = getProxy()
if (proxy) {
void ses.setProxy({
proxyRules: `http://${proxy.host}:${proxy.port}`,
})
}
setSesProxy(ses, proxy?.host, proxy?.port)
/**
* Initial window options
@@ -109,7 +120,7 @@ export const createWindow = () => {
browserWindow = new BrowserWindow(options)
const winURL = process.env.NODE_ENV !== 'production' ? 'http://localhost:9080' : `file://${path.join(encodePath(__dirname), 'index.html')}`
void browserWindow.loadURL(winURL + `?os=${getPlatform()}&dt=${global.envParams.cmdParams.dt}&dark=${shouldUseDarkColors}&theme=${encodeURIComponent(JSON.stringify(theme))}`)
void browserWindow.loadURL(winURL + `?os=${getPlatform()}&osver=${encodeURIComponent(getOSVersion())}&dt=${global.envParams.cmdParams.dt}&dark=${shouldUseDarkColors}&theme=${encodeURIComponent(JSON.stringify(theme))}`)
winEvent()
@@ -131,18 +142,22 @@ export const closeWindow = () => {
browserWindow.close()
}
const setSesProxy = (ses: Electron.Session, host?: string, port?: string | number) => {
if (host) {
void ses.setProxy({
mode: 'fixed_servers',
proxyRules: `http://${host}:${port}`,
})
} else {
void ses.setProxy({
mode: 'direct',
})
}
}
export const setProxy = () => {
if (!browserWindow) return
const proxy = getProxy()
if (proxy) {
void browserWindow.webContents.session.setProxy({
proxyRules: `http://${proxy.host}:${proxy.port}`,
})
} else {
void browserWindow.webContents.session.setProxy({
proxyRules: '',
})
}
setSesProxy(browserWindow.webContents.session, proxy?.host, proxy?.port)
}
@@ -226,13 +241,17 @@ export const toggleDevTools = () => {
export const setFullScreen = (isFullscreen: boolean): boolean => {
if (!browserWindow) return false
if (isLinux) { // linux 需要先设置为可调整窗口大小才能全屏
// https://github.com/any-listen/any-listen/issues/190
// in electron ^41.2.0, windows -dt mode need to set resizable to true before setting full screen
if (!!global.envParams.cmdParams.dt || isLinux) {
// linux 需要先设置为可调整窗口大小才能全屏
if (isFullscreen) {
browserWindow.setResizable(isFullscreen)
browserWindow.setFullScreen(isFullscreen)
} else {
browserWindow.setFullScreen(isFullscreen)
browserWindow.setResizable(isFullscreen)
// windows/linux need to set resizable to true after exiting full screen
if (!isMac) browserWindow.setResizable(isFullscreen)
}
} else {
browserWindow.setFullScreen(isFullscreen)

View File

@@ -137,8 +137,8 @@ export const sendFocus = () => {
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.focus)
}
export const sendTaskbarButtonClick = (action: LX.Player.StatusButtonActions) => {
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, action)
export const sendTaskbarButtonClick = (action: LX.Player.StatusButtonActions, data?: unknown) => {
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.player_action_on_button_click, { action, data })
}
export const sendConfigChange = (setting: Partial<LX.AppSetting>) => {
sendEvent(WIN_MAIN_RENDERER_EVENT_NAME.on_config_change, setting)

View File

@@ -3,8 +3,6 @@ import { registerRendererEvents as list } from '@main/modules/commonRenderers/li
import { registerRendererEvents as dislike } from '@main/modules/commonRenderers/dislike'
import app, { sendConfigChange } from './app'
import hotKey from './hotKey'
import kw_decodeLyric from './kw_decodeLyric'
import tx_decodeLyric from './tx_decodeLyric'
import userApi from './userApi'
import sync from './sync'
import data from './data'
@@ -30,8 +28,6 @@ export default () => {
dislike(sendEvent)
app()
hotKey()
kw_decodeLyric()
tx_decodeLyric()
userApi()
sync()
data()

View File

@@ -1,53 +0,0 @@
import { inflate } from 'zlib'
import iconv from 'iconv-lite'
import { mainHandle } from '@common/mainIpc'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
const handleInflate = async(data: Buffer) => {
return new Promise((resolve: (result: Buffer) => void, reject) => {
inflate(data, (err, result) => {
if (err) {
reject(err)
return
}
resolve(result)
})
})
}
const buf_key = Buffer.from('yeelion')
const buf_key_len = buf_key.length
const decodeLyric = async(buf: Buffer, isGetLyricx: boolean) => {
// const info = buf.slice(0, index).toString()
// if (!info.startsWith('tp=content')) return null
// const isLyric = info.includes('\r\nlrcx=0\r\n')
if (buf.toString('utf8', 0, 10) != 'tp=content') return ''
// const index = buf.indexOf('\r\n\r\n') + 4
const lrcData = await handleInflate(buf.subarray(buf.indexOf('\r\n\r\n') + 4))
if (!isGetLyricx) return iconv.decode(lrcData, 'gb18030')
const buf_str = Buffer.from(lrcData.toString(), 'base64')
const buf_str_len = buf_str.length
const output = new Uint8Array(buf_str_len)
let i = 0
while (i < buf_str_len) {
let j = 0
while (j < buf_key_len && i < buf_str_len) {
output[i] = buf_str[i] ^ buf_key[j]
i++
j++
}
}
return iconv.decode(Buffer.from(output), 'gb18030')
}
export default () => {
mainHandle<{ lrcBase64: string, isGetLyricx: boolean }, string>(WIN_MAIN_RENDERER_EVENT_NAME.handle_kw_decode_lyric, async({ params: { lrcBase64, isGetLyricx } }) => {
const lrc = await decodeLyric(Buffer.from(lrcBase64, 'base64'), isGetLyricx)
return Buffer.from(lrc).toString('base64')
})
}

View File

@@ -1,61 +0,0 @@
import { createInflate, constants as zlibConstants } from 'node:zlib'
// import path from 'path'
import { mainHandle } from '@common/mainIpc'
import { WIN_MAIN_RENDERER_EVENT_NAME } from '@common/ipcNames'
// eslint-disable-next-line @typescript-eslint/dot-notation, @typescript-eslint/quotes
// const require = module[`require`].bind(module)
let qrc_decode: (buf: Buffer, len: number) => Buffer
const inflate = async(lrcBuf: Buffer) => new Promise<string>((resolve, reject) => {
const buffer_builder: Buffer[] = []
const decompress_stream = createInflate()
.on('data', (chunk) => {
buffer_builder.push(chunk)
})
.on('close', () => {
resolve(Buffer.concat(buffer_builder).toString())
})
.on('error', (err: any) => {
// console.log(err)
if (err.errno !== zlibConstants.Z_BUF_ERROR) { // EOF: expected
reject(err)
}
})
// decompress_stream.write(lrcBuf)
decompress_stream.end(lrcBuf)
})
const decode = async(str: string): Promise<string> => {
if (!str) return ''
const buf = Buffer.from(str, 'hex')
return inflate(qrc_decode(buf, buf.length))
}
// 感谢某位不愿透露姓名的大佬提供的C++算法源码,但由于作者不希望公开,所以将会以预构建二进制文件的形式加入代码仓库中
const handleDecode = async(lrc: string, tlrc: string, rlrc: string) => {
if (!qrc_decode) {
// const nativeBindingPath = path.join(__dirname, '../build/Release/qrc_decode.node')
// const nativeBindingPath = process.env.NODE_ENV !== 'production' ? path.join(__dirname, '../build/Release/qrc_decode.node')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const addon = require('qrc_decode.node')
// console.log(addon)
qrc_decode = addon.qrc_decode
}
const [lyric, tlyric, rlyric] = await Promise.all([decode(lrc), decode(tlrc), decode(rlrc)])
return {
lyric,
tlyric,
rlyric,
}
}
export default () => {
mainHandle<{ lrc: string, tlrc: string, rlrc: string }, { lyric: string, tlyric: string, rlyric: string }>(WIN_MAIN_RENDERER_EVENT_NAME.handle_tx_decode_lyric, async({ params: { lrc, tlrc, rlrc } }) => {
return handleDecode(lrc, tlrc, rlrc)
})
}

View File

@@ -5,10 +5,10 @@
"./types"
],
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@common/*": ["common/*"],
"@main/*": ["main/*"],
"@static/*": ["static/*"],
"@/*": ["./*"],
"@common/*": ["../common/*"],
"@main/*": ["../main/*"],
"@static/*": ["../static/*"],
"@/*": ["../*"],
},
},
}

View File

@@ -1,4 +1,4 @@
import { encodePath, isUrl, throttle } from '@common/utils'
import { encodePath, isUrl, throttle, isMac } from '@common/utils'
import migrateSetting from '@common/utils/migrateSetting'
import getStore from '@main/utils/store'
import { STORE_NAMES, URL_SCHEME_RXP } from '@common/constants'
@@ -9,11 +9,11 @@ import { nativeTheme, powerSaveBlocker } from 'electron'
import { joinPath } from '@common/utils/nodejs'
import themes from '@common/theme/index.json'
export const parseEnvParams = (): { cmdParams: LX.CmdParams, deeplink: string | null } => {
export const parseEnvParams = (argv = process.argv): { cmdParams: LX.CmdParams, deeplink: string | null } => {
const cmdParams: LX.CmdParams = {}
let deeplink = null
const rx = /^-\w+/
for (let param of process.argv) {
for (let param of argv) {
if (URL_SCHEME_RXP.test(param)) {
deeplink = param
}
@@ -107,6 +107,11 @@ export const mergeSetting = (originSetting: LX.AppSetting, targetSetting?: Parti
}
}
const applyInitSetting = (setting: LX.AppSetting) => {
if (global.envParams.cmdParams.hidden && !setting['tray.enable']) {
setting['tray.enable'] = true
}
}
export const updateSetting = (setting?: Partial<LX.AppSetting>, isInit: boolean = false) => {
const electronStore_config = getStore(STORE_NAMES.APP_SETTINGS)
@@ -114,6 +119,7 @@ export const updateSetting = (setting?: Partial<LX.AppSetting>, isInit: boolean
let originSetting: LX.AppSetting
if (isInit) {
setting &&= migrateSetting(setting)
applyInitSetting(setting as LX.AppSetting)
originSetting = { ...defaultSetting }
} else originSetting = global.lx.appSetting
@@ -257,7 +263,7 @@ export const getTheme = () => {
theme.config.extInfo['--background-image'] =
isUrl(theme.config.extInfo['--background-image'])
? `url(${theme.config.extInfo['--background-image']})`
: `url(file:///${encodePath(joinPath(global.lxDataPath, 'theme_images', theme.config.extInfo['--background-image']))})`
: `url(${encodePath(joinPath(global.lxDataPath, 'theme_images', theme.config.extInfo['--background-image']))})`
}
} else {
themeId = global.lx.appSetting['theme.id'] == 'auto' && shouldUseDarkColors ? 'black' : 'green'
@@ -287,7 +293,7 @@ export const setPowerSaveBlocker = (enabled: boolean) => {
let isEnabled = powerSaveBlockerId != null && powerSaveBlocker.isStarted(powerSaveBlockerId)
if (enabled) {
if (isEnabled) return
powerSaveBlockerId = powerSaveBlocker.start('prevent-app-suspension')
powerSaveBlockerId = powerSaveBlocker.start(isMac ? 'prevent-display-sleep' : 'prevent-app-suspension')
} else {
if (!isEnabled) return
powerSaveBlocker.stop(powerSaveBlockerId!)

View File

@@ -1,8 +1,5 @@
import needle, { type NeedleHttpVerbs, type NeedleOptions, type BodyData, type NeedleCallback, type NeedleResponse } from 'needle'
// import progress from 'request-progress'
import { httpOverHttp, httpsOverHttp } from 'tunnel'
import { type ClientRequest } from 'node:http'
import { getProxy } from './index'
import { request, type Options } from '@common/utils/request'
// import fs from 'fs'
export const requestMsg = {
@@ -14,53 +11,6 @@ export const requestMsg = {
cancelRequest: '取消http请求',
} as const
const httpsRxp = /^https:/
const getRequestAgent = (url: string) => {
const proxy = getProxy()
return proxy ? (httpsRxp.test(url) ? httpsOverHttp : httpOverHttp)({ proxy }) : undefined
}
export interface RequestOptions extends NeedleOptions {
method?: NeedleHttpVerbs
body?: BodyData
form?: BodyData
formData?: BodyData
}
export type RequestCallback = NeedleCallback
type RequestResponse = NeedleResponse
const request = (url: string, options: RequestOptions, callback: RequestCallback): ClientRequest => {
let data: BodyData = null
if (options.body) {
data = options.body
} else if (options.form) {
data = options.form
// data.content_type = 'application/x-www-form-urlencoded'
options.json = false
} else if (options.formData) {
data = options.formData
// data.content_type = 'multipart/form-data'
options.json = false
}
options.response_timeout = options.timeout
return needle.request(options.method ?? 'get', url, data, options, (err, resp, body) => {
if (!err) {
body = resp.body = resp.raw.toString()
try {
resp.body = JSON.parse(resp.body)
} catch (_) {}
body = resp.body
}
callback(err, resp, body)
// @ts-expect-error
}).request
}
const defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36',
}
// var proxyUrl = "http://" + user + ":" + password + "@" + host + ":" + port;
// var proxiedRequest = request.defaults({'proxy': proxyUrl});
@@ -68,70 +18,13 @@ const defaultHeaders = {
// abort: () => void
// }
/**
* promise 形式的请求方法
* @param {*} url
* @param {*} options
*/
const buildHttpPromose = async(url: string, options: RequestOptions): Promise<RequestResponse> => {
return new Promise((resolve, reject) => {
void fetchData(url, options.method, options, (err, resp, body) => {
// options.isShowProgress && window.api.hideProgress()
// debugRequest && console.log(`\n---response------${url}------------`)
// debugRequest && console.log(body)
// obj.requestObj = null
// obj.cancelFn = null
if (err) {
reject(err)
return
}
resolve(resp)
})
// .then(request => {
// // obj.requestObj = ro
// // if (obj.isCancelled) obj.cancelHttp()
// promise.abort = () => {
// request.destroy(new Error('cancelled'))
// }
// })
})
// let obj = {
// isCancelled: false,
// cancelHttp: () => {
// if (!obj.requestObj) return obj.isCancelled = true
// cancelHttp(obj.requestObj)
// obj.requestObj = null
// obj.promise = obj.cancelHttp = null
// obj.cancelFn(new Error(requestMsg.cancelRequest))
// obj.cancelFn = null
// },
// }
// obj.promise = new Promise((resolve, reject) => {
// obj.cancelFn = reject
// debugRequest && console.log(`\n---send request------${url}------------`)
// fetchData(url, options.method, options, (err, resp, body) => {
// // options.isShowProgress && window.api.hideProgress()
// debugRequest && console.log(`\n---response------${url}------------`)
// debugRequest && console.log(body)
// obj.requestObj = null
// obj.cancelFn = null
// if (err) { reject(err); return }
// resolve(resp)
// }).then(ro => {
// obj.requestObj = ro
// if (obj.isCancelled) obj.cancelHttp()
// })
// })
// return obj
}
/**
* 请求超时自动重试
* @param {*} url
* @param {*} options
*/
export const httpFetch = async(url: string, options: RequestOptions = { method: 'get' }) => {
return buildHttpPromose(url, options).catch(async(err: any) => {
export const httpFetch = async<T = unknown> (url: string, options: Options) => {
return request<T>(url, options).catch(async(err: any) => {
// console.log('出错', err)
if (err.message === 'socket hang up') {
// window.globalObj.apiSource = 'temp'
@@ -166,24 +59,4 @@ export const httpFetch = async(url: string, options: RequestOptions = { method:
// return requestPromise
}
const fetchData = async(url: string, method: RequestOptions['method'], {
headers = {},
format = 'json',
timeout = 15000,
...options
}, callback: RequestCallback) => {
// console.log(url, options)
console.log('---start---', url)
headers = Object.assign({}, headers)
return request(url, {
...options,
method,
headers: Object.assign({}, defaultHeaders, headers),
timeout,
agent: getRequestAgent(url),
json: format === 'json',
}, (err, resp, body) => {
callback(err, resp, body)
})
}
export type RequestOptions = Options

View File

@@ -0,0 +1,44 @@
import os from 'node:os'
import { dialog } from 'electron'
import { STORE_NAMES } from '@common/constants'
import getStore from '@main/utils/store'
import { openUrl } from '@common/utils/electron'
const showWinLegacyMessage = () => {
if (process.platform !== 'win32') return
const storeKey = 'winLegacyMessageShown'
const dataStore = getStore(STORE_NAMES.DATA)
const count = dataStore.get<number>(storeKey) || 0
if (count > 2) return
const osVersion = os.release()
const majorVersion = parseInt(osVersion.split('.')[0], 10)
const isOldWindows = majorVersion < 10
if (isOldWindows) {
const result = dialog.showMessageBoxSync({
type: 'warning',
title: 'Windows 旧版提示',
message: '您正在使用专为旧版 Windows 准备的 Any Listen它仅用于实在无法升级到新版系统的用户。\n该版本缺乏安全更新可能存在安全风险并且功能未经测试可能存在兼容性问题。\n\n如果您的设备支持 Windows 10 或更高版本,强烈建议升级系统,切换到标准版本的 Any Listen以获得更好的安全性和性能。',
buttons: ['知道了'],
})
if (result === 0) {
dataStore.set(storeKey, count + 1)
}
} else {
const result = dialog.showMessageBoxSync({
type: 'warning',
title: '您正在使用旧版',
message: '您正在使用专为旧版 Windows 准备的 Any Listen它仅用于实在无法升级到新版系统的用户但您的系统似乎在标准版的支持范围内为了获得最佳体验请切换到 Any Listen 的标准版本。\n\n注意当前版本缺乏安全更新可能存在安全风险并且功能未经测试可能存在兼容性问题。',
buttons: ['跳转新版发布页', '知道了'],
})
if (result === 0) {
void openUrl('https://github.com/lyswhut/lx-music-desktop/releases')
} else if (result === 1) {
dataStore.set('winLegacyMessageShown', count + 1)
}
}
}
showWinLegacyMessage()

View File

@@ -36,6 +36,7 @@ export const init = (lxDataPath: string): boolean | null => {
initTables(db)
dbFileExists = false
}
db.pragma('journal_mode = WAL')
if (dbFileExists) migrateData(db)

View File

@@ -17,6 +17,7 @@ import {
createMusicInfoOrderClearStatement,
createMusicInfoByListAndMusicInfoIdQueryStatement,
createMusicInfoByMusicInfoIdQueryStatement,
createMusicInfoOrderStatement,
} from './statements'
const idFixRxp = /\.0$/
@@ -368,3 +369,14 @@ export const overwriteListData = (lists: LX.DBService.UserListInfo[], musicInfos
})(lists, musicInfos)
}
/**
* 获取列表内音乐的排序
* @param listId 列表id
* @param musicInfoId 音乐id
* @returns 音乐排序信息
*/
export const getMusicInfoOrder = (listId: string, musicInfoId: string) => {
const musicInfoOrderStatement = createMusicInfoOrderStatement()
return musicInfoOrderStatement.get({ listId, musicInfoId })
}

View File

@@ -18,10 +18,12 @@ import {
updateMusicInfoOrder,
updateMusicInfos,
updateUserLists as updateUserListsFromDB,
getMusicInfoOrder,
} from './dbHelper'
let userLists: LX.DBService.UserListInfo[]
let musicLists = new Map<string, LX.Music.MusicInfo[]>()
let rawPoss = new Map<string, number>()
const toDBMusicInfo = (musicInfos: LX.Music.MusicInfo[], listId: string, offset: number = 0): LX.DBService.MusicInfo[] => {
return musicInfos.map((info, index) => {
@@ -41,8 +43,10 @@ const toDBMusicInfo = (musicInfos: LX.Music.MusicInfo[], listId: string, offset:
export const getAllUserList = (): LX.List.UserListInfo[] => {
userLists ??= queryAllUserList()
rawPoss.clear()
return userLists.map(list => {
const { position, ...newList } = list
rawPoss.set(list.id, position)
return newList
})
}
@@ -55,10 +59,15 @@ export const getAllUserList = (): LX.List.UserListInfo[] => {
export const createUserLists = (position: number, lists: LX.List.UserListInfo[]) => {
userLists ??= queryAllUserList()
if (position < 0 || position >= userLists.length) {
// 如果是最末尾,那么取最后一个列表的原始位置 + 1 作为新列表的原始位置,否则新列表的原始位置为 position
// 因为原始 pos 可能比 userLists.length 大,所以不能直接用 userLists.length 作为新列表的原始位置
const order = userLists.length ? (rawPoss.get(userLists.at(-1)!.id) ?? userLists.length) + 1 : 0
const newLists: LX.DBService.UserListInfo[] = lists.map((list, index) => {
const pos = order + index
rawPoss.set(list.id, pos)
return {
...list,
position: position + index,
position: pos,
}
})
insertUserLists(newLists)
@@ -72,6 +81,8 @@ export const createUserLists = (position: number, lists: LX.List.UserListInfo[])
})
insertUserLists(newUserLists, true)
userLists = newUserLists
rawPoss.clear()
for (const list of userLists) rawPoss.set(list.id, list.position)
}
}
@@ -97,6 +108,7 @@ export const createUserLists = (position: number, lists: LX.List.UserListInfo[])
export const removeUserLists = (ids: string[]) => {
deleteUserLists(ids)
userLists &&= queryAllUserList()
for (const id of ids) rawPoss.delete(id)
}
/**
@@ -147,6 +159,8 @@ export const updateUserListsPosition = (position: number, ids: string[]) => {
})
insertUserLists(newUserLists, true)
userLists = newUserLists
rawPoss.clear()
for (const list of userLists) rawPoss.set(list.id, list.position)
}
/**
@@ -210,10 +224,14 @@ export const musicsAdd = (listId: string, musicInfos: LX.Music.MusicInfo[], addM
arrUnshift(targetList, musicInfos)
break
case 'bottom':
default:
insertMusicInfoList(toDBMusicInfo(musicInfos, listId, targetList.length))
default: {
// 如果是添加到最后,那么取最后一个歌曲的原始位置 + 1 作为新歌曲的原始位置,否则新歌曲的原始位置为 position
// 因为原始 pos 可能比 targetList.length 大,所以不能直接用 targetList.length 作为新歌曲的原始位置
const order = targetList.length ? (getMusicInfoOrder(listId, targetList.at(-1)!.id)?.order ?? targetList.length) + 1 : 0
insertMusicInfoList(toDBMusicInfo(musicInfos, listId, order))
arrPush(targetList, musicInfos)
break
}
}
}
@@ -257,10 +275,14 @@ export const musicsMove = (fromId: string, toId: string, musicInfos: LX.Music.Mu
arrUnshift(toList, musicInfos)
break
case 'bottom':
default:
moveMusicInfo(fromId, ids, toDBMusicInfo(musicInfos, toId, toList.length))
default:{
// 如果是添加到最后,那么取最后一个歌曲的原始位置 + 1 作为新歌曲的原始位置,否则新歌曲的原始位置为 position
// 因为原始 pos 可能比 targetList.length 大,所以不能直接用 targetList.length 作为新歌曲的原始位置
const order = toList.length ? (getMusicInfoOrder(toId, toList.at(-1)!.id)?.order ?? toList.length) + 1 : 0
moveMusicInfo(fromId, ids, toDBMusicInfo(musicInfos, toId, order))
arrPush(toList, musicInfos)
break
}
}
listSet = new Set<string>(ids)
@@ -363,6 +385,8 @@ export const listDataOverwrite = (myListData: MakeOptional<LX.List.ListDataFull,
if (userLists) userLists.splice(0, userLists.length, ...dbLists)
else userLists = dbLists
rawPoss.clear()
for (const list of userLists) rawPoss.set(list.id, list.position)
musicLists.clear()
musicLists.set(LIST_IDS.DEFAULT, listData.defaultList)
musicLists.set(LIST_IDS.LOVE, listData.loveList)

View File

@@ -145,6 +145,17 @@ export const createMusicInfoByMusicInfoIdQueryStatement = () => {
}
/**
* 创建根据列表Id与音乐id查询音乐排序语句
* @returns 查询语句
*/
export const createMusicInfoOrderStatement = () => {
const db = getDB()
return db.prepare<[LX.DBService.ListMusicInfoQuery], LX.DBService.MusicInfoOrder>(`SELECT "listId", "musicInfoId", "order"
FROM "main"."my_list_music_info_order"
WHERE "listId"=@listId AND "musicInfoId"=@musicInfoId`)
}
/**
* 创建音乐信息排序插入语句
* @returns 插入语句

View File

@@ -1,6 +1,6 @@
<template>
<div id="container" :class="[{ lock: setting['desktopLyric.isLock'] }, { hide: isHide || (isHoverHide && isMouseEnter) }]">
<div id="main" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @mousemove="handleMouseMoveMain">
<div id="container" :class="[{ lock: setting['desktopLyric.isLock'] }, { hide: isHide || isHoverHide }]">
<div id="main">
<transition enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut">
<div v-show="!setting['desktopLyric.isLock']" class="control-bar">
<layout-control-bar />
@@ -41,7 +41,7 @@ import usePauseHide from '@lyric/useApp/usePauseHide'
const isShowResize = window.os != 'windows'
useCommon()
const { handleMouseDown, handleTouchDown } = useWindowSize()
const { handleMouseMoveMain, isHoverHide, isMouseEnter } = useHoverHide()
const isHoverHide = useHoverHide()
useLyric()
useTheme()
const isHide = usePauseHide()
@@ -87,6 +87,10 @@ body {
}
&.hide {
opacity: .05;
&:not(.lock):hover {
opacity: 1;
}
}
}

View File

@@ -41,8 +41,9 @@ export const setPlaybackRate = (rate: number) => {
export const setLyric = () => {
if (!musicInfo.id) return
const extendedLyrics = []
if (setting['player.isShowLyricTranslation'] && lyrics.tlyric) extendedLyrics.push(lyrics.tlyric)
if (setting['player.isShowLyricRoma'] && lyrics.rlyric) extendedLyrics.push(lyrics.rlyric)
if (setting['player.isShowLyricTranslation'] && lyrics.tlyric) extendedLyrics.push(lyrics.tlyric)
if (setting['player.isSwapLyricTranslationAndRoma']) extendedLyrics.reverse()
lrc.setLyric(
setting['player.isPlayLxlrc'] && lyrics.lxlyric ? lyrics.lxlyric : lyrics.lyric,
extendedLyrics,

View File

@@ -34,6 +34,7 @@ export const setting = shallowReactive<LX.DesktopLyric.Config>({
'common.langId': 'zh-cn',
'player.isShowLyricTranslation': false,
'player.isShowLyricRoma': false,
'player.isSwapLyricTranslationAndRoma': false,
'player.isPlayLxlrc': false,
'player.playbackRate': 1,
})

View File

@@ -3,11 +3,11 @@
"compilerOptions": {
"isolatedModules": true,
"paths": { /* Specify a set of entries that re-map imports to additional lookup locations. */
"@common/*": ["common/*"],
"@common/*": ["../common/*"],
// "@renderer/*": ["renderer/*"],
"@lyric/*": ["renderer-lyric/*"],
"@static/*": ["static/*"],
"@root/*": ["./*"],
"@lyric/*": ["../renderer-lyric/*"],
"@static/*": ["../static/*"],
"@root/*": ["../*"],
},
"typeRoots": [ /* Specify multiple folders that act like './node_modules/@types'. */
"./types"

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