mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 10:48:05 +08:00
Compare commits
2 Commits
v0.4.0
...
38c079ed5c
| Author | SHA1 | Date | |
|---|---|---|---|
| 38c079ed5c | |||
| 68e3b92a6a |
71
README.md
71
README.md
@ -1,25 +1,60 @@
|
|||||||
# Nekosonic
|
# Nekosonic
|
||||||
|
|
||||||
一款轻量的跨平台的音乐播放器,支持Windows/Linux系统,音源主要源自的网易云音乐。
|
一款轻量的跨平台音乐播放器,支持 Windows / Linux / macOS,音源源自网易云音乐。
|
||||||
|
|
||||||
## ✨ 特性
|
## ✨ 特性
|
||||||
|
|
||||||
- 🔴 网易云账号登录(扫码)
|
### 播放
|
||||||
- 🎵 多音质播放(标准 / 较高 / 极高 / 无损 / Hi-Res)
|
|
||||||
- 📻 私人漫游,沉浸式全屏歌词体验
|
- 🎵 在线音乐播放,流式缓冲边下边播
|
||||||
- ❤️ 一键喜欢 / 取消喜欢
|
- 🎵 多音质选择(标准 / 较高 / 极高 HQ / 无损 SQ / Hi-Res)
|
||||||
- 📋 歌单管理,收藏 / 取消收藏歌单
|
- 🔄 播放模式切换(列表循环 / 随机播放 / 单曲循环)
|
||||||
|
- ⏯ 播放控制(播放 / 暂停 / 上一首 / 下一首 / 进度跳转 / 音量调节)
|
||||||
|
- 📋 播放队列管理(查看队列 / 移除歌曲 / 清空队列)
|
||||||
|
- 📻 私人漫游 FM(个性化推荐,VIP 试听自动跳过)
|
||||||
|
- 🎵 本地音乐播放(支持 mp3 / flac / wav / ogg / aac / m4a / wma / opus)
|
||||||
|
- 🔊 音频输出设备选择
|
||||||
|
|
||||||
|
### 发现与浏览
|
||||||
|
|
||||||
|
- 🔍 关键词搜索歌曲 + 热门搜索标签
|
||||||
|
- 📋 歌单浏览(推荐歌单 / 排行榜 / 用户歌单 / 收藏歌单)
|
||||||
|
- 📋 歌单详情(歌曲列表 + 收藏 / 取消收藏 + 歌单评论)
|
||||||
|
- 🎤 歌手详情(热门歌曲 / 专辑 / 简介)
|
||||||
|
- 💿 专辑详情(歌曲列表 + 播放全部)
|
||||||
- 📅 每日推荐歌曲
|
- 📅 每日推荐歌曲
|
||||||
- 🕐 本地播放历史记录
|
|
||||||
- 🔍 关键词搜索歌曲
|
### 歌词与评论
|
||||||
- 🎤 实时滚动歌词
|
|
||||||
|
- 🎤 实时滚动歌词(自动滚动 / 点击跳转 / 渐变透明度)
|
||||||
|
- 🎤 全屏漫游模式(大封面 + 歌词 / 评论双标签页)
|
||||||
|
- 💬 歌曲评论查看(热门评论 + 无限滚动加载 + 点赞)
|
||||||
|
|
||||||
|
### 收藏与下载
|
||||||
|
|
||||||
|
- ❤️ 一键喜欢 / 取消喜欢(同步到网易云账号)
|
||||||
|
- ⬇️ 歌曲下载(带进度显示 / VIP 拦截 / 元数据保存)
|
||||||
|
- 🎵 本地音乐管理(列出 / 播放 / 删除 / 音频元数据与封面读取)
|
||||||
|
- 🕐 本地播放历史记录(最多 200 首)
|
||||||
|
|
||||||
|
### 账号
|
||||||
|
|
||||||
|
- 🔴 网易云账号登录(二维码扫码 / 手机号密码)
|
||||||
|
- 🔑 登录态持久化(重启后自动恢复)
|
||||||
|
|
||||||
|
### 系统与设置
|
||||||
|
|
||||||
|
- 📡 系统托盘(播放控制 / 显示窗口 / 退出)
|
||||||
|
- 🛡 单实例运行(防止重复启动)
|
||||||
|
- ⌨️ 自定义快捷键(应用内 + 系统全局)
|
||||||
- 🌚 Light / Dark Mode 主题切换
|
- 🌚 Light / Dark Mode 主题切换
|
||||||
- 🛠 更多特性添加中
|
- ⚙️ 关闭窗口行为设置(每次询问 / 最小化到托盘 / 直接退出)
|
||||||
|
- 🔄 自动更新(启动静默检测 + 自定义弹窗 + 忽略版本 + 下载进度)
|
||||||
|
- 📝 更新日志查看
|
||||||
|
|
||||||
## 📦️ 安装
|
## 📦️ 安装
|
||||||
|
|
||||||
访问本项目的 [Releases](https://gitea.atdunbg.xyz/atdunbg/Nekosonic-Music/releases) 页面下载安装包。
|
访问本项目的 [Releases](https://github.com/atdunbg/Nekosonic-Music/releases) 页面下载安装包。
|
||||||
|
|
||||||
|
|
||||||
## 💻 配置开发环境
|
## 💻 配置开发环境
|
||||||
|
|
||||||
@ -55,13 +90,18 @@ npm run tauri build
|
|||||||
|
|
||||||
## ☑️ Todo
|
## ☑️ Todo
|
||||||
|
|
||||||
|
- [x] 评论系统
|
||||||
|
- [x] 歌曲下载
|
||||||
|
- [x] 本地音乐管理
|
||||||
|
- [x] 歌手详情页
|
||||||
|
- [x] 专辑详情页
|
||||||
|
- [x] 自定义全局快捷键
|
||||||
|
- [x] 自动更新
|
||||||
- [ ] MV 播放
|
- [ ] MV 播放
|
||||||
- [ ] 音乐云盘
|
- [ ] 音乐云盘
|
||||||
- [ ] 评论系统
|
|
||||||
- [ ] 下载功能
|
|
||||||
- [ ] 自定义全局快捷键
|
|
||||||
- [ ] 歌词翻译
|
- [ ] 歌词翻译
|
||||||
- [ ] 更多主题
|
- [ ] 更多主题
|
||||||
|
- [ ] 桌面歌词
|
||||||
|
|
||||||
欢迎提 Issue 和 Pull request。
|
欢迎提 Issue 和 Pull request。
|
||||||
|
|
||||||
@ -71,7 +111,6 @@ npm run tauri build
|
|||||||
|
|
||||||
基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。
|
基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。
|
||||||
|
|
||||||
|
|
||||||
## 致谢
|
## 致谢
|
||||||
|
|
||||||
- [ncm-api-rs](https://crates.io/crates/ncm-api-rs) — 网易云音乐 API 的 Rust 封装
|
- [ncm-api-rs](https://crates.io/crates/ncm-api-rs) — 网易云音乐 API 的 Rust 封装
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-8 text-content max-w-2xl">
|
<div class="p-8 text-content">
|
||||||
<button @click="$router.back()" class="mb-4 text-content-2 hover:text-content transition">
|
<button @click="$router.back()" class="mb-4 text-content-2 hover:text-content transition">
|
||||||
← 返回
|
← 返回
|
||||||
</button>
|
</button>
|
||||||
@ -157,15 +157,25 @@
|
|||||||
<p class="text-xs text-content-3 leading-relaxed">
|
<p class="text-xs text-content-3 leading-relaxed">
|
||||||
Nekosonic 是一款高颜值的跨平台第三方网易云音乐桌面客户端,基于 Tauri 2 + Vue 3 构建,提供轻量流畅的音乐播放体验。
|
Nekosonic 是一款高颜值的跨平台第三方网易云音乐桌面客户端,基于 Tauri 2 + Vue 3 构建,提供轻量流畅的音乐播放体验。
|
||||||
</p>
|
</p>
|
||||||
<button
|
<div class="flex items-center gap-2">
|
||||||
@click="handleCheckUpdate"
|
<button
|
||||||
:disabled="updater.checking.value"
|
@click="handleCheckUpdate"
|
||||||
class="flex items-center gap-2 px-4 py-2 bg-subtle hover:bg-muted rounded-lg text-sm transition"
|
:disabled="updater.checking.value"
|
||||||
>
|
class="flex items-center gap-2 px-4 py-2 bg-subtle hover:bg-muted rounded-lg text-sm transition"
|
||||||
<svg v-if="!updater.checking.value" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
>
|
||||||
<svg v-else class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.22-8.56"/></svg>
|
<svg v-if="!updater.checking.value" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
|
||||||
{{ updater.checking.value ? '检查中...' : '检查更新' }}
|
<svg v-else class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.22-8.56"/></svg>
|
||||||
</button>
|
{{ updater.checking.value ? '检查中...' : '检查更新' }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="fetchChangelog"
|
||||||
|
:disabled="fetchingChangelog"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 bg-subtle hover:bg-muted rounded-lg text-sm transition"
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||||
|
{{ fetchingChangelog ? '获取中...' : '更新日志' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p v-if="updater.error.value" class="text-xs text-content-3">{{ updater.error.value }}</p>
|
<p v-if="updater.error.value" class="text-xs text-content-3">{{ updater.error.value }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -188,6 +198,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="showChangelogModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" @click.self="showChangelogModal = false">
|
||||||
|
<div class="bg-surface border border-line rounded-2xl shadow-2xl w-[480px] max-h-[80vh] flex flex-col select-auto">
|
||||||
|
<div class="p-6 pb-4">
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<h2 class="text-lg font-semibold text-content">更新日志</h2>
|
||||||
|
<span v-if="changelogRelease" class="text-xs font-medium px-2 py-0.5 rounded-full bg-accent/15 text-accent-text">v{{ changelogRelease.tag_name?.replace('v', '') }}</span>
|
||||||
|
</div>
|
||||||
|
<p v-if="changelogRelease?.published_at" class="text-xs text-content-3 mt-1">{{ formatDate(changelogRelease.published_at) }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="changelogRelease?.body" class="px-6 pb-4 flex-1 overflow-y-auto max-h-60">
|
||||||
|
<div class="text-sm text-content-2 leading-relaxed whitespace-pre-wrap">{{ changelogRelease.body }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="px-6 pb-4">
|
||||||
|
<p class="text-sm text-content-3">暂无更新日志</p>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 border-t border-line flex gap-3">
|
||||||
|
<button @click="showChangelogModal = false"
|
||||||
|
class="flex-1 py-2 rounded-lg bg-muted hover:bg-emphasis text-sm text-content-2 transition">
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
<button v-if="changelogRelease?.html_url" @click="openUrl(changelogRelease.html_url)"
|
||||||
|
class="flex-1 py-2 rounded-lg bg-accent/20 hover:bg-accent/30 text-accent-text text-sm font-medium transition">
|
||||||
|
在 GitHub 中查看
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -249,6 +289,38 @@ async function handleCheckUpdate() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchingChangelog = ref(false);
|
||||||
|
const changelogRelease = ref<any>(null);
|
||||||
|
const showChangelogModal = ref(false);
|
||||||
|
|
||||||
|
async function fetchChangelog() {
|
||||||
|
fetchingChangelog.value = true;
|
||||||
|
try {
|
||||||
|
const resp = await fetch('https://api.github.com/repos/atdunbg/Nekosonic-Music/releases?per_page=1');
|
||||||
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
|
const releases = await resp.json();
|
||||||
|
if (releases && releases.length > 0) {
|
||||||
|
changelogRelease.value = releases[0];
|
||||||
|
showChangelogModal.value = true;
|
||||||
|
} else {
|
||||||
|
showToast('暂无发布版本', 'info');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
showToast(`获取失败: ${e}`, 'error');
|
||||||
|
} finally {
|
||||||
|
fetchingChangelog.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
try {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
return d.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
} catch {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const recordingId = ref<string | null>(null);
|
const recordingId = ref<string | null>(null);
|
||||||
|
|
||||||
function formatShortcut(key: string): string {
|
function formatShortcut(key: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user