feat: v0.3.0 - 流式播放、本地音乐、下载系统、漫游修复

### 新功能
- 流式播放:边下载边播放,缓冲 64KB 后即刻开始,无需等待完整下载
- 本地音乐页面:支持浏览、播放本地歌曲,横向菜单含「从磁盘删除」
- 下载系统:支持下载歌曲到自定义路径,保存完整元数据(封面/专辑/时长)
- 封面补全:本地音乐缺少封面时自动从网易云 API 获取
- 更新信息:接入 Gitea Releases API,查看最新版更新日志

### 修复
- 修复私人漫游播完一首歌后跳三首的问题(双重触发:audio-ended + startTick)
- 修复全屏漫游抽屉和漫游页面无封面歌曲显示破损图片
- 修复 PlayerBar 无封面歌曲显示破损图片
- 修复下载路径修改后不生效(Rust serde camelCase 映射)
- 修复本地音乐始终只显示默认路径歌曲
- 修复下载完成提示弹出 4 次
- 修复播放网络歌曲时进度条先走但无声音(audio-started 事件同步)

### 优化
- PlayerBar 下载状态:未下载显示下载按钮,下载中显示进度,已下载不显示
- audio.rs 新增 manual_stop 标志防止 stop_audio 触发虚假 audio-ended
- player.ts 新增 waitForAudioStart() 确保 playing 状态与实际播放同步
- 切歌/停止时立即清除 tickInterval 防止重复触发 next()
This commit is contained in:
2026-05-15 02:24:48 +08:00
parent 02f7df4201
commit 718d3ed641
25 changed files with 2123 additions and 216 deletions

View File

@ -38,18 +38,23 @@
<div v-if="loading" class="text-content-2">搜索中...</div>
<div v-else class="space-y-3">
<div
v-for="song in results"
v-for="(song, index) in results"
:key="song.id"
@click="playSong(song)"
@click="playSong(song, index)"
class="flex items-center gap-4 p-3 rounded-xl backdrop-blur-md bg-subtle hover:bg-muted border border-line-2 cursor-pointer transition"
>
<img :src="song.al?.picUrl" class="w-12 h-12 rounded-lg object-cover" />
<div>
<p class="font-medium">{{ song.name }}</p>
<p class="text-sm text-content-2">
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ song.name }}</p>
<p class="text-sm text-content-2 truncate">
{{ song.ar?.map((a: any) => a.name).join(' / ') }}
</p>
</div>
<button @click.stop="download.downloadSong(song)" class="text-content-3 hover:text-accent-text transition flex-shrink-0" :title="download.isDownloaded(song.id) ? '已下载' : '下载'">
<svg v-if="download.isDownloading(song.id)" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><path d="M21 12a9 9 0 11-6.22-8.56"/></svg>
<svg v-else-if="download.isDownloaded(song.id)" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-accent-text"><polyline points="20 6 9 17 4 12"/></svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
</button>
</div>
<p v-if="!loading && hasSearched && results.length === 0" class="text-content-2">无结果</p>
</div>
@ -63,10 +68,12 @@ import { ref, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore } from '../stores/player';
import { useDownload } from '../composables/useDownload';
const router = useRouter();
const route = useRoute();
const player = usePlayerStore();
const download = useDownload();
const keyword = ref('');
const results = ref<any[]>([]);
@ -116,8 +123,15 @@ function searchTag(tag: string) {
handleSearch();
}
async function playSong(song: any) {
player.play(song);
async function playSong(_song: any, index: number) {
const normalized = results.value.map((s: any) => ({
id: s.id,
name: s.name,
ar: s.ar || s.artists || [],
al: s.al || s.album || { picUrl: '' },
dt: s.dt || 0,
}));
player.playFromList(normalized, index);
}
</script>