diff --git a/CHANGELOG.md b/CHANGELOG.md index 11100d9..b7cc986 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +## v0.7.0 + +### ✨ 新功能 +- **音乐云盘**:新增云盘页面,可浏览、播放云盘中的歌曲,查看文件详情(文件名、大小、比特率、上传时间),删除云盘歌曲,查看存储空间使用情况 +- **云盘上传**:支持上传本地音频文件到云盘,上传过程显示实时进度,支持 mp3/flac/wav/ogg/aac/m4a 格式 +- **下载音乐**:本地音乐拆分为「本地音乐」和「下载音乐」两个独立页面,下载音乐只显示通过应用下载的歌曲 +- **本地音乐多文件夹**:本地音乐支持添加多个扫描文件夹,通过三点按钮+弹窗管理文件夹路径 +- **歌手关注**:歌手详情页新增关注/取关按钮,关注状态在离开页面后不会丢失 +- **粘性导航栏**:页面滚动较深时顶部自动显示返回按钮和功能按钮,渐变模糊效果,不影响阅读 +- **骨架屏加载**:首页、歌单、歌手、专辑、云盘等多个页面加载时显示骨架占位动画,不再只有"加载中"文字 + +### 🐛 修复 +- 网络较差时播放启动超时,音乐实际已开始播放但界面仍显示暂停 +- 全屏漫游抽屉打开时点击评论按钮无法切换到评论页 +- 关闭漫游抽屉后再打开,始终显示评论而非歌词 +- 歌手详情页关注后离开再回来,关注状态丢失 +- 切歌时偶尔触发上一首歌的播放结束事件导致异常 +- 评论点赞无限叠加(改为服务端状态驱动) +- 播放栏进度条上方多余分隔线 + +### 🎨 变更 +- 歌手详情页头像改为圆形,简介从独立标签页移至头部内嵌显示,溢出时可展开查看完整介绍 +- 歌单详情页描述溢出时显示"查看完整介绍"按钮 +- 首页推荐和排行榜加载失败时显示重试按钮,支持分别重试 +- 多个页面的返回按钮统一为粘性导航栏组件 +- 消息提示增加去重和数量限制,避免重复弹出 + +### ⚡ 优化 +- 页面切换更流畅,路由全部改为懒加载 +- 页面缓存管理优化,30 秒未访问自动释放,多级跳转时保留导航链上的页面,「我喜欢的音乐」常驻缓存 +- 本地音乐扫描不再阻塞界面导航 +- 应用启动不再等待网络请求完成 + + ## v0.6.0 ### ✨ 新功能 diff --git a/README.md b/README.md index 2c9242e..750eaff 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,102 @@ +
+ # Nekosonic -一款轻量的跨平台音乐播放器,支持 Windows / Linux / macOS,音源源自网易云音乐。 +轻量跨平台桌面音乐播放器 · 网易云音乐 -## ✨ 特性 +[![Windows](https://img.shields.io/badge/Windows-0078D4?logo=windows11&logoColor=white)](https://github.com/atdunbg/Nekosonic-Music/releases) +[![Linux](https://img.shields.io/badge/Linux-FCC624?logo=linux&logoColor=black)](https://github.com/atdunbg/Nekosonic-Music/releases) +[![macOS](https://img.shields.io/badge/macOS-000000?logo=apple&logoColor=white)](https://github.com/atdunbg/Nekosonic-Music/releases) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -### 播放 +[下载安装](https://github.com/atdunbg/Nekosonic-Music/releases) -- 🎵 在线音乐播放,流式缓冲边下边播 -- 🎵 多音质选择(标准 / 较高 / 极高 HQ / 无损 SQ / Hi-Res) -- 🔄 播放模式切换(列表循环 / 随机播放 / 单曲循环) -- ⏯ 播放控制(播放 / 暂停 / 上一首 / 下一首 / 进度跳转 / 音量调节) -- 📋 播放队列管理(查看队列 / 移除歌曲 / 清空队列) -- 📻 私人漫游 FM(个性化推荐,VIP 试听自动跳过) -- 🎵 本地音乐播放(支持 mp3 / flac / wav / ogg / aac / m4a / wma / opus) -- 🔊 音频输出设备选择 -- 🎧 系统媒体控制(蓝牙耳机/键盘媒体键/系统面板,支持 Linux / Windows / macOS) +--- -### 发现与浏览 +
-- 🔍 关键词搜索歌曲 + 热门搜索标签 -- 📋 歌单浏览(推荐歌单 / 排行榜 / 用户歌单 / 收藏歌单) -- 📋 歌单详情(歌曲列表 + 收藏 / 取消收藏 + 歌单评论) -- 🎤 歌手详情(热门歌曲 / 专辑 / 简介) -- 💿 专辑详情(歌曲列表 + 播放全部) -- 📅 每日推荐歌曲 +## 🎵 播放 -### 歌词与评论 +- 多音质选择(标准 / 较高 / HQ / SQ / Hi-Res) +- 私人漫游 FM(个性化推荐) +- 系统媒体控制集成(MPRIS / SMTC / Now Playing) +- 音频输出设备选择 -- 🎤 实时滚动歌词(自动滚动 / 点击跳转 / 渐变透明度) -- 🎤 歌词翻译显示 -- 🎤 全屏漫游模式(大封面 + 歌词 / 评论双标签页) -- 💬 歌曲评论查看(热门评论 + 无限滚动加载 + 点赞) +## 🔍 发现 -### 收藏与下载 +- 关键词搜索(歌曲 / 歌手 / 专辑)+ 搜索建议 + 热门搜索 +- 歌单浏览(推荐 / 排行榜 / 用户 / 收藏) +- 歌手详情(热门歌曲 / 专辑 / 简介 + 关注) +- 专辑详情 +- 每日推荐歌曲 -- ❤️ 一键喜欢 / 取消喜欢(同步到网易云账号) -- ⬇️ 歌曲下载(带进度显示 / VIP 拦截 / 元数据保存) -- 🎵 本地音乐管理(列出 / 播放 / 删除 / 音频元数据与封面读取) -- 🕐 本地播放历史记录(最多 200 首) +## 🎤 歌词与评论 -### 账号 +- 实时滚动歌词(ease-out 缓动 / 点击跳转 / 渐变透明度) +- 歌词翻译 +- 全屏漫游模式(封面主色提取 + 歌词/评论双标签) +- 歌曲评论(无限滚动 + 点赞) -- 🔴 网易云账号登录(二维码扫码 / 手机号密码) -- 🔑 登录态持久化(重启后自动恢复) +## ❤️ 收藏与下载 -### 系统与设置 +- 一键喜欢 / 取消喜欢(同步到网易云账号) +- 歌曲下载 +- 音乐云盘(上传 / 删除 / 详情 / 存储空间 / 上传进度) +- 本地音乐(多文件夹扫描 / 封面补全) +- 下载音乐(独立管理 / 删除) -- 📡 系统托盘(播放控制 / 显示窗口 / 退出) -- 🛡 单实例运行(防止重复启动) -- ⌨️ 自定义快捷键(应用内 + 系统全局) -- 🎨 多主题切换(天蓝 / 翠绿 / 玫红 / 紫罗兰 / 橙色 / 青色 / 粉色) -- ⚙️ 关闭窗口行为设置(每次询问 / 最小化到托盘 / 直接退出) -- 🔄 自动更新(启动静默检测 + 自定义弹窗 + 忽略版本 + 下载进度) -- 📝 更新日志查看 -- 📶 网络状态检测(断网/恢复 Toast 提示 + 自动重试加载) +## 🎨 个性化 -## 📦️ 安装 +- 多主题色(天蓝 / 翠绿 / 玫红 / 紫罗兰 / 橙色 / 青色 / 粉色) +- 自定义快捷键(应用内 + 系统全局) +- 关闭行为设置 +- 自动更新 -访问本项目的 [Releases](https://github.com/atdunbg/Nekosonic-Music/releases) 页面下载安装包。 +--- -## 💻 配置开发环境 +## 安装 + +前往 [Releases](https://github.com/atdunbg/Nekosonic-Music/releases) 下载对应平台安装包。 + +## 配置开发环境 ```bash -# 安装前端依赖 npm install - -# 启动开发服务器 -npm run tauri dev - -# 构建发布 -npm run tauri build +npm run tauri dev # 开发 +npm run tauri build # 构建 ``` -### 环境要求 +> 环境要求:Node.js ≥ 18 · Rust ≥ 1.70 · Tauri CLI 2 -- Node.js >= 18 -- Rust >= 1.70 -- Tauri CLI 2 - -## 🛠 技术栈 +## 技术栈 | 层级 | 技术 | -|------|------| +|:------|:------| | 桌面框架 | Tauri 2 | -| 前端 | Vue 3 + TypeScript | -| 样式 | Tailwind CSS v4 + CSS 变量主题系统 | -| 状态管理 | Pinia | -| 路由 | Vue Router 4 | -| 音频解码 | symphonia + ringbuf (Rust) | -| 媒体控制 | souvlaki (Linux MPRIS / Windows SMTC / macOS Now Playing) | +| 前端 | Vue 3 + TypeScript + Pinia | +| 样式 | Tailwind CSS v4 + CSS 变量主题 | +| 音频解码 | symphonia + ringbuf | +| 媒体控制 | souvlaki | | 网易云 API | ncm-api-rs | -| 构建工具 | Vite 6 | +| 构建 | Vite 6 | -## ☑️ Todo +## Todo -- [x] 评论系统 +- [x] 评论查看 - [x] 歌曲下载 -- [x] 本地音乐管理 +- [x] 本地音乐 - [x] 歌手详情页 - [x] 专辑详情页 - [x] 自定义全局快捷键 - [x] 自动更新 - [x] 歌词翻译 - [x] 更多主题 -- [x] 系统媒体控制(蓝牙耳机/键盘媒体键) +- [x] 音乐云盘 - [ ] MV 播放 -- [ ] 音乐云盘 - [ ] 桌面歌词 欢迎提 Issue 和 Pull request。 -## 📜 开源许可 +## 开源许可 -本项目仅供个人学习研究使用,禁止用于商业及非法用途。 - -基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。 - -## 致谢 - -- [ncm-api-rs](https://crates.io/crates/ncm-api-rs) — 网易云音乐 API 的 Rust 封装 -- [Tauri](https://tauri.app/) — 跨平台桌面应用框架 -- [Vue.js](https://vuejs.org/) — 渐进式 JavaScript 框架 -- [Tailwind CSS](https://tailwindcss.com/) — 实用优先的 CSS 框架 -- [symphonia](https://crates.io/crates/symphonia) — Rust 纯音频解码库 -- [souvlaki](https://crates.io/crates/souvlaki) — 跨平台 OS 媒体控制库 +本项目仅供个人学习研究使用,禁止用于商业及非法用途。基于 [MIT License](https://opensource.org/licenses/MIT) 开源。 diff --git a/package.json b/package.json index a3fe3b9..266903b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "nekosonic", "private": true, - "version": "0.6.0", + "version": "0.7.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fa3fc0e..9629829 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,13 +4,14 @@ version = 4 [[package]] name = "Nekosonic" -version = "0.6.0" +version = "0.7.0" dependencies = [ "base64 0.22.1", "cpal", "dirs 5.0.1", "futures-util", "lofty", + "md5", "ncm-api-rs", "raw-window-handle", "reqwest 0.12.28", @@ -78,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.11.1", + "bitflags 2.12.1", "cfg-if", "libc", ] @@ -353,9 +354,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -381,7 +382,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cexpr", "clang-sys", "itertools", @@ -389,7 +390,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] @@ -416,9 +417,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" dependencies = [ "serde_core", ] @@ -471,9 +472,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.2" +version = "8.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -482,19 +483,28 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] -name = "bumpalo" -version = "3.20.2" +name = "bs58" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -523,7 +533,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cairo-sys-rs", "glib", "libc", @@ -595,14 +605,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.61" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -817,7 +827,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-graphics-types 0.2.0", "foreign-types 0.5.0", @@ -841,7 +851,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "libc", ] @@ -941,7 +951,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.13.1", + "phf", "smallvec", ] @@ -1166,7 +1176,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "libc", "objc2", @@ -1174,9 +1184,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1292,9 +1302,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "embed-resource" @@ -1849,7 +1859,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "futures-channel", "futures-core", "futures-executor", @@ -1898,9 +1908,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "global-hotkey" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" +checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" dependencies = [ "crossbeam-channel", "keyboard-types", @@ -1994,9 +2004,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -2040,9 +2050,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -2079,9 +2089,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -2130,7 +2140,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.6.4", "tokio", "tower-service", "tracing", @@ -2303,7 +2313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2353,16 +2363,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -2506,9 +2506,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.97" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -2544,7 +2544,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "serde", "unicode-segmentation", ] @@ -2631,9 +2631,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -2705,9 +2705,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "lru-slab" @@ -2755,10 +2755,16 @@ dependencies = [ ] [[package]] -name = "memchr" -version = "2.8.0" +name = "md5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memoffset" @@ -2808,9 +2814,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -2819,9 +2825,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" dependencies = [ "crossbeam-channel", "dpi", @@ -2869,7 +2875,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "jni-sys 0.3.1", "log", "ndk-sys 0.5.0+25.2.9519653", @@ -2883,7 +2889,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "jni-sys 0.3.1", "log", "ndk-sys 0.6.0+11769913", @@ -2962,9 +2968,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -3054,7 +3060,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "objc2", "objc2-core-foundation", @@ -3067,7 +3073,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-foundation", ] @@ -3088,7 +3094,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", ] @@ -3099,7 +3105,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -3132,7 +3138,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", "objc2-core-graphics", @@ -3159,7 +3165,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "libc", "objc2", @@ -3172,7 +3178,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", ] @@ -3183,7 +3189,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-app-kit", "objc2-foundation", @@ -3195,7 +3201,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3207,7 +3213,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "objc2", "objc2-cloud-kit", @@ -3238,7 +3244,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "objc2", "objc2-app-kit", @@ -3271,9 +3277,9 @@ dependencies = [ [[package]] name = "ogg_pager" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6d1ca8364b84e0cf725eed06b1460c44671e6c0fb28765f5262de3ece07fdc" +checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216" dependencies = [ "byteorder", ] @@ -3286,9 +3292,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "5.3.4" +version = "5.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" dependencies = [ "dunce", "is-wsl", @@ -3413,24 +3419,14 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - [[package]] name = "phf" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.13.1", - "phf_shared 0.13.1", + "phf_macros", + "phf_shared", "serde", ] @@ -3440,18 +3436,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.6", + "phf_generator", + "phf_shared", ] [[package]] @@ -3461,20 +3447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand 2.4.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.117", + "phf_shared", ] [[package]] @@ -3483,22 +3456,13 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.117", ] -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.13.1" @@ -3584,7 +3548,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "crc32fast", "fdeflate", "flate2", @@ -3708,7 +3672,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -3762,9 +3726,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.3" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721da970c312655cde9b4ffe0547f20a8494866a4af5ff51f18b7c633d0c870b" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", ] @@ -3782,7 +3746,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -3819,7 +3783,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.6.4", "tracing", "windows-sys 0.60.2", ] @@ -3916,7 +3880,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", ] [[package]] @@ -4042,9 +4006,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", @@ -4183,7 +4147,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4196,7 +4160,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -4219,9 +4183,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -4370,7 +4334,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4393,12 +4357,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "cssparser", "derive_more", "log", "new_debug_unreachable", - "phf 0.13.1", + "phf", "phf_codegen", "precomputed-hash", "rustc-hash", @@ -4471,9 +4435,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -4525,11 +4489,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -4544,9 +4509,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling", "proc-macro2", @@ -4613,6 +4578,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -4685,9 +4656,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4798,7 +4769,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.13.1", + "phf_shared", "precomputed-hash", ] @@ -4808,8 +4779,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -5066,11 +5037,11 @@ dependencies = [ [[package]] name = "tao" -version = "0.35.2" +version = "0.35.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "block2", "core-foundation 0.10.1", "core-graphics 0.25.0", @@ -5117,9 +5088,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -5134,9 +5105,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.0" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" dependencies = [ "anyhow", "bytes", @@ -5162,7 +5133,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest 0.13.3", + "reqwest 0.13.4", "serde", "serde_json", "serde_repr", @@ -5185,9 +5156,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.0" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" dependencies = [ "anyhow", "cargo_toml", @@ -5206,9 +5177,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.0" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" dependencies = [ "base64 0.22.1", "brotli", @@ -5233,9 +5204,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.0" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5247,9 +5218,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.6.0" +version = "2.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8d5f58bfd0cdcfdbc0a68dc08b354eea2afc551b421de91b07b69e0dd769d57" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" dependencies = [ "anyhow", "glob", @@ -5305,9 +5276,9 @@ dependencies = [ [[package]] name = "tauri-plugin-global-shortcut" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" +checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b" dependencies = [ "global-hotkey", "log", @@ -5337,7 +5308,7 @@ dependencies = [ "thiserror 2.0.18", "url", "windows 0.61.3", - "zbus 5.15.0", + "zbus 5.16.0", ] [[package]] @@ -5362,7 +5333,7 @@ dependencies = [ "thiserror 2.0.18", "tracing", "windows-sys 0.60.2", - "zbus 5.15.0", + "zbus 5.16.0", ] [[package]] @@ -5381,7 +5352,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest 0.13.3", + "reqwest 0.13.4", "rustls", "semver", "serde", @@ -5400,9 +5371,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.0" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" dependencies = [ "cookie", "dpi", @@ -5425,9 +5396,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.0" +version = "2.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" dependencies = [ "gtk", "http", @@ -5451,9 +5422,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" dependencies = [ "anyhow", "brotli", @@ -5467,7 +5438,7 @@ dependencies = [ "json-patch", "log", "memchr", - "phf 0.11.3", + "phf", "plist", "proc-macro2", "quote", @@ -5619,9 +5590,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.2" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5629,7 +5600,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.4", "tokio-macros", "windows-sys 0.61.2", ] @@ -5707,7 +5678,7 @@ dependencies = [ "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5763,14 +5734,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5779,7 +5750,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -5805,20 +5776,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -5900,9 +5871,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uds_windows" @@ -5964,9 +5935,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -6025,9 +5996,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -6118,9 +6089,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -6131,9 +6102,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.70" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -6141,9 +6112,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6151,9 +6122,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -6164,9 +6135,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.120" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -6225,7 +6196,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap 2.14.0", "semver", @@ -6233,9 +6204,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.97" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -6257,7 +6228,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" dependencies = [ - "phf 0.13.1", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", @@ -6916,9 +6887,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -6997,7 +6968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.12.1", "indexmap 2.14.0", "log", "serde", @@ -7143,9 +7114,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -7207,9 +7178,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.15.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast 0.7.2", "async-executor", @@ -7234,10 +7205,10 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.2", - "zbus_macros 5.15.0", + "winnow 1.0.3", + "zbus_macros 5.16.0", "zbus_names 4.3.2", - "zvariant 5.11.0", + "zvariant 5.12.0", ] [[package]] @@ -7256,17 +7227,17 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.15.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", "zbus_names 4.3.2", - "zvariant 5.11.0", - "zvariant_utils 3.3.1", + "zvariant 5.12.0", + "zvariant_utils 3.4.0", ] [[package]] @@ -7287,24 +7258,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 1.0.2", - "zvariant 5.11.0", + "winnow 1.0.3", + "zvariant 5.12.0", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -7313,9 +7284,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -7405,16 +7376,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", - "winnow 1.0.2", - "zvariant_derive 5.11.0", - "zvariant_utils 3.3.1", + "winnow 1.0.3", + "zvariant_derive 5.12.0", + "zvariant_utils 3.4.0", ] [[package]] @@ -7432,15 +7403,15 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.11.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", "quote", "syn 2.0.117", - "zvariant_utils 3.3.1", + "zvariant_utils 3.4.0", ] [[package]] @@ -7456,13 +7427,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", "syn 2.0.117", - "winnow 1.0.2", + "winnow 1.0.3", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d824053..c021572 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Nekosonic" -version = "0.6.0" +version = "0.7.0" description = "A Simple music app" authors = ["atdunbg"] edition = "2021" @@ -33,6 +33,7 @@ futures-util = "0.3" dirs = "5" lofty = "0.22" base64 = "0.22" +md5 = "0.7" ncm-api-rs = "0.1" tokio = { version = "1", features = ["rt", "sync"] } diff --git a/src-tauri/src/api.rs b/src-tauri/src/api.rs index 9f9bd5e..15dff53 100644 --- a/src-tauri/src/api.rs +++ b/src-tauri/src/api.rs @@ -2,6 +2,7 @@ use ncm_api_rs::{create_client, ApiClient, Query}; use serde::Deserialize; use serde_json::json; use tauri::{Manager, State, Emitter}; +use tokio::sync::Mutex as AsyncMutex; use std::sync::Mutex as StdMutex; use std::sync::atomic::Ordering; @@ -44,14 +45,14 @@ use base64::Engine; /// ``` macro_rules! api_call { ($state:expr, $method:ident) => {{ - let client = $state.client.lock().unwrap().clone(); + let client = $state.client.lock().await.clone(); let q = $state.build_query(); client.$method(&q).await .map(|r| r.body.to_string()) .map_err(|e| e.to_string()) }}; ($state:expr, $method:ident, params: [$(($key:expr, $val:expr)),* $(,)?]) => {{ - let client = $state.client.lock().unwrap().clone(); + let client = $state.client.lock().await.clone(); let mut q = $state.build_query(); $(q = q.param($key, $val);)* client.$method(&q).await @@ -59,7 +60,7 @@ macro_rules! api_call { .map_err(|e| e.to_string()) }}; ($state:expr, $method:ident, query: $q:expr) => {{ - let client = $state.client.lock().unwrap().clone(); + let client = $state.client.lock().await.clone(); client.$method(&$q).await .map(|r| r.body.to_string()) .map_err(|e| e.to_string()) @@ -67,7 +68,7 @@ macro_rules! api_call { } pub struct ApiController { - client: StdMutex, + client: AsyncMutex, cookie: StdMutex>, cookie_path: PathBuf, } @@ -94,7 +95,7 @@ impl ApiController { let client = create_client(None); ApiController { - client: StdMutex::new(client), + client: AsyncMutex::new(client), cookie: StdMutex::new(saved_cookie), cookie_path, } @@ -111,11 +112,10 @@ impl ApiController { query } /// 将 Cookie 字符串持久化到本地文件并同步到 API 客户端 - fn save_cookie(&self, cookie_str: &str) { + async fn save_cookie(&self, cookie_str: &str) { let _ = fs::write(&self.cookie_path, cookie_str); - if let Ok(mut client) = self.client.lock() { - client.set_cookie(cookie_str.to_string()); - } + let mut client = self.client.lock().await; + client.set_cookie(cookie_str.to_string()); } } @@ -191,7 +191,7 @@ pub struct SongUrlQuery { pub id: u64, pub level: Option, pub fm_mode: O /// 获取歌曲播放地址(返回完整 data 对象,包含 url、freeTrialInfo 等) #[tauri::command] pub async fn get_song_url(query: SongUrlQuery, state: State<'_, ApiController>) -> Result { - let client = state.client.lock().unwrap().clone(); + let client = state.client.lock().await.clone(); let level = query.level.as_deref().unwrap_or("standard"); let resp = if query.fm_mode.unwrap_or(false) { @@ -251,7 +251,7 @@ pub async fn get_playlist_detail(id: u64, state: State<'_, ApiController>) -> Re /// 手机号密码登录 #[tauri::command] pub async fn login(query: LoginQuery, state: State<'_, ApiController>) -> Result { - let client = state.client.lock().unwrap().clone(); + let client = state.client.lock().await.clone(); let q = Query::new() .param("phone", &query.phone) .param("password", &query.password); @@ -260,7 +260,7 @@ pub async fn login(query: LoginQuery, state: State<'_, ApiController>) -> Result if !resp.cookie.is_empty() { let cookie_str = cookies_to_key_values(&resp.cookie); *state.cookie.lock().map_err(|e| e.to_string())? = Some(cookie_str.clone()); - state.save_cookie(&cookie_str); + state.save_cookie(&cookie_str).await; } Ok(resp.body.to_string()) @@ -277,7 +277,7 @@ pub async fn logout(state: State<'_, ApiController>) -> Result<(), String> { /// 获取二维码登录密钥 #[tauri::command] pub async fn get_qr_key(state: State<'_, ApiController>) -> Result { - let client = state.client.lock().unwrap().clone(); + let client = state.client.lock().await.clone(); let q = state.build_query(); let resp = client.login_qr_key(&q).await.map_err(|e| e.to_string())?; resp.body["unikey"] @@ -292,7 +292,7 @@ pub async fn create_qr( query: QrKeyQuery, state: State<'_, ApiController>, ) -> Result { - let client = state.client.lock().unwrap().clone(); + let client = state.client.lock().await.clone(); let q = state .build_query() .param("key", &query.key) @@ -308,13 +308,13 @@ pub async fn create_qr( /// 检查二维码扫码状态 #[tauri::command] pub async fn check_qr_status(query: QrKeyQuery, state: State<'_, ApiController>) -> Result { - let client = state.client.lock().unwrap().clone(); + let client = state.client.lock().await.clone(); let q = state.build_query().param("key", &query.key); let resp = client.login_qr_check(&q).await.map_err(|e| e.to_string())?; if resp.body["code"].as_u64() == Some(803) && !resp.cookie.is_empty() { let cookie_str = cookies_to_key_values(&resp.cookie); *state.cookie.lock().map_err(|e| e.to_string())? = Some(cookie_str.clone()); - state.save_cookie(&cookie_str); + state.save_cookie(&cookie_str).await; } Ok(resp.body.to_string()) } @@ -500,7 +500,7 @@ pub async fn download_song( let q = state.build_query() .param("id", &query.id.to_string()) .param("level", level); - let client = state.client.lock().unwrap().clone(); + let client = state.client.lock().await.clone(); let resp = client.song_url_v1(&q).await.map_err(|e| e.to_string())?; let data = &resp.body["data"][0]; let url = data["url"].as_str().filter(|s| !s.is_empty()); @@ -582,18 +582,17 @@ pub async fn download_song( Ok(filename) } -/// 列出本地已下载的歌曲,优先使用元数据文件补充信息 -#[tauri::command] -pub fn list_local_songs(app_handle: tauri::AppHandle, download_path: Option) -> Result, String> { - let download_dir = resolve_download_dir(&app_handle, download_path.as_deref()); - if !download_dir.exists() { +/// 扫描指定目录下的音频文件,优先使用元数据文件补充信息 +/// `downloaded_only` 为 true 时,只返回有对应 .json 元数据的文件(即通过应用下载的) +fn scan_dir_for_songs(dir: &PathBuf, downloaded_only: bool) -> Result, String> { + if !dir.exists() { return Ok(Vec::new()); } let audio_exts = ["mp3", "flac", "wav", "ogg", "aac", "m4a", "wma", "opus"]; let mut meta_map: std::collections::HashMap = std::collections::HashMap::new(); - let entries = fs::read_dir(&download_dir).map_err(|e| format!("读取目录失败: {}", e))?; + let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {}", e))?; for entry in entries.flatten() { let path = entry.path(); @@ -609,7 +608,7 @@ pub fn list_local_songs(app_handle: tauri::AppHandle, download_path: Option = Vec::new(); - let entries = fs::read_dir(&download_dir).map_err(|e| format!("读取目录失败: {}", e))?; + let entries = fs::read_dir(dir).map_err(|e| format!("读取目录失败: {}", e))?; for entry in entries.flatten() { let path = entry.path(); @@ -621,6 +620,11 @@ pub fn list_local_songs(app_handle: tauri::AppHandle, download_path: Option) -> Result, String> { + let download_dir = resolve_download_dir(&app_handle, download_path.as_deref()); + tokio::task::spawn_blocking(move || { + scan_dir_for_songs(&download_dir, true) // 只显示下载的歌曲 + }).await.map_err(|e| format!("扫描任务失败: {}", e))? +} + +/// 扫描多个本地文件夹中的音频文件 +#[tauri::command] +pub async fn scan_local_folders(paths: Vec) -> Result, String> { + tokio::task::spawn_blocking(move || { + let mut all_songs: Vec = Vec::new(); + let mut seen_paths: std::collections::HashSet = std::collections::HashSet::new(); + + for p in &paths { + let dir = PathBuf::from(p); + let songs = scan_dir_for_songs(&dir, false)?; // 本地音乐:显示所有音频 + for song in songs { + if seen_paths.insert(song.path.clone()) { + all_songs.push(song); + } + } + } + + Ok(all_songs) + }).await.map_err(|e| format!("扫描任务失败: {}", e))? +} + /// 读取音频文件的元数据(标题、艺术家、专辑、时长、封面) fn read_audio_metadata(path: &PathBuf) -> (String, String, String, u64, Option) { match lofty::read_from_path(path) { @@ -743,29 +777,33 @@ pub struct DeleteLocalSongQuery { /// 删除本地已下载的歌曲文件及其元数据 #[tauri::command] -pub fn delete_local_song( +pub async fn delete_local_song( app_handle: tauri::AppHandle, query: DeleteLocalSongQuery, ) -> Result<(), String> { let download_dir = resolve_download_dir(&app_handle, query.download_path.as_deref()); - let file_path = download_dir.join(&query.filename); - let meta_path = download_dir.join(format!("{}.json", query.id)); + tokio::task::spawn_blocking(move || { + let file_path = download_dir.join(&query.filename); + let meta_path = download_dir.join(format!("{}.json", query.id)); - if file_path.exists() { - fs::remove_file(&file_path).map_err(|e| format!("删除文件失败: {}", e))?; - } - if meta_path.exists() { - fs::remove_file(&meta_path).map_err(|e| format!("删除元数据失败: {}", e))?; - } - Ok(()) + if file_path.exists() { + fs::remove_file(&file_path).map_err(|e| format!("删除文件失败: {}", e))?; + } + if meta_path.exists() { + fs::remove_file(&meta_path).map_err(|e| format!("删除元数据失败: {}", e))?; + } + Ok(()) + }).await.map_err(|e| format!("删除任务失败: {}", e))? } /// 检查指定歌曲是否已下载到本地 #[tauri::command] -pub fn check_local_song(app_handle: tauri::AppHandle, id: u64, download_path: Option) -> Result { +pub async fn check_local_song(app_handle: tauri::AppHandle, id: u64, download_path: Option) -> Result { let download_dir = resolve_download_dir(&app_handle, download_path.as_deref()); - let meta_path = download_dir.join(format!("{}.json", id)); - Ok(meta_path.exists()) + tokio::task::spawn_blocking(move || { + let meta_path = download_dir.join(format!("{}.json", id)); + Ok(meta_path.exists()) + }).await.map_err(|e| format!("检查任务失败: {}", e))? } /// 解析下载目录,优先使用自定义路径,否则使用默认目录 @@ -795,7 +833,7 @@ fn get_default_download_dir(app_handle: &tauri::AppHandle) -> PathBuf { /// 获取默认下载路径字符串,供前端使用 #[tauri::command] -pub fn get_default_download_path(app_handle: tauri::AppHandle) -> String { +pub async fn get_default_download_path(app_handle: tauri::AppHandle) -> String { get_default_download_dir(&app_handle).to_string_lossy().to_string() } @@ -867,6 +905,28 @@ pub async fn artist_desc(id: u64, state: State<'_, ApiController>) -> Result } + +#[tauri::command] +pub async fn artist_sub(query: ArtistSubQuery, state: State<'_, ApiController>) -> Result { + let t = if query.sub.unwrap_or(true) { "1" } else { "0" }; + api_call!(state, artist_sub, params: [("id", &query.id.to_string()), ("t", t)]) +} + +/// 获取已关注的歌手列表 +#[derive(Deserialize)] +pub struct ArtistSublistQuery { pub limit: Option, pub offset: Option } + +#[tauri::command] +pub async fn artist_sublist(query: ArtistSublistQuery, state: State<'_, ApiController>) -> Result { + let q = state.build_query() + .param("limit", &query.limit.unwrap_or(100).to_string()) + .param("offset", &query.offset.unwrap_or(0).to_string()); + api_call!(state, artist_sublist, query: &q) +} + /// 获取专辑详情 #[tauri::command] pub async fn album_detail(id: u64, state: State<'_, ApiController>) -> Result { @@ -977,3 +1037,288 @@ pub struct CommentLikeQuery { pub id: u64, pub cid: u64, } + +// ==================== 云盘 ==================== + +/// 获取云盘列表 +#[tauri::command] +pub async fn user_cloud(limit: Option, offset: Option, state: State<'_, ApiController>) -> Result { + api_call!(state, user_cloud, params: [("limit", &limit.unwrap_or(30).to_string()), ("offset", &offset.unwrap_or(0).to_string())]) +} + +/// 获取云盘歌曲详情 +#[tauri::command] +pub async fn user_cloud_detail(id: String, state: State<'_, ApiController>) -> Result { + api_call!(state, user_cloud_detail, params: [("id", &id)]) +} + +/// 删除云盘歌曲 +#[tauri::command] +pub async fn user_cloud_del(id: u64, state: State<'_, ApiController>) -> Result { + api_call!(state, user_cloud_del, params: [("id", &id.to_string())]) +} + +/// 查询 NOS LBS 获取上传节点域名 +/// +/// 通过 `http://wannos.127.net/lbs` 查询指定 bucket 的上传节点, +/// 从返回的 `nosup-.127.net` 中提取区域标识, +/// 构造 multipart upload 所需的 `.nos-.163yun.com` 域名。 +async fn query_nos_upload_host(bucket: &str) -> Result { + let lbs_url = format!("http://wannos.127.net/lbs?version=1.0&bucketname={}", bucket); + let http = reqwest::Client::new(); + let resp = http.get(&lbs_url).send().await + .map_err(|e| format!("LBS查询请求失败: {}", e))?; + + let lbs_data: serde_json::Value = resp.json().await + .map_err(|e| format!("LBS响应解析失败: {}", e))?; + + let nosup_url = lbs_data["upload"][0].as_str() + .ok_or_else(|| format!("LBS响应缺少upload字段: {}", lbs_data))?; + + // 从 "nosup-jd1.127.net" 中提取区域 "jd" + let region = extract_nos_region(nosup_url)?; + Ok(format!("https://{}.nos-{}.163yun.com", bucket, region)) +} + +/// 从 nosup 上传节点 URL 中提取 NOS 区域标识 +/// +/// 例如: `http://nosup-jd1.127.net` → `jd` +/// `http://nosup-hz1.127.net` → `hz` +fn extract_nos_region(nosup_url: &str) -> Result { + let start = nosup_url.find("nosup-") + .ok_or_else(|| format!("无法从LBS响应中解析区域: {}", nosup_url))?; + let after = &nosup_url[start + 6..]; + let dot = after.find('.') + .ok_or_else(|| format!("无法从LBS响应中解析区域: {}", nosup_url))?; + let region_with_num = &after[..dot]; + // 去掉末尾数字: "jd1" → "jd" + let region: String = region_with_num + .chars() + .take_while(|c| !c.is_ascii_digit()) + .collect(); + if region.is_empty() { + return Err(format!("区域标识为空: {}", nosup_url)); + } + Ok(region) +} + +/// 云盘上传:完整流程(检查 → [获取Token → LBS查询 → NOS上传] → 提交信息 → 发布) +/// +/// 关键发现:upload check 返回的 songId 是十六进制字符串(如 MD5 摘要),不是数字 ID。 +/// needUpload=false 表示文件已在 NOS 上,无需重复上传,直接走 info+pub 即可。 +/// 参考 ydq/netease-cloud-disk-music-upload 实现。 +#[tauri::command] +pub async fn cloud_upload(file_path: String, app_handle: tauri::AppHandle, state: State<'_, ApiController>) -> Result { + let path = PathBuf::from(&file_path); + if !path.exists() { + return Err("文件不存在".to_string()); + } + + let file_bytes = fs::read(&path).map_err(|e| format!("读取文件失败: {}", e))?; + let file_size = file_bytes.len() as i64; + let filename = path.file_name().unwrap_or_default().to_string_lossy().to_string(); + + // 计算 MD5 + let md5_hex = format!("{:x}", md5::compute(&file_bytes)); + + // 读取音频元数据 + let (song_name, artist, album, _duration, _cover) = read_audio_metadata(&path); + let bitrate = { + match lofty::read_from_path(&path) { + Ok(tf) => { + let br = tf.properties().audio_bitrate().unwrap_or(0) * 1000; + if br > 0 { br.to_string() } else { "999000".to_string() } + } + Err(_) => "999000".to_string() + } + }; + + // 文件扩展名 + let ext = if filename.contains('.') { + filename.rsplit('.').next().unwrap_or("mp3").to_string() + } else { + "mp3".to_string() + }; + + let client = state.client.lock().await.clone(); + + // Step 1: 上传检查 + let check_q = state.build_query() + .param("bitrate", &bitrate) + .param("length", &file_size.to_string()) + .param("md5", &md5_hex) + .param("ext", &ext) + .param("songId", "0"); + + let check_res = client.cloud_upload_check(&check_q).await + .map_err(|e| format!("上传检查失败: {}", e))?; + let check_data = &check_res.body; + + // songId 是十六进制字符串(非数字),需要保持字符串传递 + let song_id = check_data["songId"].as_str().unwrap_or("0").to_string(); + let need_upload = check_data["needUpload"].as_bool().unwrap_or(true); + + let mut resource_id = String::new(); + + // Step 2-4: 仅在 needUpload=true 时执行 NOS 上传 + if need_upload { + // Step 2: 获取 NOS 上传 Token + let token_q = state.build_query() + .param("filename", &filename) + .param("md5", &md5_hex); + + let token_res = client.cloud_upload_token_alloc(&token_q).await + .map_err(|e| format!("获取上传Token失败: {}", e))?; + let token_data = &token_res.body; + + resource_id = token_data["result"]["resourceId"].as_str().unwrap_or("").to_string(); + let object_key_raw = token_data["result"]["objectKey"].as_str().unwrap_or("").to_string(); + let object_key = object_key_raw.replace('/', "%2F"); + let token_str = token_data["result"]["token"].as_str().unwrap_or("").to_string(); + + if token_str.is_empty() { + return Err(format!("获取上传Token为空, 响应: {}", token_data)); + } + + // Step 3: 查询 LBS 获取正确的 NOS 上传节点 + let bucket = "jd-musicrep-privatecloud-audio-public"; + let nos_host = query_nos_upload_host(bucket).await + .unwrap_or_else(|_| format!("https://{}.nos-jd.163yun.com", bucket)); + + let content_type = match ext.as_str() { + "flac" => "audio/flac", + "wav" => "audio/wav", + "ogg" => "audio/ogg", + "aac" | "m4a" => "audio/aac", + _ => "audio/mpeg", + }; + + // Step 4: 上传文件到 NOS(multipart upload) + let http_client = reqwest::Client::new(); + + // 4a: 初始化 multipart upload + let init_url = format!("{}/{}?uploads", nos_host, object_key); + let init_res = http_client.post(&init_url) + .header("x-nos-token", &token_str) + .header("X-Nos-Meta-Content-Type", content_type) + .send() + .await + .map_err(|e| format!("初始化NOS上传失败: {}", e))?; + + let init_status = init_res.status(); + let init_xml = init_res.text().await.map_err(|e| format!("读取NOS响应失败: {}", e))?; + + if !init_status.is_success() { + return Err(format!("初始化NOS上传失败: HTTP {} 响应: {}", init_status, init_xml)); + } + + // 解析 UploadId + let upload_id = init_xml + .split("") + .nth(1) + .and_then(|s| s.split("").next()) + .unwrap_or_default() + .to_string(); + + if upload_id.is_empty() { + return Err(format!("获取UploadId失败, NOS响应: {}", init_xml)); + } + + // 4b: 分块上传(每块 10MB) + let block_size = 10 * 1024 * 1024; + let file_size_usize = file_bytes.len(); + let mut offset = 0usize; + let mut block_index = 1u32; + let mut etags = Vec::new(); + let total_blocks = ((file_size_usize + block_size - 1) / block_size).max(1); + + while offset < file_size_usize { + let end = (offset + block_size).min(file_size_usize); + let chunk = file_bytes[offset..end].to_vec(); + + let part_url = format!( + "{}/{}?partNumber={}&uploadId={}", + nos_host, object_key, block_index, upload_id + ); + + let part_res = http_client.put(&part_url) + .header("x-nos-token", &token_str) + .header("Content-Type", content_type) + .body(chunk) + .send() + .await + .map_err(|e| format!("上传分块{}失败: {}", block_index, e))?; + + if let Some(etag) = part_res.headers().get("etag") { + etags.push(etag.to_str().unwrap_or_default().to_string()); + } + + // 发送上传进度 + let progress = (block_index as f64 / total_blocks as f64 * 100.0).min(100.0); + let _ = app_handle.emit("cloud-upload-progress", json!({ + "filename": filename, + "progress": progress, + "uploaded": end, + "total": file_size_usize, + })); + + offset = end; + block_index += 1; + } + + // 4c: 完成 multipart upload + let mut complete_xml = String::from(""); + for (i, etag) in etags.iter().enumerate() { + complete_xml.push_str(&format!( + "{}{}", + i + 1, etag + )); + } + complete_xml.push_str(""); + + let complete_url = format!("{}/{}?uploadId={}", nos_host, object_key, upload_id); + let complete_res = http_client.post(&complete_url) + .header("Content-Type", "text/plain;charset=UTF-8") + .header("X-Nos-Meta-Content-Type", content_type) + .header("x-nos-token", &token_str) + .body(complete_xml) + .send() + .await + .map_err(|e| format!("完成NOS上传失败: {}", e))?; + + if !complete_res.status().is_success() { + let status = complete_res.status(); + let body = complete_res.text().await.unwrap_or_default(); + return Err(format!("完成NOS上传失败: HTTP {} 响应: {}", status, body)); + } + } + + // Step 5: 提交歌曲信息(songId 使用 check 返回的字符串值) + let info_q = state.build_query() + .param("md5", &md5_hex) + .param("songId", &song_id) + .param("filename", &filename) + .param("song", &song_name) + .param("album", &album) + .param("artist", &artist) + .param("bitrate", &bitrate) + .param("resourceId", &resource_id); + + let info_res = client.cloud_upload_info(&info_q).await + .map_err(|e| format!("提交歌曲信息失败: {}", e))?; + let info_data = &info_res.body; + + // info 可能返回新的 songId,优先使用 + let final_song_id = info_data["songId"].as_str() + .filter(|s| s != &"0" && !s.is_empty()) + .unwrap_or(&song_id) + .to_string(); + + // Step 6: 发布 + let pub_q = state.build_query().param("songId", &final_song_id); + let pub_res = client.cloud_publish(&pub_q).await + .map_err(|e| format!("发布失败: {}", e))?; + + let _ = app_handle.emit("cloud-upload-complete", &filename); + Ok(pub_res.body.to_string()) +} diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 548f687..bf27eac 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -35,6 +35,7 @@ pub struct AudioController { tx: Sender, current_url: Arc>>, position: Arc>, + is_playing: Arc, } impl AudioController { @@ -43,11 +44,13 @@ impl AudioController { let (tx, rx) = channel(); let current_url = Arc::new(Mutex::new(None)); let position = Arc::new(Mutex::new(0.0)); + let is_playing = Arc::new(AtomicBool::new(false)); let url_clone = current_url.clone(); let pos_clone = position.clone(); + let playing_clone = is_playing.clone(); let ah_clone = app_handle.clone(); - thread::spawn(move || audio_thread(rx, url_clone, pos_clone, ah_clone)); - AudioController { tx, current_url, position } + thread::spawn(move || audio_thread(rx, url_clone, pos_clone, playing_clone, ah_clone)); + AudioController { tx, current_url, position, is_playing } } /// 播放指定URL的网络音频 @@ -78,6 +81,9 @@ impl AudioController { pub fn get_position(&self) -> f64 { *self.position.lock().unwrap() } + pub fn get_is_playing(&self) -> bool { + self.is_playing.load(Ordering::Relaxed) + } } /// 缓冲区内部状态,存储已下载的字节数据及完成/取消标志 @@ -908,7 +914,7 @@ fn restart_playback_on_device_change( } /// 音频线程主循环,接收命令并管理播放生命周期,包括设备热切换和播放结束检测 -fn audio_thread(rx: Receiver, _current_url: Arc>>, shared_position: Arc>, app_handle: AppHandle) { +fn audio_thread(rx: Receiver, _current_url: Arc>>, shared_position: Arc>, is_playing: Arc, app_handle: AppHandle) { let mut selected_device: Option = None; let mut current_volume: f32 = 1.0; let mut output_ctx: Option = None; @@ -925,6 +931,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> AudioCmd::Play(url) => { audio_active = false; audio_paused = false; + is_playing.store(false, Ordering::Relaxed); manual_stop = false; current_local_path = None; @@ -971,6 +978,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> Ok(ctx) => { output_ctx = Some(ctx); audio_active = true; + is_playing.store(true, Ordering::Relaxed); let _ = app_handle.emit("audio-started", ()); } Err(e) => { @@ -982,6 +990,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> AudioCmd::PlayLocal(path) => { audio_active = false; audio_paused = false; + is_playing.store(false, Ordering::Relaxed); manual_stop = false; current_local_path = Some(path.clone()); @@ -1008,6 +1017,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> Ok(ctx) => { output_ctx = Some(ctx); audio_active = true; + is_playing.store(true, Ordering::Relaxed); let _ = app_handle.emit("audio-started", ()); } Err(e) => { @@ -1018,6 +1028,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> AudioCmd::Pause => { audio_paused = true; + is_playing.store(false, Ordering::Relaxed); if let Some(ref ctx) = output_ctx { ctx.playback.playing.store(false, Ordering::Relaxed); } @@ -1025,6 +1036,9 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> AudioCmd::Resume => { audio_paused = false; + if audio_active { + is_playing.store(true, Ordering::Relaxed); + } if let Some(ref ctx) = output_ctx { ctx.playback.playing.store(true, Ordering::Relaxed); } @@ -1033,6 +1047,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> AudioCmd::Stop => { audio_active = false; audio_paused = false; + is_playing.store(false, Ordering::Relaxed); manual_stop = true; stop_playback(&mut output_ctx, &shared_position); if let Some(ref buf) = current_audio_buffer { @@ -1054,6 +1069,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> output_ctx = Some(ctx); audio_active = true; audio_paused = false; + is_playing.store(true, Ordering::Relaxed); } Err(e) => { eprintln!("[audio] seek 播放失败: {}", e); @@ -1097,6 +1113,7 @@ fn audio_thread(rx: Receiver, _current_url: Arc>> && ctx.playback.buffer_exhausted.load(Ordering::Relaxed) && !manual_stop && !audio_paused { audio_active = false; + is_playing.store(false, Ordering::Relaxed); let _ = app_handle.emit("audio-ended", ()); } let pos = ctx.playback.position(); @@ -1204,3 +1221,8 @@ pub fn get_audio_position(state: State<'_, AppAudio>) -> f64 { pub fn set_volume(state: State<'_, AppAudio>, vol: f32) { if let Ok(ctrl) = state.0.lock() { ctrl.set_volume(vol); } } + +#[tauri::command] +pub fn is_audio_playing(state: State<'_, AppAudio>) -> bool { + if let Ok(ctrl) = state.0.lock() { ctrl.get_is_playing() } else { false } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9721492..1c5b936 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -173,9 +173,11 @@ pub fn run() { audio::seek_audio, audio::get_audio_position, audio::set_volume, + audio::is_audio_playing, api::download_song, api::list_local_songs, + api::scan_local_folders, api::delete_local_song, api::check_local_song, api::get_default_download_path, @@ -184,11 +186,17 @@ pub fn run() { api::artist_songs, api::artist_album, api::artist_desc, + api::artist_sub, + api::artist_sublist, api::album_detail, api::comment_new, api::comment_hot, api::comment_floor, api::comment_like, + api::user_cloud, + api::user_cloud_detail, + api::user_cloud_del, + api::cloud_upload, ]) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_updater::Builder::new().build()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5b26e80..acd781a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Nekosonic", - "version": "0.6.0", + "version": "0.7.0", "identifier": "com.atdunbg.Nekosonic", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.vue b/src/App.vue index 6c33168..50f4051 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,7 +7,7 @@
- + @@ -38,6 +38,7 @@ diff --git a/src/components/RoamDrawer.vue b/src/components/RoamDrawer.vue index 08920e2..c5582f5 100644 --- a/src/components/RoamDrawer.vue +++ b/src/components/RoamDrawer.vue @@ -43,18 +43,18 @@
- -
-
+

