1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2026-04-18 21:29:21 +08:00

feat: 添加正在播放页面 (#386)

* 引入tailwindcss, 更新默认主题ui

* 引入tailwindcss, 添加新的主题ui

* 引入tailwindcss, 添加新的主题ui

* 更新tailwind主题

* fix merge error

* fix merge error

* feat: Enhance song metadata display and API fetching

- Add more detailed song metadata in frontend (title, artist, album, year, genre, lyrics)
- Update API call to include music tags
- Replace default music icon with song cover image
- Improve type hinting in httpserver.py

* feat: Add keyboard shortcuts for music playback control

- Implement keyboard shortcuts for play/pause (Space), previous/next track (Left/Right arrows)
- Add volume control via Up/Down arrow keys
- Prevent default browser actions for shortcut keys
- Add event listener for keydown and remove on component unmount
- Enhance user interaction with music player

* feat: Improve song metadata handling and error resilience

- Enhance current song information retrieval with more robust error handling
- Add fallback values for song metadata when API calls fail
- Update current song state with additional properties like cover image
- Optimize song loading process with better error management
- Ensure consistent song information display even with incomplete data
This commit is contained in:
jhcj.z
2025-02-05 22:08:24 +08:00
committed by GitHub
parent e25fad9cc6
commit d122ba9545
6 changed files with 748 additions and 26 deletions

View File

@@ -3,4 +3,4 @@
if __name__ == "__main__":
from xiaomusic.cli import main
main()
main()

View File

@@ -8,7 +8,10 @@ import tempfile
import urllib.parse
from contextlib import asynccontextmanager
from dataclasses import asdict
from typing import Annotated
from typing import Annotated, TYPE_CHECKING
if TYPE_CHECKING:
from xiaomusic.xiaomusic import XiaoMusic
import aiofiles
from fastapi import (
@@ -49,7 +52,7 @@ from xiaomusic.utils import (
update_version,
)
xiaomusic = None
xiaomusic: "XiaoMusic" = None
config = None
log = None

View File

@@ -16,7 +16,7 @@ const API = {
.map(name => `name=${encodeURIComponent(name)}`)
.join('&');
const response = await fetch(`/musicinfos?${queryParams}`);
const response = await fetch(`/musicinfos?${queryParams}&musictag=true`);
return response.json();
},

View File

@@ -181,7 +181,7 @@
<div class="flex items-center gap-3">
<div
class="w-10 h-10 bg-base-200 rounded-lg flex items-center justify-center flex-shrink-0">
<span class="material-icons icon-sm opacity-50">music_note</span>
<img :src="song.cover" :alt="song.title" class="w-10 h-10 rounded-lg">
</div>
<div class="flex flex-col">
<span class="font-medium"
@@ -251,12 +251,14 @@
<div class="flex items-center justify-between max-w-7xl mx-auto">
<!-- 歌曲信息 -->
<div class="flex items-center gap-4">
<div class="avatar">
<div class="w-12 rounded-lg">
<img :src="currentSong?.cover" :alt="currentSong?.title" />
</div>
<div class="avatar cursor-pointer">
<a href="./now_playing.html" target="_blank">
<div class="w-12 rounded-lg">
<img :src="currentSong?.cover" :alt="currentSong?.title" />
</div>
</a>
</div>
<div>
<div class="cursor-pointer hover:text-primary transition-colors">
<div class="font-bold">{{ currentSong?.title }}</div>
<div class="text-sm opacity-50">
{{ currentSong?.artist }} - {{ currentSong?.album }}
@@ -485,21 +487,31 @@
currentTime.value = data.offset || 0;
duration.value = data.duration || 0;
if (data.cur_music) {
const existingSong = songs.value.find(s => s.title === data.cur_music);
if (existingSong) {
currentSong.value = existingSong;
} else {
const songInfo = await API.getMusicInfo(data.cur_music);
if (songInfo && songInfo.ret === 'OK') {
if (data.cur_music && data.cur_music !== currentSong.value?.title) {
try {
// 获取音乐详细信息
const musicInfo = await API.getMusicInfo(data.cur_music);
if (musicInfo && musicInfo.ret === 'OK') {
const tags = musicInfo.tags || {};
currentSong.value = {
title: data.cur_music,
url: songInfo.url,
artist: songInfo.tags?.artist || '未知歌手',
album: songInfo.tags?.album || '未知专辑',
tags: songInfo.tags
title: tags.title || data.cur_music,
artist: tags.artist || '未知歌手',
album: tags.album || '未知专辑',
cover: tags.picture || "/static/xiaoai.png",
url: musicInfo.url,
isPlaying: data.is_playing
};
}
} catch (error) {
console.error('Error getting music info:', error);
// 如果获取详细信息失败,至少更新基本信息
currentSong.value = {
title: data.cur_music,
artist: '未知歌手',
album: '未知专辑',
cover: "/static/xiaoai.png",
isPlaying: data.is_playing
};
}
localStorage.setItem('cur_music', data.cur_music);
@@ -669,18 +681,31 @@
isLoading: false
};
}
return {
const songData = {
id: songName,
title: songName,
title: info.tags?.title || songName,
artist: info.tags?.artist || '未知歌手',
album: info.tags?.album || '未知专辑',
year: info.tags?.year || '',
genre: info.tags?.genre || '',
duration: info.duration ? formatTime(info.duration) : '0:00',
cover: info.cover || '/static/xiaoai.png',
cover: info.tags?.picture || '/static/xiaoai.png',
url: info.url,
lyrics: info.tags?.lyrics || '',
isLoading: false
};
});
// 如果这是当前播放的歌曲更新currentSong
if (currentSong.value && songName === currentSong.value.title) {
currentSong.value = {
...currentSong.value,
cover: songData.cover
};
}
return songData;
});
} catch (error) {
console.error('Error loading songs:', error);
songs.value = [];
@@ -1241,6 +1266,9 @@
// 开始更新播放状态
startPlayingStatusUpdate();
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyPress)
} catch (error) {
console.error('Error in onMounted:', error);
}
@@ -1249,8 +1277,48 @@
// 在组件卸载时停止更新
onUnmounted(() => {
stopPlayingStatusUpdate();
document.removeEventListener('keydown', handleKeyPress)
});
// 处理键盘事件
async function handleKeyPress(event) {
// 如果用户正在输入,不处理快捷键
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return
}
switch (event.code) {
case 'Space': // 空格键:播放/暂停
event.preventDefault() // 防止页面滚动
if (currentSong.value) {
await togglePlay(currentSong.value)
}
break
case 'ArrowLeft': // 左方向键:上一首
event.preventDefault()
await sendCommand(API.commands.PLAY_PREVIOUS)
break
case 'ArrowRight': // 右方向键:下一首
event.preventDefault()
await sendCommand(API.commands.PLAY_NEXT)
break
case 'ArrowUp': // 上方向键:增加音量
event.preventDefault()
if (volume.value < 100) {
volume.value = Math.min(100, volume.value + 5)
await setVolume({ target: { value: volume.value } })
}
break
case 'ArrowDown': // 下方向键:减小音量
event.preventDefault()
if (volume.value > 0) {
volume.value = Math.max(0, volume.value - 5)
await setVolume({ target: { value: volume.value } })
}
break
}
}
// 返回所有需要的数据和方法
return {
isDarkTheme,

View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>正在播放 - XiaoMusic</title>
<link rel="icon" href="./favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.23/dist/full.min.css" rel="stylesheet" type="text/css" />
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<script src="https://unpkg.com/vue@3.5.13/dist/vue.global.js"></script>
<script src="./api.js"></script>
</head>
<body class="font-sans">
<div id="app" class="h-screen flex flex-col">
<!-- Toast 提示 -->
<div class="toast toast-top toast-center z-[9999]">
<div v-if="showToast" :class="['alert', toastType]">
<span>{{ toastMessage }}</span>
</div>
</div>
<!-- 错误提示 -->
<div v-if="error" class="fixed top-4 right-4 z-50 alert alert-error shadow-lg max-w-sm">
<span class="material-icons">error</span>
<span>{{ error }}</span>
</div>
<!-- 背景图 -->
<div class="fixed inset-0 bg-cover bg-center transition-all duration-1000 ease-in-out"
:style="{ backgroundImage: `url(${currentSong.cover})` }"></div>
<div class="fixed inset-0 backdrop-blur-2xl bg-black/40"></div>
<!-- 主内容区 -->
<div class="relative z-10 flex h-full">
<!-- 左侧内容 -->
<div class="flex-1">
<!-- 顶部信息 -->
<div class="text-center p-6">
<h2 class="text-3xl font-bold text-white mb-1 tracking-wide">{{ currentSong.title }}</h2>
<p class="text-l text-white/40 mb-1">{{ currentSong.artist }} - {{ currentSong.album }}</p>
<!-- 标签显示 -->
<div v-if="currentSong.tags" class="flex justify-center gap-2 mt-2 flex-wrap">
<span v-for="(value, key) in currentSong.tags" :key="key"
class="px-2 py-1 rounded-full bg-primary/20 text-primary text-sm">
{{ key }}: {{ value }}
</span>
</div>
</div>
<!-- 歌词区域 -->
<div class="flex-grow overflow-y-auto lyrics-container px-4 py-2 scrollbar-hide">
<div class="max-w-2xl mx-auto pb-24">
<div v-if="currentSong.lyrics.length === 0" class="text-center text-white/50 text-xl mt-10">
暂无歌词
</div>
<p v-else v-for="(line, index) in currentSong.lyrics" :key="index"
:class="{'text-white text-3xl font-semibold': currentLyricIndex === index, 'text-white/50 text-xl': currentLyricIndex !== index}"
class="mb-6 transition-all duration-300 text-center"
:data-index="index">
{{ line.text }}
</p>
</div>
</div>
</div>
<!-- 右侧控制面板 -->
<div class="fixed right-8 top-1/2 -translate-y-1/2 flex items-start">
<!-- 控制面板内容 -->
<div class="transition-all duration-300"
:class="[showControlPanel ? 'translate-x-0 opacity-100' : 'translate-x-8 opacity-0']">
<div class="bg-black/30 backdrop-blur-lg rounded-xl p-6 shadow-2xl">
<!-- 快捷键提示 -->
<div class="flex flex-col gap-4 text-white/50 text-sm mb-8">
<h3 class="text-white/70 font-semibold mb-2">快捷键</h3>
<div class="flex items-center gap-2">
<kbd class="kbd kbd-sm">Space</kbd>
<span>播放/暂停</span>
</div>
<div class="flex items-center gap-2">
<div class="flex gap-1">
<kbd class="kbd kbd-sm"></kbd>
<kbd class="kbd kbd-sm"></kbd>
</div>
<span>切换歌曲</span>
</div>
<div class="flex items-center gap-2">
<div class="flex gap-1">
<kbd class="kbd kbd-sm"></kbd>
<kbd class="kbd kbd-sm"></kbd>
</div>
<span>调节音量</span>
</div>
</div>
<!-- 歌词偏移控制 -->
<div class="flex flex-col gap-4">
<h3 class="text-white/70 font-semibold">歌词偏移</h3>
<div class="flex items-center gap-2">
<button @click="adjustLyricsOffset(-0.5)"
class="btn btn-sm btn-ghost text-white/70 hover:text-white">
<span class="material-icons text-lg">remove</span>
</button>
<div class="text-white/70 text-sm">
{{ lyricsOffset.toFixed(1) }}s
</div>
<button @click="adjustLyricsOffset(0.5)"
class="btn btn-sm btn-ghost text-white/70 hover:text-white">
<span class="material-icons text-lg">add</span>
</button>
<button @click="resetLyricsOffset"
class="btn btn-sm btn-ghost text-white/70 hover:text-white"
title="重置歌词偏移">
<span class="material-icons text-lg">restart_alt</span>
</button>
</div>
</div>
</div>
</div>
<!-- 切换按钮 -->
<button @click="showControlPanel = !showControlPanel"
class="w-6 h-6 bg-black/30 backdrop-blur-lg rounded-full flex items-center justify-center hover:bg-black/50 transition-colors ml-2"
:class="{'rotate-180': !showControlPanel}">
<span class="material-icons text-white/70 text-sm">chevron_left</span>
</button>
</div>
</div>
<!-- 底部控制栏 -->
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/60 to-transparent pt-10 pb-4 px-4">
<!-- 进度条 -->
<div class="mb-4">
<input type="range" min="0" :max="duration" v-model="currentTime"
class="range range-xs range-primary w-full opacity-70 hover:opacity-100 transition-opacity" @input="seek">
<div class="flex justify-between text-xs text-white/50 mt-1">
<span>{{ formatTime(currentTime) }}</span>
<span>{{ formatTime(duration) }}</span>
</div>
</div>
<div class="flex items-center justify-between">
<!-- 播放模式 -->
<div class="flex space-x-3">
<button class="text-white/70 hover:text-white transition-colors p-2 rounded-full"
:class="{'bg-primary/20 text-primary': playMode === 'repeat'}" @click="setPlayMode('repeat')">
<span class="material-icons text-xl">repeat</span>
</button>
<button class="text-white/70 hover:text-white transition-colors p-2 rounded-full"
:class="{'bg-primary/20 text-primary': playMode === 'repeat_one'}" @click="setPlayMode('repeat_one')">
<span class="material-icons text-xl">repeat_one</span>
</button>
<button class="text-white/70 hover:text-white transition-colors p-2 rounded-full"
:class="{'bg-primary/20 text-primary': playMode === 'shuffle'}" @click="setPlayMode('shuffle')">
<span class="material-icons text-xl">shuffle</span>
</button>
</div>
<!-- 播放控制 -->
<div class="flex items-center space-x-4">
<button class="text-white/70 hover:text-white transition-colors" @click="previousSong">
<span class="material-icons text-3xl">skip_previous</span>
</button>
<button class="bg-white text-black rounded-full p-2 hover:bg-primary hover:text-white transition-all"
@click="togglePlay">
<span class="material-icons text-3xl">{{ isPlaying ? 'pause' : 'play_arrow' }}</span>
</button>
<button class="text-white/70 hover:text-white transition-colors" @click="nextSong">
<span class="material-icons text-3xl">skip_next</span>
</button>
<button class="text-white/70 hover:text-white transition-colors" @click="stopPlay">
<span class="material-icons text-3xl">stop</span>
</button>
</div>
<!-- 音量控制 -->
<div class="flex items-center space-x-2">
<span class="material-icons text-white/70 text-xl hover:text-white transition-colors cursor-pointer">
{{ volume > 0 ? 'volume_up' : 'volume_off' }}
</span>
<input type="range" min="0" max="100" step="1" v-model.number="volume"
class="range range-primary w-32" @input="setVolume">
</div>
</div>
</div>
</div>
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* 添加平滑滚动效果 */
.lyrics-container {
scroll-behavior: smooth;
}
/* 自定义范围输入样式 */
.range {
height: 8px;
background-color: rgba(255, 255, 255, 0.1);
}
.range::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
.range::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
cursor: pointer;
border: none;
transition: all 0.3s ease-in-out;
}
/* 歌词动画效果 */
.lyrics-container p {
transition: all 0.3s ease-out;
}
.lyrics-container p.active {
transform: scale(1.05);
}
</style>
<script src="./now_playing.js"></script>
</body>
</html>

