mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
* 引入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 * fix bugs * tailwind fix bugs * Merge branch 'main' of github.com:xiaomusic * Merge branch 'main' of github.com:xiaomusic * 修复刮削歌曲的歌曲名称和文件名称不一致,播放失败的问题
This commit is contained in:
191
docs/issues/378.md
Normal file
191
docs/issues/378.md
Normal file
@@ -0,0 +1,191 @@
|
||||
---
|
||||
title: 求助:如何自动或口令刷新本地音乐列表?
|
||||
---
|
||||
|
||||
# 求助:如何自动或口令刷新本地音乐列表?
|
||||
|
||||
docker容器部署的xiaomusic,目前是[0.3.74]最新版,音响是小米AI音箱二代。目前可以正常通过口令 “小爱同学,播放本地音乐 XXX”来 播放容器外挂载的目录 /music 中的mp3、flac等音乐歌曲。
|
||||
|
||||
但是,每次往 /music 里面新拷入一些音乐之后,xiaomusic 既没有自动刷新本地音乐列表,也不能通过“小爱同学,刷新列表”、“小爱同学,本地音乐 刷新列表”、“小爱同学,刷新本地列表”、“小爱同学,刷新本地音乐列表”等口令来刷新列表,必须在电脑上打开 xiaomusic 的后台管理页面并点一下那个刷新列表的按钮,然后本地音乐列表中才能刷新看见新增的音乐。
|
||||
|
||||
请问有没有办法做到每次往 /music 拷入音乐后自动刷新列表,或者通过口令刷新列表?谢谢~
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
刷新列表口令默认不在唤醒口令里,你可以先说播放歌曲,在播放中再说刷新列表,或者把刷新列表口令加到唤醒口令里。
|
||||
|
||||
`允许唤醒的命令:`
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - foxfire881
|
||||
|
||||
> 刷新列表口令默认不在唤醒口令里,你可以先说播放歌曲,在播放中再说刷新列表,或者把刷新列表口令加到唤醒口令里。
|
||||
>
|
||||
> `允许唤醒的命令:`
|
||||
|
||||
每次都要这样操作很麻烦欸~
|
||||
|
||||
注意到xiaomusic是用python写的,python有个可以监控文件系统变化的库 watchdog 使用很方便,可否增加一个自动刷新列表的功能?当监控到音乐目录发生变化时(增、删、改),自动刷新xiaomusic的音乐列表 —— 这样连口令都不用说了,xiaomusic自动实时刷新列表,更方便了!期待~~ @hanxi
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - foxfire881
|
||||
|
||||
豆包给了个demo:
|
||||
|
||||
```python
|
||||
import time
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
class MyEventHandler(FileSystemEventHandler):
|
||||
def on_modified(self, event):
|
||||
print(f"File modified: {event.src_path}")
|
||||
|
||||
def on_created(self, event):
|
||||
print(f"File created: {event.src_path}")
|
||||
|
||||
def on_deleted(self, event):
|
||||
print(f"File deleted: {event.src_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
event_handler = MyEventHandler()
|
||||
observer = Observer()
|
||||
path = '.' # 要监控的目录
|
||||
observer.schedule(event_handler, path, recursive=True)
|
||||
observer.start()
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
```
|
||||
代码解释
|
||||
|
||||
- 定义一个继承自 FileSystemEventHandler 的类 MyEventHandler,并重写 on_modified、on_created 和 on_deleted 方法,用于处理文件修改、创建和删除事件。
|
||||
- 创建一个 Observer 对象,并将事件处理程序和要监控的目录传递给它。
|
||||
- 启动观察者,并进入一个无限循环,直到用户按下 Ctrl+C 停止程序。
|
||||
|
||||
watchdog 库的优点是实时性好,能够及时响应文件系统的变化,并且支持多种操作系统。因此,推荐使用 watchdog 库来监控文件目录变化。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - hanxi
|
||||
|
||||
@foxfire881 谢谢,有空我实现一下吧。需要做个队列延迟处理的,要不然拷贝一堆文件的时候会刷新多次。
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - foxfire881
|
||||
|
||||
刷新多次好像也影响不大,xiaomusic刷新列表挺快的,我的音乐目录有大概有100G左右近5000个mp3、flac音乐,几乎是秒刷,点一下按钮瞬间就更新了。
|
||||
|
||||
所以是不是也可以另外开个线程在后台定时每隔一段时间(用户可设置)自动刷新一下列表,这样实现快速、简单点。
|
||||
|
||||
队列方案更完美,实现也更复杂些,可以作远期目标持续优化。
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - foxfire881
|
||||
|
||||
@hanxi 另外还有一个问题想请教下如何解决?
|
||||
|
||||
一首歌曲往往有多个版本,例如王菲、梁静茹都唱过《红豆》:
|
||||
|
||||
/music/王菲/红豆.mp3
|
||||
/music/梁静茹/红豆.mp3
|
||||
/music/张惠妹/红豆生南国.mp3
|
||||
/music/其他/张学友-红豆.mp3
|
||||
|
||||
如果我想听梁静茹的《红豆》,应该说什么指令?
|
||||
“小爱同学,播放本地歌曲 红豆” —— 这个指令好像只能定位到第一个匹配到的文件,后面的文件都忽略了。
|
||||
|
||||
“小爱同学,播放本地歌曲 梁静茹 红豆” —— 这个指令无效,小爱同学没有反应。
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 7 - hanxi
|
||||
|
||||
> [@hanxi](https://github.com/hanxi) 另外还有一个问题想请教下如何解决?
|
||||
>
|
||||
> 一首歌曲往往有多个版本,例如王菲、梁静茹都唱过《红豆》:
|
||||
>
|
||||
> /music/王菲/红豆.mp3 /music/梁静茹/红豆.mp3 /music/张惠妹/红豆生南国.mp3 /music/其他/张学友-红豆.mp3
|
||||
>
|
||||
> 如果我想听梁静茹的《红豆》,应该说什么指令? “小爱同学,播放本地歌曲 红豆” —— 这个指令好像只能定位到第一个匹配到的文件,后面的文件都忽略了。
|
||||
>
|
||||
> “小爱同学,播放本地歌曲 梁静茹 红豆” —— 这个指令无效,小爱同学没有反应。
|
||||
|
||||
文件名需要唯一,像张学友-红豆一样命名。然后说张学友红豆就行。相同的文件名是会覆盖的,只有一首生效。
|
||||
|
||||
---
|
||||
|
||||
### 评论 8 - foxfire881
|
||||
|
||||
> > [@hanxi](https://github.com/hanxi) 另外还有一个问题想请教下如何解决?
|
||||
> > 一首歌曲往往有多个版本,例如王菲、梁静茹都唱过《红豆》:
|
||||
> > /music/王菲/红豆.mp3 /music/梁静茹/红豆.mp3 /music/张惠妹/红豆生南国.mp3 /music/其他/张学友-红豆.mp3
|
||||
> > 如果我想听梁静茹的《红豆》,应该说什么指令? “小爱同学,播放本地歌曲 红豆” —— 这个指令好像只能定位到第一个匹配到的文件,后面的文件都忽略了。
|
||||
> > “小爱同学,播放本地歌曲 梁静茹 红豆” —— 这个指令无效,小爱同学没有反应。
|
||||
>
|
||||
> 文件名需要唯一,像张学友-红豆一样命名。然后说张学友红豆就行。相同的文件名是会覆盖的,只有一首生效。
|
||||
|
||||
原来如此。改文件名有点麻烦,能否做到把关键字搜出来的结果作为一个播放列表顺序播放?例如 “小爱同学,播放本地音乐 红豆”,然后 xiaomusic 把上述所有包含 “红豆” 关键字的歌曲全部搜索出来,作为一个临时播放列表顺序播放?
|
||||
|
||||
---
|
||||
|
||||
### 评论 9 - hanxi
|
||||
|
||||
可以用 music tag web 工具自动改名。不能同名是最早的基础设定,改动比较大,现在不好改了。
|
||||
|
||||
---
|
||||
|
||||
### 评论 10 - foxfire881
|
||||
|
||||
> 可以用 music tag web 工具自动改名。不能同名是最早的基础设定,改动比较大,现在不好改了。
|
||||
|
||||
关键是网上下载的很多mp3、flac文件没有 tag 或者 tag 不规范,要重命名得先把 tag 整理一遍,那个工程量就太太太大了
|
||||
|
||||
没关系,先这样用着吧,不着急。以后有空了还是希望可以重构一下,解决同名文件的问题。
|
||||
|
||||
或者也可以考虑下不一定要严格按照文件名去匹配,可以把文件路径、目录名也包含在模糊匹配规则里面,这样也可以大大提高命中准确率。
|
||||
|
||||
---
|
||||
|
||||
### 评论 11 - AisukaYuki
|
||||
|
||||
其实你可以用定时任务定时刷新列表。暂时先实现一下,隔几分钟刷新一下。
|
||||
```
|
||||
[
|
||||
{
|
||||
"expression": "*/10 * * * *",
|
||||
"name": "refresh_music_list"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 评论 12 - newmanfung
|
||||
|
||||
> > 可以用 music tag web 工具自动改名。不能同名是最早的基础设定,改动比较大,现在不好改了。
|
||||
>
|
||||
> 关键是网上下载的很多mp3、flac文件没有 tag 或者 tag 不规范,要重命名得先把 tag 整理一遍,那个工程量就太太太大了
|
||||
>
|
||||
> 没关系,先这样用着吧,不着急。以后有空了还是希望可以重构一下,解决同名文件的问题。
|
||||
>
|
||||
> 或者也可以考虑下不一定要严格按照文件名去匹配,可以把文件路径、目录名也包含在模糊匹配规则里面,这样也可以大大提高命中准确率。
|
||||
|
||||
用mp3tag,通过网上的的歌曲meta数据自动更改tag的,之前几十张专辑的时候大概3个小时就搞定了。现在我都弄了几百张专辑了。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/378)
|
||||
@@ -27,7 +27,7 @@ title: 微信交流群二维码
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
if __name__ == "__main__":
|
||||
from xiaomusic.cli import main
|
||||
|
||||
main()
|
||||
main()
|
||||
@@ -8,10 +8,13 @@ import tempfile
|
||||
import urllib.parse
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import asdict
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
from typing import TYPE_CHECKING, TYPE_CHECKING, Annotated
|
||||
|
||||
import socketio
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from xiaomusic.xiaomusic import XiaoMusic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from xiaomusic.xiaomusic import XiaoMusic
|
||||
|
||||
|
||||
5
xiaomusic/static/tailwind/api.js
vendored
5
xiaomusic/static/tailwind/api.js
vendored
@@ -29,7 +29,10 @@ const API = {
|
||||
// 获取当前播放状态
|
||||
async getPlayingStatus(did = 'web_device') {
|
||||
const response = await fetch(`/playingmusic?did=${did}`);
|
||||
return response.json();
|
||||
const data = await response.json();
|
||||
localStorage.setItem('cur_music', data.cur_music);
|
||||
localStorage.setItem('cur_playlist', data.cur_playlist);
|
||||
return data;
|
||||
},
|
||||
|
||||
// 播放歌单中的歌曲
|
||||
|
||||
352
xiaomusic/static/tailwind/index.html
vendored
352
xiaomusic/static/tailwind/index.html
vendored
@@ -132,7 +132,7 @@
|
||||
<!-- 系统歌单 -->
|
||||
<ul class="menu gap-1 mb-4">
|
||||
<li v-for="playlist in systemPlaylists" :key="playlist.id">
|
||||
<a class="flex justify-between" :class="{ active: currentPlaylist === playlist.name }"
|
||||
<a class="flex justify-between" :class="{ active: curSelectPlaylist === playlist.name }"
|
||||
@click="selectPlaylist(playlist.name)">
|
||||
{{ playlist.name }}
|
||||
<div class="badge badge-sm">{{ playlist.count }}</div>
|
||||
@@ -144,7 +144,7 @@
|
||||
<div class="divider">自定义歌单</div>
|
||||
<ul class="menu gap-1">
|
||||
<li v-for="playlist in customPlaylists" :key="playlist.name">
|
||||
<a class="flex justify-between" :class="{ active: currentPlaylist === playlist.name }"
|
||||
<a class="flex justify-between" :class="{ active: curSelectPlaylist === playlist.name }"
|
||||
@click="selectPlaylist(playlist.name)">
|
||||
<span class="truncate flex-1">{{ playlist.name }}</span>
|
||||
<div class="badge badge-sm">{{ playlist.count }}</div>
|
||||
@@ -157,7 +157,7 @@
|
||||
<!-- 右侧歌曲列表 -->
|
||||
<div class="flex-1 bg-base-100 rounded-box p-4 shadow-lg overflow-hidden">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">{{ currentPlaylist }}</h2>
|
||||
<h2 class="text-xl font-bold">{{ curSelectPlaylist }}</h2>
|
||||
</div>
|
||||
<div class="h-[calc(100vh-240px)] overflow-y-auto">
|
||||
<table class="table w-full">
|
||||
@@ -258,14 +258,16 @@
|
||||
</div>
|
||||
</a>
|
||||
</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 }}
|
||||
</div>
|
||||
<div class="text-sm opacity-50">
|
||||
{{ currentPlaylist }}
|
||||
</div>
|
||||
<div class="cursor-pointer hover:text-primary transition-colors" class="cursor-pointer hover:text-primary transition-colors">
|
||||
<a href="./now_playing.html" target="_blank">
|
||||
<div class="font-bold">{{ currentSong?.title }}</div>
|
||||
<div class="text-sm opacity-50">
|
||||
{{ currentSong?.artist }} - {{ currentSong?.album }}
|
||||
</div>
|
||||
<div class="text-sm opacity-50">
|
||||
{{ currentPlaylist }}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-circle btn-sm"
|
||||
@click="currentSong && toggleFavorite(currentSong)"
|
||||
@@ -371,10 +373,11 @@
|
||||
const currentTime = ref(0);
|
||||
const duration = ref(0);
|
||||
const volume = ref(80);
|
||||
const currentPlaylist = ref(localStorage.getItem('cur_playlist') || '所有歌曲');
|
||||
const curSelectPlaylist = ref('所有歌曲');
|
||||
const systemPlaylists = ref([]);
|
||||
const customPlaylists = ref([]);
|
||||
const newPlaylistName = ref('');
|
||||
const currentPlaylist = ref(localStorage.getItem('cur_playlist') || '所有歌曲');
|
||||
|
||||
// 添加音频播放相关状态
|
||||
const audioPlayer = ref(null);
|
||||
@@ -458,10 +461,10 @@
|
||||
// 优化歌单切换
|
||||
const selectPlaylist = async (name) => {
|
||||
try {
|
||||
if (name === currentPlaylist.value) return;
|
||||
if (name === curSelectPlaylist.value) return;
|
||||
|
||||
currentPlaylist.value = name;
|
||||
localStorage.setItem('cur_playlist', name);
|
||||
curSelectPlaylist.value = name;
|
||||
localStorage.setItem('curSelectPlaylist', name);
|
||||
|
||||
// 获取歌单中的歌曲
|
||||
let songNames = [];
|
||||
@@ -481,12 +484,14 @@
|
||||
if (window.did === 'web_device') return;
|
||||
|
||||
const data = await API.getPlayingStatus(window.did);
|
||||
|
||||
|
||||
if (data.ret === 'OK') {
|
||||
isPlaying.value = data.is_playing;
|
||||
currentTime.value = data.offset || 0;
|
||||
duration.value = data.duration || 0;
|
||||
|
||||
currentPlaylist.value = data.cur_playlist;
|
||||
|
||||
if (data.cur_music && data.cur_music !== currentSong.value?.title) {
|
||||
try {
|
||||
// 获取音乐详细信息
|
||||
@@ -516,6 +521,7 @@
|
||||
|
||||
localStorage.setItem('cur_music', data.cur_music);
|
||||
localStorage.setItem('is_playing', data.is_playing);
|
||||
localStorage.setItem('cur_playlist', data.cur_playlist);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -530,7 +536,7 @@
|
||||
localStorage.setItem('theme', theme);
|
||||
// 更新 isDarkTheme 状态,用于图标显示
|
||||
isDarkTheme.value = ['dark', 'black', 'luxury', 'dracula', 'night', 'coffee'].includes(theme);
|
||||
showNotification(`已切换到 ${theme} 主题`, 'alert-success');
|
||||
showMessage(`已切换到 ${theme} 主题`, 'alert-success');
|
||||
};
|
||||
|
||||
const filteredSongs = computed(() => {
|
||||
@@ -770,24 +776,74 @@
|
||||
// 音频播放控制方法
|
||||
const initAudioPlayer = async () => {
|
||||
try {
|
||||
// 创建音频播放器
|
||||
// 检查是否已经存在音频播放器
|
||||
let existingPlayer = document.getElementById('audio-player');
|
||||
if (existingPlayer) {
|
||||
document.body.removeChild(existingPlayer);
|
||||
}
|
||||
|
||||
// 创建新的音频播放器
|
||||
const audio = document.createElement('audio');
|
||||
audio.id = 'audio-player';
|
||||
|
||||
// 设置音频属性
|
||||
audio.preload = 'auto'; // 预加载
|
||||
audio.crossOrigin = 'anonymous'; // 允许跨域
|
||||
|
||||
// 添加到文档
|
||||
document.body.appendChild(audio);
|
||||
audioPlayer.value = audio;
|
||||
|
||||
// 监听错误事件
|
||||
audio.addEventListener('error', (e) => {
|
||||
console.error('Audio playback error:', e);
|
||||
const error = e.target.error;
|
||||
let errorMessage = '播放出错';
|
||||
|
||||
if (error) {
|
||||
switch (error.code) {
|
||||
case error.MEDIA_ERR_ABORTED:
|
||||
errorMessage = '播放被中断';
|
||||
break;
|
||||
case error.MEDIA_ERR_NETWORK:
|
||||
errorMessage = '网络错误';
|
||||
break;
|
||||
case error.MEDIA_ERR_DECODE:
|
||||
errorMessage = '解码错误';
|
||||
break;
|
||||
case error.MEDIA_ERR_SRC_NOT_SUPPORTED:
|
||||
errorMessage = '不支持的音频格式';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showMessage(errorMessage, 'alert-error');
|
||||
isPlaying.value = false;
|
||||
});
|
||||
|
||||
// 监听播放状态变化
|
||||
audio.addEventListener('play', () => {
|
||||
isPlaying.value = true;
|
||||
localStorage.setItem('is_playing', 'true');
|
||||
});
|
||||
|
||||
audio.addEventListener('pause', () => {
|
||||
isPlaying.value = false;
|
||||
localStorage.setItem('is_playing', 'false');
|
||||
});
|
||||
|
||||
// 监听播放进度
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
currentTime.value = audio.currentTime;
|
||||
duration.value = audio.duration;
|
||||
// 保存播放进度到localStorage
|
||||
localStorage.setItem('current_time', audio.currentTime.toString());
|
||||
});
|
||||
|
||||
// 监听播放状态
|
||||
audio.addEventListener('play', () => {
|
||||
isPlaying.value = true;
|
||||
});
|
||||
|
||||
audio.addEventListener('pause', () => {
|
||||
isPlaying.value = false;
|
||||
// 监听元数据加载
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
duration.value = audio.duration;
|
||||
localStorage.setItem('duration', audio.duration.toString());
|
||||
});
|
||||
|
||||
// 监听播放结束
|
||||
@@ -799,23 +855,31 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 监听加载元数据
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
duration.value = audio.duration;
|
||||
});
|
||||
|
||||
// 监听错误
|
||||
audio.addEventListener('error', (e) => {
|
||||
console.error('Audio playback error:', e);
|
||||
showNotification('播放出错,请重试', 'alert-error');
|
||||
});
|
||||
|
||||
audioPlayer.value = audio;
|
||||
|
||||
// 设置初始音量
|
||||
audio.volume = volume.value / 100;
|
||||
|
||||
// 恢复上次的播放状态
|
||||
const lastPlayingState = localStorage.getItem('is_playing') === 'true';
|
||||
const lastCurrentTime = parseFloat(localStorage.getItem('current_time') || '0');
|
||||
const lastSong = localStorage.getItem('cur_music');
|
||||
|
||||
if (lastSong && lastPlayingState) {
|
||||
try {
|
||||
const musicInfo = await API.getMusicInfo(lastSong);
|
||||
if (musicInfo && musicInfo.ret === 'OK' && musicInfo.url) {
|
||||
audio.src = musicInfo.url;
|
||||
audio.currentTime = lastCurrentTime;
|
||||
if (lastPlayingState) {
|
||||
await audio.play();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restoring last playing state:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing audio player:', error);
|
||||
showMessage('初始化音频播放器失败', 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -823,57 +887,125 @@
|
||||
try {
|
||||
if (!song || !song.title) {
|
||||
console.error('Invalid song object:', song);
|
||||
showMessage('无效的歌曲信息', 'alert-error');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPlaylistName = localStorage.getItem("cur_playlist") || "所有歌曲";
|
||||
console.log('Playing song:', song.title, 'from playlist:', currentPlaylistName);
|
||||
|
||||
const currentPlaylist = curSelectPlaylist.value;
|
||||
console.log('did', window.did, 'Playing song:', song.id, 'from playlist:', currentPlaylist);
|
||||
// song.id 修复刮削歌曲的歌曲名称和文件名称不一致的问题
|
||||
if (window.did === 'web_device') {
|
||||
// Web播放模式
|
||||
const musicInfo = await API.getMusicInfo(song.title);
|
||||
if (!musicInfo || musicInfo.ret !== 'OK') {
|
||||
console.error('Failed to get music info:', musicInfo);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const musicInfo = await API.getMusicInfo(song.id);
|
||||
if (!musicInfo || musicInfo.ret !== 'OK') {
|
||||
console.error('Failed to get music info:', musicInfo);
|
||||
showMessage('获取歌曲信息失败', 'alert-error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioPlayer.value) {
|
||||
if (audioPlayer.value.src === musicInfo.url) {
|
||||
// 同一首歌,切换播放状态
|
||||
if (audioPlayer.value.paused) {
|
||||
await audioPlayer.value.play();
|
||||
isPlaying.value = true;
|
||||
} else {
|
||||
audioPlayer.value.pause();
|
||||
if (!musicInfo.url) {
|
||||
console.error('No URL in music info:', musicInfo);
|
||||
showMessage('歌曲URL无效', 'alert-error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证URL是否有效
|
||||
const validUrl = new URL(musicInfo.url);
|
||||
if (!validUrl.pathname.endsWith('.mp3')) {
|
||||
console.error('Invalid music URL format:', validUrl);
|
||||
showMessage('音乐文件格式不支持', 'alert-error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (audioPlayer.value) {
|
||||
try {
|
||||
if (audioPlayer.value.src === musicInfo.url) {
|
||||
// 同一首歌,切换播放状态
|
||||
if (audioPlayer.value.paused) {
|
||||
await audioPlayer.value.play();
|
||||
isPlaying.value = true;
|
||||
showMessage('继续播放');
|
||||
} else {
|
||||
audioPlayer.value.pause();
|
||||
isPlaying.value = false;
|
||||
showMessage('暂停播放');
|
||||
}
|
||||
} else {
|
||||
// 播放新歌曲
|
||||
audioPlayer.value.src = musicInfo.url;
|
||||
await audioPlayer.value.play();
|
||||
isPlaying.value = true;
|
||||
showMessage('开始播放新歌曲');
|
||||
}
|
||||
|
||||
// 更新当前歌曲和播放状态
|
||||
currentSong.value = song;
|
||||
localStorage.setItem("cur_music", song.id);
|
||||
localStorage.setItem("cur_playlist", currentPlaylist);
|
||||
localStorage.setItem("is_playing", "true");
|
||||
|
||||
// 高亮当前播放的歌曲
|
||||
songs.value = songs.value.map(s => ({
|
||||
...s,
|
||||
isPlaying: s.id === song.id
|
||||
}));
|
||||
} catch (playError) {
|
||||
console.error('Error playing audio:', playError);
|
||||
showMessage('播放失败: ' + playError.message, 'alert-error');
|
||||
// 重置播放状态
|
||||
isPlaying.value = false;
|
||||
localStorage.setItem("is_playing", "false");
|
||||
}
|
||||
} else {
|
||||
// 播放新歌曲
|
||||
audioPlayer.value.src = musicInfo.url;
|
||||
await audioPlayer.value.play();
|
||||
isPlaying.value = true;
|
||||
console.error('Audio player not initialized');
|
||||
showMessage('音频播放器未初始化', 'alert-error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in web playback:', error);
|
||||
showMessage('播放出错: ' + error.message, 'alert-error');
|
||||
}
|
||||
} else {
|
||||
// 设备播放模式
|
||||
const response = await API.playMusicFromList(window.did, currentPlaylistName, song.title);
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = true;
|
||||
try {
|
||||
// 如果是当前正在播放的歌曲,则切换播放状态
|
||||
if (currentSong.value && song.id === currentSong.value.id) {
|
||||
if (isPlaying.value) {
|
||||
const response = await API.sendCommand(window.did, API.commands.PLAY_PAUSE);
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = false;
|
||||
showMessage('暂停播放');
|
||||
}
|
||||
} else {
|
||||
const response = await API.playMusicFromList(window.did, currentPlaylist, song.id);
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = true;
|
||||
showMessage('继续播放');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 播放新的歌曲
|
||||
const response = await API.playMusicFromList(window.did, currentPlaylist, song.id);
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = true;
|
||||
currentSong.value = song;
|
||||
localStorage.setItem("cur_music", song.id);
|
||||
localStorage.setItem("cur_playlist", currentPlaylist);
|
||||
localStorage.setItem("is_playing", "true");
|
||||
showMessage('开始播放');
|
||||
} else {
|
||||
console.error('Device playback failed:', response);
|
||||
showMessage('设备播放失败', 'alert-error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in device playback:', error);
|
||||
showMessage('设备播放出错: ' + error.message, 'alert-error');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前歌曲和播放状态
|
||||
currentSong.value = song;
|
||||
localStorage.setItem("cur_music", song.title);
|
||||
|
||||
// 高亮当前播放的歌曲
|
||||
songs.value = songs.value.map(s => ({
|
||||
...s,
|
||||
isPlaying: s.id === song.id
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error playing song:', error);
|
||||
showNotification('播放失败,请重试', 'alert-error');
|
||||
console.error('Error in playSong:', error);
|
||||
showMessage('播放失败,请重试', 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -891,15 +1023,38 @@
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 设备播放模式
|
||||
const response = await API.sendCommand(window.did, isPlaying.value ? "暂停播放" : "继续播放");
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = !isPlaying.value;
|
||||
try {
|
||||
if (isPlaying.value) {
|
||||
// 如果正在播放,则暂停
|
||||
const response = await API.sendCommand(window.did, API.commands.PLAY_PAUSE)
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = false
|
||||
showMessage('小爱同学: 暂停播放')
|
||||
}
|
||||
} else {
|
||||
// 如果当前是暂停状态,获取当前歌曲信息并重新播放
|
||||
const status = await API.getPlayingStatus(window.did)
|
||||
if (status.ret === 'OK' && status.cur_music && status.cur_playlist) {
|
||||
// 使用 playmusiclist 接口重新播放当前歌曲
|
||||
const response = await API.playMusicFromList(window.did, status.cur_playlist, status.cur_music)
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = true
|
||||
showMessage('小爱同学: 开始播放')
|
||||
} else {
|
||||
showMessage('小爱同学: 播放失败', 'error')
|
||||
}
|
||||
} else {
|
||||
showMessage('小爱同学: 获取播放信息失败', 'error')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling play state:', error)
|
||||
showMessage('小爱同学: 播放控制失败', 'error')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling play state:', error);
|
||||
showNotification('播放控制失败,请重试', 'alert-error');
|
||||
showMessage('播放控制失败,请重试' + window.did, 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -963,7 +1118,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error playing previous song:', error);
|
||||
showNotification('切换上一首失败,请重试', 'alert-error');
|
||||
showMessage('切换上一首失败,请重试', 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -987,7 +1142,7 @@
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error playing next song:', error);
|
||||
showNotification('切换下一首失败,请重试', 'alert-error');
|
||||
showMessage('切换下一首失败,请重试', 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1000,19 +1155,19 @@
|
||||
// Web播放模式下直接更新
|
||||
currentPlayMode.value = newMode;
|
||||
localStorage.setItem('play_mode', newMode.toString());
|
||||
showNotification(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
|
||||
showMessage(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
|
||||
} else {
|
||||
// 设备播放模式
|
||||
const response = await API.sendCommand(window.did, playModes.value[newMode].cmd);
|
||||
if (response.ret === 'OK') {
|
||||
currentPlayMode.value = newMode;
|
||||
localStorage.setItem('play_mode', newMode.toString());
|
||||
showNotification(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
|
||||
showMessage(`已切换到${playModes.value[newMode].cmd}模式`, 'alert-info');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling play mode:', error);
|
||||
showNotification('切换播放模式失败,请重试', 'alert-error');
|
||||
showMessage('切换播放模式失败,请重试', 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1075,17 +1230,17 @@
|
||||
// 修改 toggleFavorite 方法
|
||||
const toggleFavorite = async (song) => {
|
||||
try {
|
||||
const isLiked = favoriteList.value.includes(song.title);
|
||||
const isLiked = favoriteList.value.includes(song.id);
|
||||
const cmd = isLiked ? "取消收藏" : "加入收藏";
|
||||
|
||||
localStorage.setItem("cur_music", song.title);
|
||||
localStorage.setItem("cur_music", song.id);
|
||||
|
||||
const response = await API.sendCommand(window.did, cmd);
|
||||
if (response.ret === 'OK') {
|
||||
if (isLiked) {
|
||||
favoriteList.value = favoriteList.value.filter(name => name !== song.title);
|
||||
favoriteList.value = favoriteList.value.filter(name => name !== song.id);
|
||||
} else {
|
||||
favoriteList.value.push(song.title);
|
||||
favoriteList.value.push(song.id);
|
||||
}
|
||||
|
||||
if (currentPlaylist.value === '收藏') {
|
||||
@@ -1103,7 +1258,7 @@
|
||||
const toastType = ref('alert-info');
|
||||
|
||||
// 添加显示 toast 的方法
|
||||
const showNotification = (message, type = 'alert-info') => {
|
||||
const showMessage = (message, type = 'alert-info') => {
|
||||
toastMessage.value = message;
|
||||
toastType.value = type;
|
||||
showToast.value = true;
|
||||
@@ -1116,7 +1271,7 @@
|
||||
// 修改 deleteMusic 方法
|
||||
const deleteMusic = async (song) => {
|
||||
try {
|
||||
if (!confirm(`确定要删除歌曲"${song.title}"吗?`)) {
|
||||
if (!confirm(`确定要删除歌曲"${song.id}"吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1125,35 +1280,35 @@
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ name: song.title })
|
||||
body: JSON.stringify({ name: song.id })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data === 'success' || data.ret === 'OK') {
|
||||
// 如果删除的是当前播放的歌曲,切换到下一首
|
||||
if (currentSong.value?.title === song.title) {
|
||||
if (currentSong.value?.id === song.id) {
|
||||
await playNext();
|
||||
}
|
||||
|
||||
// 从当前列表中移除歌曲
|
||||
songs.value = songs.value.filter(s => s.title !== song.title);
|
||||
songs.value = songs.value.filter(s => s.id !== song.id);
|
||||
|
||||
// 如果在收藏列表中,也从收藏列表中移除
|
||||
if (favoriteList.value.includes(song.title)) {
|
||||
favoriteList.value = favoriteList.value.filter(name => name !== song.title);
|
||||
if (favoriteList.value.includes(song.id)) {
|
||||
favoriteList.value = favoriteList.value.filter(name => name !== song.id);
|
||||
}
|
||||
|
||||
// 刷新歌单信息
|
||||
await loadPlaylists();
|
||||
|
||||
// 显示成功通知
|
||||
showNotification(`删除歌曲"${song.title}"成功`, 'alert-success');
|
||||
showMessage(`删除歌曲"${song.id}"成功`, 'alert-success');
|
||||
} else {
|
||||
throw new Error(typeof data === 'string' ? data : (data.msg || '删除失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting song:', error);
|
||||
showNotification(`删除失败: ${error.message}`, 'alert-error');
|
||||
showMessage(`删除失败: ${error.message}`, 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1226,10 +1381,10 @@
|
||||
currentTime.value = 0;
|
||||
}
|
||||
}
|
||||
showNotification('已发送停止播放的请求', 'alert-info');
|
||||
showMessage('已发送停止播放的请求', 'alert-info');
|
||||
} catch (error) {
|
||||
console.error('Error stopping playback:', error);
|
||||
showNotification('停止播放失败,请重试', 'alert-error');
|
||||
showMessage('停止播放失败,请重试', 'alert-error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1352,6 +1507,7 @@
|
||||
playNext,
|
||||
togglePlayMode,
|
||||
playSong,
|
||||
curSelectPlaylist,
|
||||
currentPlaylist,
|
||||
systemPlaylists,
|
||||
customPlaylists,
|
||||
|
||||
270
xiaomusic/static/tailwind/now_playing.html
vendored
270
xiaomusic/static/tailwind/now_playing.html
vendored
@@ -13,6 +13,276 @@
|
||||
<script src="./api.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="font-sans">
|
||||
<div id="app" class="h-screen flex flex-col overflow-hidden">
|
||||
<!-- 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 flex-col h-full">
|
||||
<!-- 添加一个返回按钮 在页面左侧顶部-->
|
||||
<a class="btn btn-sm btn-ghost text-white/70 hover:text-white fixed top-4 left-4" href="./index.html">
|
||||
<span class="material-icons text-lg">arrow_back</span>
|
||||
</a>
|
||||
<!-- 顶部信息 -->
|
||||
<div class="text-center p-6">
|
||||
<h2 class="text-3xl font-bold text-white mb-1 tracking-wide">{{ currentSong.title }}</h2>
|
||||
<p class="text-sm text-white/40 mb-1">{{ currentSong.cur_playlist }}</p>
|
||||
<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-1 overflow-y-auto lyrics-container px-4 py-2 scrollbar-hide">
|
||||
<div class="max-w-2xl mx-auto pb-32">
|
||||
<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 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 class="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/60 to-transparent pt-10 pb-4 px-4 z-20">
|
||||
<!-- 进度条 -->
|
||||
<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 cursor-pointer" @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>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* 添加平滑滚动效果 */
|
||||
.lyrics-container {
|
||||
scroll-behavior: smooth;
|
||||
mask-image: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
black 10%,
|
||||
black 90%,
|
||||
transparent 100%
|
||||
);
|
||||
-webkit-mask-image: linear-gradient(to bottom,
|
||||
transparent 0%,
|
||||
black 10%,
|
||||
black 90%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
/* 自定义范围输入样式 */
|
||||
.range {
|
||||
height: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.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::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.range::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.range::-moz-range-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 歌词动画效果 */
|
||||
.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><!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 提示 -->
|
||||
|
||||
565
xiaomusic/static/tailwind/now_playing.js
vendored
565
xiaomusic/static/tailwind/now_playing.js
vendored
@@ -1,5 +1,570 @@
|
||||
const { createApp, ref, computed, onMounted, watch, onUnmounted } = Vue
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const currentSong = ref({
|
||||
title: '',
|
||||
artist: '',
|
||||
album: '',
|
||||
cover: '',
|
||||
lyrics: [],
|
||||
tags: null,
|
||||
name: '', // 原始文件名
|
||||
cur_playlist: '' // 当前歌单
|
||||
})
|
||||
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) // 控制面板显示状态
|
||||
const deviceId = ref(localStorage.getItem('cur_did') || 'web_device')
|
||||
const audioPlayer = ref(null) // 添加 audioPlayer ref
|
||||
|
||||
// Toast 提示相关
|
||||
const showToast = ref(false)
|
||||
const toastMessage = ref('')
|
||||
const toastType = ref('alert-info')
|
||||
let toastTimer = null
|
||||
|
||||
// 从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()
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化音频播放器
|
||||
function initAudioPlayer() {
|
||||
const audio = document.createElement('audio')
|
||||
audio.id = 'audio-player'
|
||||
document.body.appendChild(audio)
|
||||
audioPlayer.value = audio
|
||||
|
||||
// 监听播放状态变化
|
||||
audio.addEventListener('play', () => {
|
||||
isPlaying.value = true
|
||||
})
|
||||
audio.addEventListener('pause', () => {
|
||||
isPlaying.value = false
|
||||
})
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
currentTime.value = audio.currentTime
|
||||
updateCurrentLyric()
|
||||
})
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
duration.value = audio.duration
|
||||
})
|
||||
audio.addEventListener('ended', () => {
|
||||
// 根据播放模式决定下一步操作
|
||||
if (playMode.value === 'repeat_one') {
|
||||
audio.currentTime = 0
|
||||
audio.play()
|
||||
} else {
|
||||
nextSong()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更新播放状态
|
||||
async function updatePlayingStatus() {
|
||||
try {
|
||||
error.value = null
|
||||
deviceId.value = localStorage.getItem('cur_did') || 'web_device'
|
||||
|
||||
if (deviceId.value === 'web_device') {
|
||||
// Web播放模式 - 从localStorage获取当前播放信息
|
||||
const curMusic = localStorage.getItem('cur_music')
|
||||
const curPlaylist = localStorage.getItem('cur_playlist')
|
||||
|
||||
if (curMusic && (!currentSong.value?.name || curMusic !== currentSong.value.name)) {
|
||||
isLoading.value = true
|
||||
try {
|
||||
// 获取音乐详细信息
|
||||
const musicInfo = await API.getMusicInfo(curMusic)
|
||||
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,
|
||||
cur_playlist: curPlaylist
|
||||
}
|
||||
|
||||
// 如果音频播放器存在且URL不同,更新URL
|
||||
if (audioPlayer.value && audioPlayer.value.src !== musicInfo.url) {
|
||||
audioPlayer.value.src = musicInfo.url
|
||||
// 如果标记为正在播放,但实际已暂停,尝试恢复播放
|
||||
if (isPlaying.value && audioPlayer.value.paused) {
|
||||
try {
|
||||
await audioPlayer.value.play()
|
||||
} catch (e) {
|
||||
console.error('Failed to resume playback:', e)
|
||||
isPlaying.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新播放状态
|
||||
if (audioPlayer.value) {
|
||||
isPlaying.value = !audioPlayer.value.paused
|
||||
currentTime.value = audioPlayer.value.currentTime || 0
|
||||
duration.value = audioPlayer.value.duration || 0
|
||||
updateCurrentLyric()
|
||||
}
|
||||
} else {
|
||||
// 设备播放模式 - 从API获取状态
|
||||
const status = await API.getPlayingStatus(deviceId.value)
|
||||
if (status.ret === 'OK') {
|
||||
// 更新播放状态
|
||||
isPlaying.value = status.is_playing
|
||||
currentTime.value = status.offset || 0
|
||||
duration.value = status.duration || 0
|
||||
currentSong.value.cur_playlist = status.cur_playlist || ''
|
||||
// 如果有正在播放的音乐且音乐发生改变
|
||||
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,
|
||||
cur_playlist: status.cur_playlist
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
// 更新当前歌词
|
||||
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 (currentLyric) {
|
||||
const containerHeight = container.clientHeight
|
||||
const lyricTop = currentLyric.offsetTop
|
||||
const lyricHeight = currentLyric.clientHeight
|
||||
// 计算滚动位置,使当前歌词在容器中垂直居中
|
||||
container.scrollTo({
|
||||
top: lyricTop - (containerHeight / 2) + (lyricHeight / 2),
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
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() {
|
||||
try {
|
||||
if (deviceId.value === 'web_device') {
|
||||
// Web播放模式
|
||||
if (!currentSong.value?.name) {
|
||||
showMessage('没有可播放的歌曲', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (isPlaying.value) {
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.pause()
|
||||
isPlaying.value = false
|
||||
showMessage('暂停播放')
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// 获取最新的音乐URL
|
||||
const musicInfo = await API.getMusicInfo(currentSong.value.name)
|
||||
if (musicInfo && musicInfo.ret === 'OK') {
|
||||
if (audioPlayer.value) {
|
||||
if (audioPlayer.value.src !== musicInfo.url) {
|
||||
audioPlayer.value.src = musicInfo.url
|
||||
}
|
||||
await audioPlayer.value.play()
|
||||
isPlaying.value = true
|
||||
showMessage('开始播放')
|
||||
}
|
||||
} else {
|
||||
showMessage('获取音乐信息失败', 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting music info:', error)
|
||||
showMessage('播放失败', 'error')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 设备播放模式
|
||||
if (isPlaying.value) {
|
||||
// 如果正在播放,则暂停
|
||||
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_PAUSE)
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = false
|
||||
showMessage('暂停播放')
|
||||
}
|
||||
} else {
|
||||
// 如果当前是暂停状态,获取当前歌曲信息并重新播放
|
||||
const status = await API.getPlayingStatus(deviceId.value)
|
||||
if (status.ret === 'OK' && status.cur_music && status.cur_playlist) {
|
||||
// 使用 playmusiclist 接口重新播放当前歌曲
|
||||
const response = await API.playMusicFromList(deviceId.value, status.cur_playlist, status.cur_music)
|
||||
if (response.ret === 'OK') {
|
||||
isPlaying.value = true
|
||||
showMessage('开始播放')
|
||||
} else {
|
||||
showMessage('播放失败', 'error')
|
||||
}
|
||||
} else {
|
||||
showMessage('获取播放信息失败', 'error')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling play state:', error)
|
||||
showMessage('播放控制失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function previousSong() {
|
||||
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_PREVIOUS)
|
||||
if (response.ret === 'OK') {
|
||||
showMessage('播放上一首')
|
||||
}
|
||||
}
|
||||
|
||||
async function nextSong() {
|
||||
const response = await API.sendCommand(deviceId.value, API.commands.PLAY_NEXT)
|
||||
if (response.ret === 'OK') {
|
||||
showMessage('播放下一首')
|
||||
}
|
||||
}
|
||||
|
||||
async function stopPlay() {
|
||||
const response = await API.sendCommand(deviceId.value, 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.value, 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.value, volumeValue)
|
||||
if (response.ret === 'OK') {
|
||||
showMessage(`音量: ${volumeValue}%`)
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.volume = volumeValue / 100
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to set volume:', response)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting volume:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动调整进度
|
||||
async function seek() {
|
||||
try {
|
||||
if (deviceId.value === 'web_device') {
|
||||
// Web播放模式
|
||||
const audio = document.getElementById('audio-player')
|
||||
if (audio) {
|
||||
audio.currentTime = currentTime.value
|
||||
}
|
||||
} else {
|
||||
// 设备播放模式
|
||||
await API.sendCommand(deviceId.value, `seek ${Math.floor(currentTime.value)}`)
|
||||
}
|
||||
// 立即更新歌词显示
|
||||
updateCurrentLyric()
|
||||
} catch (error) {
|
||||
console.error('Error seeking:', error)
|
||||
showMessage('调整进度失败', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间
|
||||
function formatTime(time) {
|
||||
if (!time) return '00:00'
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
// 初始化音频播放器
|
||||
initAudioPlayer()
|
||||
|
||||
// 获取并更新当前音量
|
||||
try {
|
||||
const volumeResponse = await API.getVolume(deviceId.value)
|
||||
if (volumeResponse.ret === 'OK') {
|
||||
volume.value = parseInt(volumeResponse.volume)
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.volume = volume.value / 100
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
if (audioPlayer.value) {
|
||||
audioPlayer.value.remove()
|
||||
}
|
||||
})
|
||||
|
||||
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') const { createApp, ref, computed, onMounted, watch, onUnmounted } = Vue
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
const currentSong = ref({
|
||||
|
||||
Reference in New Issue
Block a user