暂无歌词
-
+
@@ -118,7 +118,6 @@ const roamLyricHovering = ref(false); const roamLyricPadPx = ref(0); const roamSong = computed(() => player.currentSong); const roamCoverError = ref(false); -const roamTab = ref<'lyric' | 'comment'>('lyric'); const roamCoverUrl = computed(() => { if (!roamSong.value) return ''; return getCoverUrl(roamSong.value) || ''; @@ -144,7 +143,6 @@ function updateRoamLyricPad() { watch(() => player.showRoamDrawer, (val) => { if (val) { - roamTab.value = player.roamInitialTab; nextTick(() => { updateRoamLyricPad(); if (roamResizeObserver) roamResizeObserver.disconnect(); diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 5cc8c37..80e37d4 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -45,6 +45,18 @@ 本地音乐 + + + 下载音乐 + + + + 音乐云盘 +
@@ -128,6 +140,8 @@ import IconLogIn from '~icons/lucide/log-in'; import IconChevronRight from '~icons/lucide/chevron-right'; import IconClock from '~icons/lucide/clock'; import IconMusic from '~icons/lucide/music'; +import IconCloud from '~icons/lucide/cloud'; +import IconDownload from '~icons/lucide/download'; const router = useRouter(); const route = useRoute(); diff --git a/src/composables/useLocalMusic.ts b/src/composables/useLocalMusic.ts new file mode 100644 index 0000000..3aec109 --- /dev/null +++ b/src/composables/useLocalMusic.ts @@ -0,0 +1,52 @@ +import { MusicApi } from '../api'; +import type { Song } from '../utils/song'; + +export interface LocalSong { + id: number; + name: string; + artist: string; + album: string; + duration: number; + cover: string | null; + filename: string; + fileSize: number; + path: string; + local: boolean; +} + +export function formatFileSize(bytes: number): string { + if (bytes === 0) return ''; + const units = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i]; +} + +export function localSongToSong(local: LocalSong): Song { + return { + id: local.id, + name: local.name, + ar: local.artist ? [{ name: local.artist }] : [], + al: { picUrl: local.cover || '', name: local.album || undefined }, + dt: local.duration || undefined, + localPath: local.path, + }; +} + +export async function fetchMissingCovers(songs: LocalSong[]): Promise { + const missing = songs.filter(s => !s.cover && s.id > 0 && s.id < 1e12); + if (missing.length === 0) return; + const ids = [...new Set(missing.map(s => s.id))]; + try { + const jsonStr: string = await MusicApi.getSongDetail(JSON.stringify(ids)); + const data = JSON.parse(jsonStr); + const detailMap = new Map(); + for (const s of data.songs || []) { + const url = s.al?.picUrl; + if (url && s.id) detailMap.set(s.id, url + '?param=100y100'); + } + for (const song of missing) { + const url = detailMap.get(song.id); + if (url) song.cover = url; + } + } catch { /* 忽略 */ } +} diff --git a/src/composables/useToast.ts b/src/composables/useToast.ts index 0dba215..b29981c 100644 --- a/src/composables/useToast.ts +++ b/src/composables/useToast.ts @@ -9,7 +9,22 @@ export interface Toast { const toasts = ref([]); let nextId = 0; +const MAX_TOASTS = 3; +const DEDUP_WINDOW = 3000; +const recentMessages = new Map(); + export function showToast(message: string, type: 'success' | 'error' | 'info' = 'info', duration = 3000) { + const key = `${type}:${message}`; + const now = Date.now(); + const lastShown = recentMessages.get(key); + if (lastShown && now - lastShown < DEDUP_WINDOW) return; + recentMessages.set(key, now); + setTimeout(() => { recentMessages.delete(key); }, DEDUP_WINDOW); + + if (toasts.value.length >= MAX_TOASTS) { + toasts.value.shift(); + } + const id = nextId++; toasts.value.push({ id, message, type }); setTimeout(() => { diff --git a/src/router/index.ts b/src/router/index.ts index 9237171..9834951 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,27 +1,20 @@ import { createRouter, createWebHistory } from 'vue-router'; -import Home from '@/views/Home.vue'; -import Discover from '@/views/Discover.vue'; -import PlaylistDetail from '@/views/PlaylistDetail.vue'; -import Login from '@/views/Login.vue'; -import FavoriteSongs from '@/views/FavoriteSongs.vue'; -import RecentPlays from '@/views/RecentPlays.vue'; -import DailySongs from '@/views/DailySongs.vue'; -import LocalMusic from '@/views/LocalMusic.vue'; -import Settings from '@/views/Settings.vue'; const routes = [ - { path: '/', name: 'home', component: Home }, - { path: '/discover', name: 'discover', component: Discover }, - { path: '/search', name: 'search', component: Discover }, - { path: '/favorites', name: 'favorites', component: FavoriteSongs }, - { path: '/recent', name: 'recent', component: RecentPlays }, - { path: '/daily', name: 'daily', component: DailySongs }, - { path: '/local-music', name: 'local-music', component: LocalMusic }, - { path: '/login', name: 'login', component: Login, meta: { guest: true } }, - { path: '/playlist/:id', name: 'playlist', component: PlaylistDetail }, + { path: '/', name: 'home', component: () => import('@/views/Home.vue') }, + { path: '/discover', name: 'discover', component: () => import('@/views/Discover.vue') }, + { path: '/search', name: 'search', component: () => import('@/views/Discover.vue') }, + { path: '/favorites', name: 'favorites', component: () => import('@/views/FavoriteSongs.vue') }, + { path: '/recent', name: 'recent', component: () => import('@/views/RecentPlays.vue') }, + { path: '/daily', name: 'daily', component: () => import('@/views/DailySongs.vue') }, + { path: '/local-music', name: 'local-music', component: () => import('@/views/LocalMusic.vue') }, + { path: '/downloaded-music', name: 'downloaded-music', component: () => import('@/views/DownloadedMusic.vue') }, + { path: '/cloud-music', name: 'cloud-music', component: () => import('@/views/CloudMusic.vue') }, + { path: '/login', name: 'login', component: () => import('@/views/Login.vue'), meta: { guest: true } }, + { path: '/playlist/:id', name: 'playlist', component: () => import('@/views/PlaylistDetail.vue') }, { path: '/artist/:id', name: 'artist', component: () => import('@/views/ArtistDetail.vue') }, { path: '/album/:id', name: 'album', component: () => import('@/views/AlbumDetail.vue') }, - { path: '/settings', name: 'settings', component: Settings }, + { path: '/settings', name: 'settings', component: () => import('@/views/Settings.vue') }, ]; const router = createRouter({ @@ -31,7 +24,7 @@ const router = createRouter({ router.beforeEach((to) => { if (to.meta.guest) { - const raw = localStorage.getItem('user'); + const raw = localStorage.getItem('user_profile'); if (raw) { try { const data = JSON.parse(raw); diff --git a/src/stores/player.ts b/src/stores/player.ts index d7873f2..9eb1609 100644 --- a/src/stores/player.ts +++ b/src/stores/player.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia'; -import { ref, watch, nextTick } from 'vue'; +import { ref, watch } from 'vue'; import { normalizeSong, type Song } from '../utils/song'; import { useSettingsStore } from './settings'; import { useUserStore } from './user'; @@ -123,7 +123,7 @@ export const usePlayerStore = defineStore('player', () => { } if (lastScrobbleId === song.id && lastScrobbleStartTime > 0) { const playedSec = Math.round((Date.now() - lastScrobbleStartTime) / 1000); - if (playedSec > 5) { + if (playedSec > 5 && navigator.onLine) { MusicApi.scrobble({ id: song.id, sourceid: isFmMode.value ? String(song.id) : '', @@ -183,6 +183,8 @@ export const usePlayerStore = defineStore('player', () => { const MAX_FM_VIP_SKIP = 10; async function playFmSong(song: Song) { + const seq = ++_playSeq; + _switchingSong = true; clearTick(); reportScrobble(); if (!song.dt || song.dt === 0) { @@ -207,6 +209,7 @@ export const usePlayerStore = defineStore('player', () => { currentSong.value = song; try { const jsonStr = await MusicApi.getSongUrl({ id: Number(song.id), level: settings.audioQuality, fm_mode: true }); + if (seq !== _playSeq) return; const data = JSON.parse(jsonStr); const url: string | undefined = data.url; if (!url) throw new Error('无播放源'); @@ -221,6 +224,7 @@ export const usePlayerStore = defineStore('player', () => { disableFmMode(); return; } + _switchingSong = false; if (fmNextCallback) { fmNextCallback(); } else { @@ -231,14 +235,23 @@ export const usePlayerStore = defineStore('player', () => { fmVipSkipCount = 0; await AudioApi.playAudio(url); - await waitForAudioStart(); - playing.value = true; - duration.value = (song.dt || 0) / 1000; - currentTime.value = 0; - startTick(); - addRecent(song); - emitPlaybackState(); + if (seq !== _playSeq) return; + const started = await waitForPlaybackStart(); + if (seq !== _playSeq) return; + if (started) { + playing.value = true; + duration.value = (song.dt || 0) / 1000; + currentTime.value = 0; + startTick(); + addRecent(song); + emitPlaybackState(); + } else { + playing.value = false; + showToast('FM 播放启动超时,仍在尝试加载…', 'info'); + watchForLatePlayback(seq, song); + } } catch (e) { + if (seq !== _playSeq) return; console.error('FM播放失败', e); playing.value = false; showToast('FM 播放失败', 'error'); @@ -247,6 +260,10 @@ export const usePlayerStore = defineStore('player', () => { } else { disableFmMode(); } + } finally { + if (seq === _playSeq) { + _switchingSong = false; + } } } @@ -298,18 +315,31 @@ export const usePlayerStore = defineStore('player', () => { let vipSkipCount = 0; const MAX_VIP_SKIP = 10; - function waitForAudioStart(): Promise { - return new Promise((resolve) => { - _audioStartedResolve = resolve; - }); + let _playSeq = 0; + let _switchingSong = false; + + async function waitForPlaybackStart(timeoutMs: number = 5000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + await new Promise(r => setTimeout(r, 100)); + try { + if (await AudioApi.isAudioPlaying()) return true; + } catch { /* 忽略 */ } + } + try { + return await AudioApi.isAudioPlaying(); + } catch { return false; } } async function playCurrent() { + const seq = ++_playSeq; + _switchingSong = true; clearTick(); reportScrobble(); const song = queue.value[currentIndex.value]; if (!song?.id) { console.error('无效的歌曲数据', song); + _switchingSong = false; return; } @@ -321,15 +351,23 @@ export const usePlayerStore = defineStore('player', () => { if (song.localPath) { await AudioApi.playLocalAudio(song.localPath); - await waitForAudioStart(); - playing.value = true; - startTick(); - addRecent(song); - emitPlaybackState(); + if (seq !== _playSeq) return; + const started = await waitForPlaybackStart(); + if (seq !== _playSeq) return; + if (started) { + playing.value = true; + startTick(); + addRecent(song); + emitPlaybackState(); + } else { + showToast('播放启动超时,仍在尝试加载…', 'info'); + watchForLatePlayback(seq, song); + } return; } const jsonStr = await MusicApi.getSongUrl({ id: Number(song.id), level: settings.audioQuality }); + if (seq !== _playSeq) return; const data = JSON.parse(jsonStr); const url: string | undefined = data.url; @@ -347,24 +385,66 @@ export const usePlayerStore = defineStore('player', () => { vipSkipCount = 0; return; } + _switchingSong = false; next(); return; } await AudioApi.playAudio(url); - await waitForAudioStart(); - playing.value = true; - startTick(); - addRecent(song); - vipSkipCount = 0; - emitPlaybackState(); + if (seq !== _playSeq) return; + const started = await waitForPlaybackStart(); + if (seq !== _playSeq) return; + if (started) { + playing.value = true; + startTick(); + addRecent(song); + vipSkipCount = 0; + emitPlaybackState(); + } else { + playing.value = false; + showToast('播放启动超时,仍在尝试加载…', 'info'); + watchForLatePlayback(seq, song); + } } catch (e) { + if (seq !== _playSeq) return; console.error('播放失败', e); playing.value = false; showToast('播放失败,请稍后重试', 'error'); + } finally { + if (seq === _playSeq) { + _switchingSong = false; + } } } + /// 超时后继续监听后端播放状态,如果后端实际开始播放则恢复状态 + function watchForLatePlayback(seq: number, song: Song) { + let attempts = 0; + const maxAttempts = 15; + const check = async () => { + if (seq !== _playSeq) return; + if (playing.value) return; + attempts++; + if (attempts > maxAttempts) return; + try { + const backendPlaying = await AudioApi.isAudioPlaying(); + if (seq !== _playSeq) return; + if (backendPlaying) { + playing.value = true; + startTick(); + addRecent(song); + vipSkipCount = 0; + emitPlaybackState(); + return; + } + } catch { /* 忽略 */ } + if (seq === _playSeq && !playing.value) { + setTimeout(check, 1000); + } + }; + setTimeout(check, 1000); + } + let onSeekStart: (() => void) | null = null; function startTick() { @@ -374,10 +454,12 @@ export const usePlayerStore = defineStore('player', () => { let syncCounter = 1; let lastSyncPos = -1; let backendFrozen = false; + let stateSyncCounter = 0; setTick(setInterval(async () => { if (playing.value && duration.value > 0) { if (seekGuard) return; syncCounter++; + stateSyncCounter++; if (syncCounter >= 2) { syncCounter = 0; try { @@ -395,6 +477,16 @@ export const usePlayerStore = defineStore('player', () => { lastSyncPos = pos; } } catch { /* 忽略 */ } + + if (stateSyncCounter >= 8) { + stateSyncCounter = 0; + try { + const backendPlaying = await AudioApi.isAudioPlaying(); + if (backendPlaying !== playing.value) { + playing.value = backendPlaying; + } + } catch { /* 忽略 */ } + } } else { if (!backendFrozen) { const next = currentTime.value + 0.25; @@ -411,6 +503,13 @@ export const usePlayerStore = defineStore('player', () => { } async function toggle() { + try { + const backendPlaying = await AudioApi.isAudioPlaying(); + if (backendPlaying !== playing.value) { + playing.value = backendPlaying; + } + } catch { /* 忽略查询失败 */ } + if (playing.value) { await AudioApi.pauseAudio(); playing.value = false; @@ -530,14 +629,13 @@ export const usePlayerStore = defineStore('player', () => { } const showRoamDrawer = ref(false); - const roamInitialTab = ref<'lyric' | 'comment'>('lyric'); + const roamTab = ref<'lyric' | 'comment'>('lyric'); const commentSongId = ref(null); const dominantColor = ref(''); function openRoamDrawer(tab: 'lyric' | 'comment' = 'lyric') { - roamInitialTab.value = tab; + roamTab.value = tab; showRoamDrawer.value = true; - nextTick(() => { roamInitialTab.value = 'lyric'; }); } function openCommentForSong(songId: number) { @@ -547,6 +645,7 @@ export const usePlayerStore = defineStore('player', () => { function closeRoamDrawer() { showRoamDrawer.value = false; + roamTab.value = 'lyric'; } function toggleRoamDrawer() { @@ -615,16 +714,8 @@ async function nextFm() { await loadFm(); } -let _audioStartedResolve: (() => void) | null = null; - -listen('audio-started', () => { - if (_audioStartedResolve) { - _audioStartedResolve(); - _audioStartedResolve = null; - } -}); - listen('audio-ended', () => { + if (_switchingSong) return; const player = usePlayerStore(); player.clearTick(); player.reportScrobble(); @@ -724,7 +815,7 @@ watch(playing, (val) => { toggleLike, showRoamDrawer, - roamInitialTab, + roamTab, commentSongId, dominantColor, openCommentForSong, diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 6b56fb0..6052f44 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -66,6 +66,7 @@ export const defaultShortcuts: Record = { interface SettingsData { audioQuality: AudioQuality; downloadPath: string; + localMusicPaths: string[]; theme: ThemeColor; appearance: Appearance; closeAction: CloseAction; @@ -87,6 +88,7 @@ function loadSettings(): SettingsData { return { audioQuality: parsed.audioQuality || 'standard', downloadPath: parsed.downloadPath || '', + localMusicPaths: parsed.localMusicPaths || [], theme: validThemes.includes(parsed.theme.slice(6)) ? parsed.theme.slice(6) : 'blue', appearance: 'light', closeAction: parsed.closeAction || 'ask', @@ -98,6 +100,7 @@ function loadSettings(): SettingsData { return { audioQuality: parsed.audioQuality || 'standard', downloadPath: parsed.downloadPath || '', + localMusicPaths: parsed.localMusicPaths || [], theme: validThemes.includes(theme) ? theme : 'blue', appearance, closeAction: parsed.closeAction || 'ask', @@ -110,6 +113,7 @@ function loadSettings(): SettingsData { return { audioQuality: 'standard', downloadPath: '', + localMusicPaths: [], theme: 'blue', appearance: 'dark', closeAction: 'ask', @@ -124,6 +128,7 @@ export const useSettingsStore = defineStore('settings', () => { const audioQuality = ref(saved.audioQuality); const downloadPath = ref(saved.downloadPath); + const localMusicPaths = ref(saved.localMusicPaths); const theme = ref(saved.theme); const appearance = ref(saved.appearance); const closeAction = ref(saved.closeAction || 'ask'); @@ -143,6 +148,16 @@ export const useSettingsStore = defineStore('settings', () => { downloadPath.value = p; } + function addLocalMusicPath(p: string) { + if (!localMusicPaths.value.includes(p)) { + localMusicPaths.value = [...localMusicPaths.value, p]; + } + } + + function removeLocalMusicPath(p: string) { + localMusicPaths.value = localMusicPaths.value.filter(v => v !== p); + } + function setTheme(t: ThemeColor) { theme.value = t; } @@ -170,6 +185,7 @@ export const useSettingsStore = defineStore('settings', () => { function resetAll() { audioQuality.value = 'standard'; downloadPath.value = ''; + localMusicPaths.value = []; theme.value = 'blue'; appearance.value = 'dark'; closeAction.value = 'ask'; @@ -178,10 +194,11 @@ export const useSettingsStore = defineStore('settings', () => { volume.value = 100; } - watch([audioQuality, downloadPath, theme, appearance, closeAction, shortcuts, outputDevice, volume], () => { + watch([audioQuality, downloadPath, localMusicPaths, theme, appearance, closeAction, shortcuts, outputDevice, volume], () => { const data: SettingsData = { audioQuality: audioQuality.value, downloadPath: downloadPath.value, + localMusicPaths: localMusicPaths.value, theme: theme.value, appearance: appearance.value, closeAction: closeAction.value, @@ -195,6 +212,7 @@ export const useSettingsStore = defineStore('settings', () => { return { audioQuality, downloadPath, + localMusicPaths, theme, appearance, dataTheme, @@ -204,6 +222,8 @@ export const useSettingsStore = defineStore('settings', () => { volume, setAudioQuality, setDownloadPath, + addLocalMusicPath, + removeLocalMusicPath, setTheme, setAppearance, setCloseAction, diff --git a/src/views/AlbumDetail.vue b/src/views/AlbumDetail.vue index f82a9db..b3568ef 100644 --- a/src/views/AlbumDetail.vue +++ b/src/views/AlbumDetail.vue @@ -1,10 +1,20 @@