View File

@@ -0,0 +1,406 @@
const { createApp, ref, computed, onMounted, watch, onUnmounted } = Vue
createApp({
setup() {
const currentSong = ref({
title: '',
artist: '',
album: '',
cover: '',
lyrics: [],
tags: null,
name: '' // 原始文件名
})
const isPlaying = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const volume = ref(1)
const playMode = ref('repeat') // 'repeat', 'repeat_one', 'shuffle'
const currentLyricIndex = ref(0)
const isLoading = ref(false)
const error = ref(null)
const lyricsOffset = ref(0) // 歌词偏移值(秒)
const showControlPanel = ref(true) // 控制面板显示状态
// Toast 提示相关
const showToast = ref(false)
const toastMessage = ref('')
const toastType = ref('alert-info')
let toastTimer = null
// 获取设备ID
const deviceId = localStorage.getItem('cur_did') || 'web_device'
// 保存设备ID到localStorage
localStorage.setItem('cur_did', deviceId)
// 从localStorage获取保存的歌词偏移值
const savedOffset = localStorage.getItem('lyrics_offset')
if (savedOffset !== null) {
lyricsOffset.value = parseFloat(savedOffset)
}
// 调整歌词偏移
function adjustLyricsOffset(seconds) {
lyricsOffset.value += seconds
// 保存偏移值到localStorage
localStorage.setItem('lyrics_offset', lyricsOffset.value.toString())
// 重新解析歌词
if (currentSong.value.tags?.lyrics) {
currentSong.value.lyrics = parseLyrics(currentSong.value.tags.lyrics)
updateCurrentLyric()
}
}
// 重置歌词偏移
function resetLyricsOffset() {
lyricsOffset.value = 0
localStorage.setItem('lyrics_offset', '0')
if (currentSong.value.tags?.lyrics) {
currentSong.value.lyrics = parseLyrics(currentSong.value.tags.lyrics)
updateCurrentLyric()
}
}
// 初始化
onMounted(async () => {
// 获取并更新当前音量
try {
const volumeResponse = await API.getVolume(deviceId)
if (volumeResponse.ret === 'OK') {
volume.value = parseInt(volumeResponse.volume)
}
} catch (err) {
console.error('Error getting volume:', err)
}
// 开始定时获取播放状态
updatePlayingStatus()
setInterval(updatePlayingStatus, 1000)
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyPress)
})
// 处理键盘事件
async function handleKeyPress(event) {
// 如果用户正在输入,不处理快捷键
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return
}
switch (event.code) {
case 'Space': // 空格键:播放/暂停
event.preventDefault() // 防止页面滚动
await togglePlay()
break
case 'ArrowLeft': // 左方向键:上一首
event.preventDefault()
await previousSong()
break
case 'ArrowRight': // 右方向键:下一首
event.preventDefault()
await nextSong()
break
case 'ArrowUp': // 上方向键:增加音量
event.preventDefault()
if (volume.value < 100) {
volume.value = Math.min(100, volume.value + 5)
await setVolume()
}
break
case 'ArrowDown': // 下方向键:减小音量
event.preventDefault()
if (volume.value > 0) {
volume.value = Math.max(0, volume.value - 5)
await setVolume()
}
break
}
}
// 在组件销毁时移除事件监听
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyPress)
})
// 更新播放状态
async function updatePlayingStatus() {
try {
error.value = null
// 获取当前播放状态
const status = await API.getPlayingStatus(deviceId)
if (status.ret === 'OK') {
// 更新播放状态
isPlaying.value = status.is_playing
currentTime.value = status.offset || 0
duration.value = status.duration || 0
// 如果有正在播放的音乐且音乐发生改变
if (status.cur_music && status.cur_music !== currentSong.value.name) {
isLoading.value = true
try {
// 获取音乐详细信息
const musicInfo = await API.getMusicInfo(status.cur_music)
if (musicInfo && musicInfo.ret === 'OK') {
const tags = musicInfo.tags || {}
currentSong.value = {
title: tags.title || musicInfo.name,
artist: tags.artist || '未知歌手',
album: tags.album || '未知专辑',
cover: tags.picture || `/cover?name=${encodeURIComponent(musicInfo.name)}`,
lyrics: parseLyrics(tags.lyrics || ''),
tags: {
year: tags.year,
genre: tags.genre
},
name: musicInfo.name
}
// 更新当前歌词
updateCurrentLyric()
}
} finally {
isLoading.value = false
}
} else {
// 即使歌曲没有改变,也要更新当前歌词(因为时间在变化)
updateCurrentLyric()
}
}
} catch (err) {
error.value = '获取播放状态失败'
console.error('Error updating playing status:', err)
}
}
// 解析歌词
function parseLyrics(lyricsText) {
if (!lyricsText) return []
const lines = lyricsText.split('\n')
const lyrics = []
const timeRegex = /\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/
for (const line of lines) {
const match = line.match(timeRegex)
if (match) {
const minutes = parseInt(match[1])
const seconds = parseInt(match[2])
const milliseconds = parseInt(match[3])
const text = match[4].trim()
// 只保留实际歌词行,排除元数据
if (text && !text.startsWith('[') &&
!text.includes('Lyricist') && !text.includes('Composer') &&
!text.includes('Producer') && !text.includes('Engineer') &&
!text.includes('Studio') && !text.includes('Company') &&
!text.includes('') && !text.includes('Original') &&
!text.includes('Design') && !text.includes('Director') &&
!text.includes('Supervisor') && !text.includes('Promoter')) {
// 保存原始时间戳,不应用偏移
const time = minutes * 60 + seconds + (milliseconds / 1000)
lyrics.push({
time: Math.max(0, time),
text: text
})
}
}
}
return lyrics.sort((a, b) => a.time - b.time)
}
// 更新当前歌词
function updateCurrentLyric() {
const lyrics = currentSong.value.lyrics
if (!lyrics.length) return
// 找到当前时间对应的歌词
let foundIndex = -1
// 应用偏移后的当前时间
const currentTimeWithOffset = currentTime.value - lyricsOffset.value
// 二分查找优化性能
let left = 0
let right = lyrics.length - 1
while (left <= right) {
const mid = Math.floor((left + right) / 2)
const lyricTime = lyrics[mid].time
if (mid === lyrics.length - 1) {
if (currentTimeWithOffset >= lyricTime) {
foundIndex = mid
break
}
} else {
const nextTime = lyrics[mid + 1].time
if (currentTimeWithOffset >= lyricTime && currentTimeWithOffset < nextTime) {
foundIndex = mid
break
}
}
if (currentTimeWithOffset < lyricTime) {
right = mid - 1
} else {
left = mid + 1
}
}
// 如果找到新的歌词索引,更新显示
if (foundIndex !== -1 && foundIndex !== currentLyricIndex.value) {
currentLyricIndex.value = foundIndex
// 获取歌词容器和当前歌词元素
const container = document.querySelector('.lyrics-container')
const currentLyric = container?.querySelector(`[data-index="${foundIndex}"]`)
if (container && currentLyric) {
// 计算目标滚动位置,使当前歌词保持在容器中央
const containerHeight = container.offsetHeight
const lyricHeight = currentLyric.offsetHeight
const targetPosition = currentLyric.offsetTop - (containerHeight / 2) + (lyricHeight / 2)
// 使用平滑滚动
container.scrollTo({
top: targetPosition,
behavior: 'smooth'
})
// 添加高亮动画效果
currentLyric.style.transition = 'transform 0.3s ease-out'
currentLyric.style.transform = 'scale(1.05)'
setTimeout(() => {
currentLyric.style.transform = 'scale(1)'
}, 200)
}
}
}
// 显示提示
function showMessage(message, type = 'info') {
if (toastTimer) {
clearTimeout(toastTimer)
}
toastMessage.value = message
toastType.value = `alert-${type}`
showToast.value = true
toastTimer = setTimeout(() => {
showToast.value = false
}, 3000)
}
// 播放控制
async function togglePlay() {
const cmd = isPlaying.value ? API.commands.PLAY_PAUSE : API.commands.PLAY_CONTINUE
const response = await API.sendCommand(deviceId, cmd)
if (response.ret === 'OK') {
isPlaying.value = !isPlaying.value
showMessage(isPlaying.value ? '开始播放' : '暂停播放')
}
}
async function previousSong() {
const response = await API.sendCommand(deviceId, API.commands.PLAY_PREVIOUS)
if (response.ret === 'OK') {
showMessage('播放上一首')
}
}
async function nextSong() {
const response = await API.sendCommand(deviceId, API.commands.PLAY_NEXT)
if (response.ret === 'OK') {
showMessage('播放下一首')
}
}
async function stopPlay() {
const response = await API.sendCommand(deviceId, API.commands.PLAY_PAUSE)
if (response.ret === 'OK') {
isPlaying.value = false
showMessage('停止播放')
}
}
async function setPlayMode(mode) {
let cmd
let modeName
switch (mode) {
case 'repeat':
cmd = API.commands.PLAY_MODE_SEQUENCE
modeName = '顺序播放'
break
case 'repeat_one':
cmd = API.commands.PLAY_MODE_SINGLE
modeName = '单曲循环'
break
case 'shuffle':
cmd = API.commands.PLAY_MODE_RANDOM
modeName = '随机播放'
break
}
if (cmd) {
const response = await API.sendCommand(deviceId, cmd)
if (response.ret === 'OK') {
playMode.value = mode
showMessage(`切换到${modeName}模式`)
}
}
}
// 音量控制
async function setVolume() {
try {
const volumeValue = parseInt(volume.value)
const response = await API.setVolume(deviceId, volumeValue)
if (response.ret === 'OK') {
showMessage(`音量: ${volumeValue}%`)
} else {
console.error('Failed to set volume:', response)
}
} catch (error) {
console.error('Error setting volume:', error)
}
}
// 进度控制
function seek() {
// 更新歌词显示
updateCurrentLyric()
}
// 时间格式化
function formatTime(time) {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds.toString().padStart(2, '0')}`
}
return {
currentSong,
isPlaying,
currentTime,
duration,
volume,
playMode,
currentLyricIndex,
isLoading,
error,
lyricsOffset,
showToast,
toastMessage,
toastType,
showControlPanel,
togglePlay,
seek,
setVolume,
previousSong,
nextSong,
stopPlay,
setPlayMode,
formatTime,
adjustLyricsOffset,
resetLyricsOffset
}
}
}).mount('#app')