mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 06:30:49 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
573023600a | ||
|
|
041aad31b4 | ||
|
|
f652932d71 | ||
|
|
7efeb9492b | ||
|
|
055536eb5c | ||
|
|
14852fc8d3 | ||
|
|
9866e772df | ||
|
|
87ca0836b1 | ||
|
|
fa07c5a40c |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,14 +1,9 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
## v3.7.0
|
## v3.7.2
|
||||||
### ✨ 新功能
|
### ✨ 优化
|
||||||
- 添加全局快捷键支持以及快捷键管理功能
|
- 优化歌词缓存
|
||||||
- 优化设置页面样式以及布局
|
- 优化音乐播放体验 如果播放失败自动播放下一首
|
||||||
|
|
||||||
### 🐞 Bug修复
|
|
||||||
- 修复弹窗层级问题
|
|
||||||
- 修复夜间模式下 歌曲收藏样式无效问题
|
|
||||||
- 优化夜间模式播放按钮颜色
|
|
||||||
|
|
||||||
|
|
||||||
## 咖啡☕️
|
## 咖啡☕️
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AlgerMusicPlayer",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "3.7.0",
|
"version": "3.7.2",
|
||||||
"description": "Alger Music Player",
|
"description": "Alger Music Player",
|
||||||
"author": "Alger <algerkc@qq.com>",
|
"author": "Alger <algerkc@qq.com>",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { app, ipcMain, nativeImage } from 'electron';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
import { loadLyricWindow } from './lyric';
|
import { loadLyricWindow } from './lyric';
|
||||||
import { initializeCacheManager } from './modules/cache';
|
|
||||||
import { initializeConfig } from './modules/config';
|
import { initializeConfig } from './modules/config';
|
||||||
import { initializeFileManager } from './modules/fileManager';
|
import { initializeFileManager } from './modules/fileManager';
|
||||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||||
@@ -27,8 +26,6 @@ let mainWindow: Electron.BrowserWindow;
|
|||||||
function initialize() {
|
function initialize() {
|
||||||
// 初始化配置管理
|
// 初始化配置管理
|
||||||
initializeConfig();
|
initializeConfig();
|
||||||
// 初始化缓存管理
|
|
||||||
initializeCacheManager();
|
|
||||||
// 初始化文件管理
|
// 初始化文件管理
|
||||||
initializeFileManager();
|
initializeFileManager();
|
||||||
// 初始化窗口管理
|
// 初始化窗口管理
|
||||||
|
|||||||
@@ -317,8 +317,8 @@ async function downloadMusic(
|
|||||||
|
|
||||||
// 等待下载完成
|
// 等待下载完成
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
writer!.on('finish', resolve);
|
writer!.on('finish', () => resolve(undefined));
|
||||||
writer!.on('error', reject);
|
writer!.on('error', (error) => reject(error));
|
||||||
response.data.pipe(writer!);
|
response.data.pipe(writer!);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { musicDB } from '@/hooks/MusicHook';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
import type { ILyric } from '@/type/lyric';
|
import type { ILyric } from '@/type/lyric';
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import requestMusic from '@/utils/request_music';
|
import requestMusic from '@/utils/request_music';
|
||||||
|
|
||||||
|
const { addData, getData, deleteData } = musicDB;
|
||||||
|
|
||||||
// 获取音乐音质详情
|
// 获取音乐音质详情
|
||||||
export const getMusicQualityDetail = (id: number) => {
|
export const getMusicQualityDetail = (id: number) => {
|
||||||
return request.get('/song/music/detail', { params: { id } });
|
return request.get('/song/music/detail', { params: { id } });
|
||||||
@@ -37,24 +40,31 @@ export const getMusicDetail = (ids: Array<number>) => {
|
|||||||
|
|
||||||
// 根据音乐Id获取音乐歌词
|
// 根据音乐Id获取音乐歌词
|
||||||
export const getMusicLrc = async (id: number) => {
|
export const getMusicLrc = async (id: number) => {
|
||||||
if (isElectron) {
|
const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000; // 10天的毫秒数
|
||||||
// 先尝试从缓存获取
|
|
||||||
const cachedLyric = await window.api.invoke('get-cached-lyric', id);
|
try {
|
||||||
console.log('cachedLyric', cachedLyric);
|
// 尝试获取缓存的歌词
|
||||||
if (cachedLyric) {
|
const cachedLyric = await getData('music_lyric', id);
|
||||||
return { data: cachedLyric };
|
if (cachedLyric?.createTime && Date.now() - cachedLyric.createTime < TEN_DAYS_MS) {
|
||||||
|
return { ...cachedLyric };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取新的歌词数据
|
||||||
|
const res = await request.get<ILyric>('/lyric', { params: { id } });
|
||||||
|
|
||||||
|
// 只有在成功获取新数据后才删除旧缓存并添加新缓存
|
||||||
|
if (res?.data) {
|
||||||
|
if (cachedLyric) {
|
||||||
|
await deleteData('music_lyric', id);
|
||||||
|
}
|
||||||
|
addData('music_lyric', { id, data: res.data, createTime: Date.now() });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取歌词失败:', error);
|
||||||
|
throw error; // 向上抛出错误,让调用者处理
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果缓存中没有,则从服务器获取
|
|
||||||
const res = await request.get<ILyric>('/lyric', { params: { id } });
|
|
||||||
|
|
||||||
// 缓存完整的响应数据
|
|
||||||
if (isElectron && res) {
|
|
||||||
await window.api.invoke('cache-lyric', id, res.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParsingMusicUrl = (id: number, data: any) => {
|
export const getParsingMusicUrl = (id: number, data: any) => {
|
||||||
|
|||||||
3687
src/renderer/assets/css/animate.css
vendored
3687
src/renderer/assets/css/animate.css
vendored
File diff suppressed because it is too large
Load Diff
@@ -106,8 +106,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import 'animate.css';
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { computed, onActivated, onMounted, ref } from 'vue';
|
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
// 定义表配置的泛型接口
|
||||||
|
export interface StoreConfig<T extends string> {
|
||||||
|
name: T;
|
||||||
|
keyPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建一个使用 IndexedDB 的组合函数
|
// 创建一个使用 IndexedDB 的组合函数
|
||||||
const useIndexedDB = () => {
|
const useIndexedDB = async <T extends string, S extends Record<T, Record<string, any>>>(
|
||||||
const db = ref<IDBDatabase | null>(null); // 数据库引用
|
dbName: string,
|
||||||
|
stores: StoreConfig<T>[],
|
||||||
|
version: number = 1
|
||||||
|
) => {
|
||||||
|
const db = ref<IDBDatabase | null>(null);
|
||||||
|
|
||||||
// 打开数据库并创建表
|
// 打开数据库并创建表
|
||||||
const initDB = (
|
const initDB = () => {
|
||||||
dbName: string,
|
|
||||||
version: number,
|
|
||||||
stores: { name: string; keyPath?: string }[]
|
|
||||||
) => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const request = indexedDB.open(dbName, version); // 打开数据库请求
|
const request = indexedDB.open(dbName, version);
|
||||||
|
|
||||||
request.onupgradeneeded = (event: any) => {
|
request.onupgradeneeded = (event: any) => {
|
||||||
const db = event.target.result; // 获取数据库实例
|
const db = event.target.result;
|
||||||
stores.forEach((store) => {
|
stores.forEach((store) => {
|
||||||
if (!db.objectStoreNames.contains(store.name)) {
|
if (!db.objectStoreNames.contains(store.name)) {
|
||||||
// 确保对象存储(表)创建
|
|
||||||
db.createObjectStore(store.name, {
|
db.createObjectStore(store.name, {
|
||||||
keyPath: store.keyPath || 'id',
|
keyPath: store.keyPath || 'id',
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
@@ -28,39 +33,41 @@ const useIndexedDB = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
request.onsuccess = (event: any) => {
|
request.onsuccess = (event: any) => {
|
||||||
db.value = event.target.result; // 保存数据库实例
|
db.value = event.target.result;
|
||||||
resolve(); // 成功时解析 Promise
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = (event: any) => {
|
request.onerror = (event: any) => {
|
||||||
reject(event.target.error); // 失败时拒绝 Promise
|
reject(event.target.error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 通用新增数据
|
await initDB();
|
||||||
const addData = (storeName: string, value: any) => {
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
if (!db.value) return reject('数据库未初始化'); // 检查数据库是否已初始化
|
|
||||||
const tx = db.value.transaction(storeName, 'readwrite'); // 创建事务
|
|
||||||
const store = tx.objectStore(storeName); // 获取对象存储
|
|
||||||
|
|
||||||
const request = store.add(value); // 添加数据请求
|
// 通用新增数据
|
||||||
|
const addData = <K extends T>(storeName: K, value: S[K]) => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (!db.value) return reject('数据库未初始化');
|
||||||
|
const tx = db.value.transaction(storeName, 'readwrite');
|
||||||
|
const store = tx.objectStore(storeName);
|
||||||
|
|
||||||
|
const request = store.add(value);
|
||||||
|
|
||||||
request.onsuccess = () => {
|
request.onsuccess = () => {
|
||||||
console.log('成功'); // 成功时输出
|
console.log('成功');
|
||||||
resolve(); // 解析 Promise
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = (event) => {
|
request.onerror = (event) => {
|
||||||
console.error('新增失败:', (event.target as IDBRequest).error); // 输出错误
|
console.error('新增失败:', (event.target as IDBRequest).error);
|
||||||
reject((event.target as IDBRequest).error); // 拒绝 Promise
|
reject((event.target as IDBRequest).error);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 通用保存数据(新增或更新)
|
// 通用保存数据(新增或更新)
|
||||||
const saveData = (storeName: string, value: any) => {
|
const saveData = <K extends T>(storeName: K, value: S[K]) => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (!db.value) return reject('数据库未初始化');
|
if (!db.value) return reject('数据库未初始化');
|
||||||
const tx = db.value.transaction(storeName, 'readwrite');
|
const tx = db.value.transaction(storeName, 'readwrite');
|
||||||
@@ -79,8 +86,8 @@ const useIndexedDB = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 通用获取数据
|
// 通用获取数据
|
||||||
const getData = (storeName: string, key: string | number) => {
|
const getData = <K extends T>(storeName: K, key: string | number) => {
|
||||||
return new Promise<any>((resolve, reject) => {
|
return new Promise<S[K]>((resolve, reject) => {
|
||||||
if (!db.value) return reject('数据库未初始化');
|
if (!db.value) return reject('数据库未初始化');
|
||||||
const tx = db.value.transaction(storeName, 'readonly');
|
const tx = db.value.transaction(storeName, 'readonly');
|
||||||
const store = tx.objectStore(storeName);
|
const store = tx.objectStore(storeName);
|
||||||
@@ -101,7 +108,7 @@ const useIndexedDB = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 删除数据
|
// 删除数据
|
||||||
const deleteData = (storeName: string, key: string | number) => {
|
const deleteData = <K extends T>(storeName: K, key: string | number) => {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
if (!db.value) return reject('数据库未初始化');
|
if (!db.value) return reject('数据库未初始化');
|
||||||
const tx = db.value.transaction(storeName, 'readwrite');
|
const tx = db.value.transaction(storeName, 'readwrite');
|
||||||
@@ -120,8 +127,8 @@ const useIndexedDB = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 查询所有数据
|
// 查询所有数据
|
||||||
const getAllData = (storeName: string) => {
|
const getAllData = <K extends T>(storeName: K) => {
|
||||||
return new Promise<any[]>((resolve, reject) => {
|
return new Promise<S[K][]>((resolve, reject) => {
|
||||||
if (!db.value) return reject('数据库未初始化');
|
if (!db.value) return reject('数据库未初始化');
|
||||||
const tx = db.value.transaction(storeName, 'readonly');
|
const tx = db.value.transaction(storeName, 'readonly');
|
||||||
const store = tx.objectStore(storeName);
|
const store = tx.objectStore(storeName);
|
||||||
@@ -142,29 +149,29 @@ const useIndexedDB = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 分页查询数据
|
// 分页查询数据
|
||||||
const getDataWithPagination = (storeName: string, page: number, pageSize: number) => {
|
const getDataWithPagination = <K extends T>(storeName: K, page: number, pageSize: number) => {
|
||||||
return new Promise<any[]>((resolve, reject) => {
|
return new Promise<S[K][]>((resolve, reject) => {
|
||||||
if (!db.value) return reject('数据库未初始化');
|
if (!db.value) return reject('数据库未初始化');
|
||||||
const tx = db.value.transaction(storeName, 'readonly');
|
const tx = db.value.transaction(storeName, 'readonly');
|
||||||
const store = tx.objectStore(storeName);
|
const store = tx.objectStore(storeName);
|
||||||
const request = store.openCursor(); // 打开游标请求
|
const request = store.openCursor();
|
||||||
const results: any[] = []; // 存储结果的数组
|
const results: S[K][] = [];
|
||||||
let index = 0; // 当前索引
|
let index = 0;
|
||||||
const skip = (page - 1) * pageSize; // 计算跳过的数量
|
const skip = (page - 1) * pageSize;
|
||||||
|
|
||||||
request.onsuccess = (event: any) => {
|
request.onsuccess = (event: any) => {
|
||||||
const cursor = event.target.result; // 获取游标
|
const cursor = event.target.result;
|
||||||
if (!cursor) {
|
if (!cursor) {
|
||||||
resolve(results); // 如果没有更多数据,解析结果
|
resolve(results);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index >= skip && results.length < pageSize) {
|
if (index >= skip && results.length < pageSize) {
|
||||||
results.push(cursor.value); // 添加当前游标值到结果
|
results.push(cursor.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
index++; // 增加索引
|
index++;
|
||||||
cursor.continue(); // 继续游标
|
cursor.continue();
|
||||||
};
|
};
|
||||||
|
|
||||||
request.onerror = (event: any) => {
|
request.onerror = (event: any) => {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import useIndexedDB from '@/hooks/IndexDBHook';
|
||||||
import { audioService } from '@/services/audioService';
|
import { audioService } from '@/services/audioService';
|
||||||
import store from '@/store';
|
import store from '@/store';
|
||||||
import type { ILyricText, SongResult } from '@/type/music';
|
import type { Artist, ILyricText, SongResult } from '@/type/music';
|
||||||
import { isElectron } from '@/utils';
|
import { isElectron } from '@/utils';
|
||||||
import { getTextColors } from '@/utils/linearColor';
|
import { getTextColors } from '@/utils/linearColor';
|
||||||
|
|
||||||
@@ -19,6 +20,15 @@ export const playMusic = computed(() => store.state.playMusic as SongResult); //
|
|||||||
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
export const sound = ref<Howl | null>(audioService.getCurrentSound());
|
||||||
export const isLyricWindowOpen = ref(false); // 新增状态
|
export const isLyricWindowOpen = ref(false); // 新增状态
|
||||||
export const textColors = ref<any>(getTextColors());
|
export const textColors = ref<any>(getTextColors());
|
||||||
|
export const artistList = computed(
|
||||||
|
() => (store.state.playMusic.ar || store.state.playMusic?.song?.artists) as Artist[]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const musicDB = await useIndexedDB('musicDB', [
|
||||||
|
{ name: 'music', keyPath: 'id' },
|
||||||
|
{ name: 'music_lyric', keyPath: 'id' },
|
||||||
|
{ name: 'api_cache', keyPath: 'id' }
|
||||||
|
]);
|
||||||
|
|
||||||
document.onkeyup = (e) => {
|
document.onkeyup = (e) => {
|
||||||
// 检查事件目标是否是输入框元素
|
// 检查事件目标是否是输入框元素
|
||||||
@@ -43,10 +53,18 @@ document.onkeyup = (e) => {
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => store.state.playMusicUrl,
|
() => store.state.playMusicUrl,
|
||||||
(newVal) => {
|
async (newVal) => {
|
||||||
if (newVal && playMusic.value) {
|
if (newVal && playMusic.value) {
|
||||||
sound.value = audioService.play(newVal, playMusic.value);
|
try {
|
||||||
setupAudioListeners();
|
const newSound = await audioService.play(newVal, playMusic.value);
|
||||||
|
sound.value = newSound as Howl;
|
||||||
|
setupAudioListeners();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('播放音频失败:', error);
|
||||||
|
store.commit('setPlayMusic', false);
|
||||||
|
// 下一首
|
||||||
|
store.commit('nextPlay');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -72,22 +90,52 @@ watch(
|
|||||||
const setupAudioListeners = () => {
|
const setupAudioListeners = () => {
|
||||||
let interval: any = null;
|
let interval: any = null;
|
||||||
|
|
||||||
|
const clearInterval = () => {
|
||||||
|
if (interval) {
|
||||||
|
window.clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理所有事件监听器
|
||||||
|
audioService.clearAllListeners();
|
||||||
|
|
||||||
// 监听播放
|
// 监听播放
|
||||||
audioService.on('play', () => {
|
audioService.on('play', () => {
|
||||||
store.commit('setPlayMusic', true);
|
store.commit('setPlayMusic', true);
|
||||||
interval = setInterval(() => {
|
clearInterval();
|
||||||
nowTime.value = sound.value?.seek() as number;
|
interval = window.setInterval(() => {
|
||||||
allTime.value = sound.value?.duration() as number;
|
try {
|
||||||
const newIndex = getLrcIndex(nowTime.value);
|
const currentSound = sound.value;
|
||||||
if (newIndex !== nowIndex.value) {
|
if (!currentSound || typeof currentSound.seek !== 'function') {
|
||||||
nowIndex.value = newIndex;
|
console.error('Invalid sound object or seek function');
|
||||||
currentLrcProgress.value = 0;
|
clearInterval();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = currentSound.seek() as number;
|
||||||
|
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
||||||
|
console.error('Invalid current time:', currentTime);
|
||||||
|
clearInterval();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nowTime.value = currentTime;
|
||||||
|
allTime.value = currentSound.duration() as number;
|
||||||
|
const newIndex = getLrcIndex(nowTime.value);
|
||||||
|
if (newIndex !== nowIndex.value) {
|
||||||
|
nowIndex.value = newIndex;
|
||||||
|
currentLrcProgress.value = 0;
|
||||||
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
|
sendLyricToWin();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
sendLyricToWin();
|
sendLyricToWin();
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
console.error('Error in interval:', error);
|
||||||
sendLyricToWin();
|
clearInterval();
|
||||||
}
|
}
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
@@ -95,7 +143,7 @@ const setupAudioListeners = () => {
|
|||||||
// 监听暂停
|
// 监听暂停
|
||||||
audioService.on('pause', () => {
|
audioService.on('pause', () => {
|
||||||
store.commit('setPlayMusic', false);
|
store.commit('setPlayMusic', false);
|
||||||
clearInterval(interval);
|
clearInterval();
|
||||||
if (isElectron && isLyricWindowOpen.value) {
|
if (isElectron && isLyricWindowOpen.value) {
|
||||||
sendLyricToWin();
|
sendLyricToWin();
|
||||||
}
|
}
|
||||||
@@ -103,14 +151,28 @@ const setupAudioListeners = () => {
|
|||||||
|
|
||||||
// 监听结束
|
// 监听结束
|
||||||
audioService.on('end', () => {
|
audioService.on('end', () => {
|
||||||
|
clearInterval();
|
||||||
|
|
||||||
if (store.state.playMode === 1) {
|
if (store.state.playMode === 1) {
|
||||||
// 单曲循环模式
|
// 单曲循环模式
|
||||||
sound.value?.play();
|
if (sound.value) {
|
||||||
|
sound.value.seek(0);
|
||||||
|
try {
|
||||||
|
sound.value.play();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error replaying song:', error);
|
||||||
|
store.commit('nextPlay');
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (store.state.playMode === 2) {
|
} else if (store.state.playMode === 2) {
|
||||||
// 随机播放模式
|
// 随机播放模式
|
||||||
const { playList } = store.state;
|
const { playList } = store.state;
|
||||||
if (playList.length <= 1) {
|
if (playList.length <= 1) {
|
||||||
sound.value?.play();
|
try {
|
||||||
|
sound.value?.play();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error replaying song:', error);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let randomIndex;
|
let randomIndex;
|
||||||
do {
|
do {
|
||||||
@@ -125,14 +187,7 @@ const setupAudioListeners = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听上一曲/下一曲控制
|
return clearInterval;
|
||||||
audioService.on('previoustrack', () => {
|
|
||||||
store.commit('prevPlay');
|
|
||||||
});
|
|
||||||
|
|
||||||
audioService.on('nexttrack', () => {
|
|
||||||
store.commit('nextPlay');
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const play = () => {
|
export const play = () => {
|
||||||
@@ -197,20 +252,47 @@ export const useLyricProgress = () => {
|
|||||||
let animationFrameId: number | null = null;
|
let animationFrameId: number | null = null;
|
||||||
|
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
if (!isPlaying.value) return;
|
if (!isPlaying.value) {
|
||||||
|
stopProgressAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const currentSound = sound.value;
|
const currentSound = sound.value;
|
||||||
if (!currentSound) return;
|
if (!currentSound || typeof currentSound.seek !== 'function') {
|
||||||
|
console.error('Invalid sound object or seek function');
|
||||||
|
stopProgressAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { start, end } = currentLrcTiming.value;
|
try {
|
||||||
const duration = end - start;
|
const { start, end } = currentLrcTiming.value;
|
||||||
const elapsed = (currentSound.seek() as number) - start;
|
if (typeof start !== 'number' || typeof end !== 'number' || start === end) {
|
||||||
currentLrcProgress.value = Math.min(Math.max((elapsed / duration) * 100, 0), 100);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = currentSound.seek() as number;
|
||||||
|
if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {
|
||||||
|
console.error('Invalid current time:', currentTime);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = currentTime - start;
|
||||||
|
const duration = end - start;
|
||||||
|
const progress = (elapsed / duration) * 100;
|
||||||
|
|
||||||
|
// 确保进度在 0-100 之间
|
||||||
|
currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating progress:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续下一帧更新
|
||||||
animationFrameId = requestAnimationFrame(updateProgress);
|
animationFrameId = requestAnimationFrame(updateProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startProgressAnimation = () => {
|
const startProgressAnimation = () => {
|
||||||
if (!animationFrameId && isPlaying.value) {
|
stopProgressAnimation(); // 先停止之前的动画
|
||||||
|
if (isPlaying.value) {
|
||||||
updateProgress();
|
updateProgress();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -222,8 +304,30 @@ export const useLyricProgress = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(isPlaying, (newIsPlaying) => {
|
// 监听播放状态变化
|
||||||
if (newIsPlaying) {
|
watch(
|
||||||
|
isPlaying,
|
||||||
|
(newIsPlaying) => {
|
||||||
|
if (newIsPlaying) {
|
||||||
|
startProgressAnimation();
|
||||||
|
} else {
|
||||||
|
stopProgressAnimation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听当前歌词索引变化
|
||||||
|
watch(nowIndex, () => {
|
||||||
|
currentLrcProgress.value = 0;
|
||||||
|
if (isPlaying.value) {
|
||||||
|
startProgressAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听音频对象变化
|
||||||
|
watch(sound, (newSound) => {
|
||||||
|
if (newSound && isPlaying.value) {
|
||||||
startProgressAnimation();
|
startProgressAnimation();
|
||||||
} else {
|
} else {
|
||||||
stopProgressAnimation();
|
stopProgressAnimation();
|
||||||
@@ -362,12 +466,13 @@ if (isElectron) {
|
|||||||
|
|
||||||
// 在组件挂载时设置监听器
|
// 在组件挂载时设置监听器
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isPlaying.value) {
|
const clearIntervalFn = setupAudioListeners();
|
||||||
useLyricProgress();
|
useLyricProgress(); // 直接调用,不需要解构返回值
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 在组件卸载时清理
|
// 在组件卸载时清理
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
audioService.stop();
|
clearIntervalFn();
|
||||||
|
audioService.stop();
|
||||||
|
audioService.clearAllListeners();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Howl } from 'howler';
|
import { Howl } from 'howler';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
|
||||||
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
import { useMusicHistory } from '@/hooks/MusicHistoryHook';
|
||||||
@@ -63,43 +64,91 @@ export const useMusicListHook = () => {
|
|||||||
fetchSongs(state, playListIndex + 1, playListIndex + 6);
|
fetchSongs(state, playListIndex + 1, playListIndex + 6);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preloadingSounds = ref<Howl[]>([]);
|
||||||
|
|
||||||
// 用于预加载下一首歌曲的 MP3 数据
|
// 用于预加载下一首歌曲的 MP3 数据
|
||||||
const preloadNextSong = (nextSongUrl: string) => {
|
const preloadNextSong = (nextSongUrl: string) => {
|
||||||
const sound = new Howl({
|
try {
|
||||||
src: [nextSongUrl],
|
// 限制同时预加载的数量
|
||||||
html5: true,
|
if (preloadingSounds.value.length >= 2) {
|
||||||
preload: true,
|
const oldestSound = preloadingSounds.value.shift();
|
||||||
autoplay: false
|
if (oldestSound) {
|
||||||
});
|
oldestSound.unload();
|
||||||
return sound;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sound = new Howl({
|
||||||
|
src: [nextSongUrl],
|
||||||
|
html5: true,
|
||||||
|
preload: true,
|
||||||
|
autoplay: false
|
||||||
|
});
|
||||||
|
|
||||||
|
preloadingSounds.value.push(sound);
|
||||||
|
|
||||||
|
// 添加加载错误处理
|
||||||
|
sound.on('loaderror', () => {
|
||||||
|
console.error('预加载音频失败:', nextSongUrl);
|
||||||
|
const index = preloadingSounds.value.indexOf(sound);
|
||||||
|
if (index > -1) {
|
||||||
|
preloadingSounds.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
sound.unload();
|
||||||
|
});
|
||||||
|
|
||||||
|
return sound;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('预加载音频出错:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
|
const fetchSongs = async (state: any, startIndex: number, endIndex: number) => {
|
||||||
const songs = state.playList.slice(
|
try {
|
||||||
Math.max(0, startIndex),
|
const songs = state.playList.slice(
|
||||||
Math.min(endIndex, state.playList.length)
|
Math.max(0, startIndex),
|
||||||
);
|
Math.min(endIndex, state.playList.length)
|
||||||
|
);
|
||||||
|
|
||||||
const detailedSongs = await Promise.all(
|
const detailedSongs = await Promise.all(
|
||||||
songs.map(async (song: SongResult) => {
|
songs.map(async (song: SongResult) => {
|
||||||
// 如果歌曲详情已经存在,就不重复请求
|
try {
|
||||||
if (!song.playMusicUrl) {
|
// 如果歌曲详情已经存在,就不重复请求
|
||||||
return await getSongDetail(song);
|
if (!song.playMusicUrl) {
|
||||||
|
return await getSongDetail(song);
|
||||||
|
}
|
||||||
|
return song;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取歌曲详情失败:', error);
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加载下一首的歌词
|
||||||
|
const nextSong = detailedSongs[0];
|
||||||
|
if (nextSong && !(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
|
||||||
|
try {
|
||||||
|
nextSong.lyric = await loadLrc(nextSong.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载歌词失败:', error);
|
||||||
}
|
}
|
||||||
return song;
|
}
|
||||||
})
|
|
||||||
);
|
|
||||||
// 加载下一首的歌词
|
|
||||||
const nextSong = detailedSongs[0];
|
|
||||||
if (!(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {
|
|
||||||
nextSong.lyric = await loadLrc(nextSong.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新播放列表中的歌曲详情
|
// 更新播放列表中的歌曲详情
|
||||||
detailedSongs.forEach((song, index) => {
|
detailedSongs.forEach((song, index) => {
|
||||||
state.playList[startIndex + index] = song;
|
if (song && startIndex + index < state.playList.length) {
|
||||||
});
|
state.playList[startIndex + index] = song;
|
||||||
preloadNextSong(nextSong.playMusicUrl);
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 只预加载下一首歌曲
|
||||||
|
if (nextSong && nextSong.playMusicUrl) {
|
||||||
|
preloadNextSong(nextSong.playMusicUrl);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取歌曲列表失败:', error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextPlay = async (state: any) => {
|
const nextPlay = async (state: any) => {
|
||||||
@@ -153,7 +202,7 @@ export const useMusicListHook = () => {
|
|||||||
const { lyrics, times } = parseLyrics(data.lrc.lyric);
|
const { lyrics, times } = parseLyrics(data.lrc.lyric);
|
||||||
const tlyric: Record<string, string> = {};
|
const tlyric: Record<string, string> = {};
|
||||||
|
|
||||||
if (data.tlyric.lyric) {
|
if (data.tlyric && data.tlyric.lyric) {
|
||||||
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
|
const { lyrics: tLyrics, times: tTimes } = parseLyrics(data.tlyric.lyric);
|
||||||
tLyrics.forEach((lyric, index) => {
|
tLyrics.forEach((lyric, index) => {
|
||||||
tlyric[tTimes[index].toString()] = lyric.text;
|
tlyric[tTimes[index].toString()] = lyric.text;
|
||||||
@@ -193,6 +242,12 @@ export const useMusicListHook = () => {
|
|||||||
audioService.getCurrentSound()?.pause();
|
audioService.getCurrentSound()?.pause();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 在组件卸载时清理预加载的音频
|
||||||
|
onUnmounted(() => {
|
||||||
|
preloadingSounds.value.forEach((sound) => sound.unload());
|
||||||
|
preloadingSounds.value = [];
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handlePlayMusic,
|
handlePlayMusic,
|
||||||
nextPlay,
|
nextPlay,
|
||||||
|
|||||||
@@ -11,7 +11,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.n-slider-handle-indicator--top {
|
.n-slider-handle-indicator--top {
|
||||||
@apply bg-transparent dark:text-[#ffffffdd] text-[#000000dd] text-2xl px-2 py-1 shadow-none mb-0 !important;
|
@apply bg-transparent text-2xl px-2 py-1 shadow-none mb-0 dark:text-[#ffffffdd] text-[#000000dd] !important;
|
||||||
|
mix-blend-mode: difference !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-binder-follower-container:has(.n-slider-handle-indicator--top) {
|
||||||
|
z-index: 999999999 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-el {
|
.text-el {
|
||||||
@@ -56,3 +61,11 @@
|
|||||||
--text-color-300: #3d3d3d;
|
--text-color-300: #3d3d3d;
|
||||||
--primary-color: #22c55e;
|
--primary-color: #22c55e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--text-color: #000000dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[class='dark'] {
|
||||||
|
--text-color: #ffffffdd;
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
|
<meta name="apple-mobile-web-app-title" content="网抑云音乐" />
|
||||||
<!-- 资源预加载 -->
|
<!-- 资源预加载 -->
|
||||||
<link rel="preload" href="./assets/icon/iconfont.css" as="style" />
|
<link rel="preload" href="./assets/icon/iconfont.css" as="style" />
|
||||||
<link rel="preload" href="./assets/css/animate.css" as="style" />
|
|
||||||
<link rel="preload" href="./assets/css/base.css" as="style" />
|
<link rel="preload" href="./assets/css/base.css" as="style" />
|
||||||
|
|
||||||
<!-- 样式表 -->
|
<!-- 样式表 -->
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
placement="bottom"
|
placement="bottom"
|
||||||
:style="{ background: currentBackground || background }"
|
:style="{ background: currentBackground || background }"
|
||||||
:to="`#layout-main`"
|
:to="`#layout-main`"
|
||||||
|
:z-index="9998"
|
||||||
>
|
>
|
||||||
<div id="drawer-target">
|
<div id="drawer-target">
|
||||||
<div class="drawer-back"></div>
|
<div class="drawer-back"></div>
|
||||||
@@ -31,13 +32,13 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="(item, index) in playMusic.ar || playMusic.song.artists"
|
v-for="(item, index) in artistList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="cursor-pointer hover:text-green-500"
|
class="cursor-pointer hover:text-green-500"
|
||||||
@click="handleArtistClick(item.id)"
|
@click="handleArtistClick(item.id)"
|
||||||
>
|
>
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
{{ index < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
|
{{ index < artistList.length - 1 ? ' / ' : '' }}
|
||||||
</span>
|
</span>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
@@ -87,6 +88,7 @@ import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
artistList,
|
||||||
lrcArray,
|
lrcArray,
|
||||||
nowIndex,
|
nowIndex,
|
||||||
playMusic,
|
playMusic,
|
||||||
@@ -124,14 +126,18 @@ const isVisible = computed({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 歌词滚动方法
|
// 歌词滚动方法
|
||||||
const lrcScroll = (behavior = 'smooth') => {
|
const lrcScroll = (behavior = 'smooth', top: null | number = null) => {
|
||||||
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
|
const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`);
|
||||||
if (isVisible.value && !isMouse.value && nowEl && lrcContainer.value) {
|
if (isVisible.value && !isMouse.value && nowEl && lrcContainer.value) {
|
||||||
const containerRect = lrcContainer.value.getBoundingClientRect();
|
if (top !== null) {
|
||||||
const nowElRect = nowEl.getBoundingClientRect();
|
lrcSider.value.scrollTo({ top, behavior });
|
||||||
const relativeTop = nowElRect.top - containerRect.top;
|
} else {
|
||||||
const scrollTop = relativeTop - lrcSider.value.$el.getBoundingClientRect().height / 2;
|
const containerRect = lrcContainer.value.getBoundingClientRect();
|
||||||
lrcSider.value.scrollTo({ top: scrollTop, behavior });
|
const nowElRect = nowEl.getBoundingClientRect();
|
||||||
|
const relativeTop = nowElRect.top - containerRect.top;
|
||||||
|
const scrollTop = relativeTop - lrcSider.value.$el.getBoundingClientRect().height / 2;
|
||||||
|
lrcSider.value.scrollTo({ top: scrollTop, behavior });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 展开全屏 -->
|
|
||||||
<!-- 底部播放栏 -->
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="music-play-bar"
|
class="music-play-bar"
|
||||||
:class="
|
:class="
|
||||||
@@ -60,13 +57,12 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="(artists, artistsindex) in playMusic.ar || playMusic.song.artists"
|
v-for="(artists, artistsindex) in artistList"
|
||||||
:key="artistsindex"
|
:key="artistsindex"
|
||||||
class="cursor-pointer hover:text-green-500"
|
class="cursor-pointer hover:text-green-500"
|
||||||
@click="handleArtistClick(artists.id)"
|
@click="handleArtistClick(artists.id)"
|
||||||
>
|
>
|
||||||
{{ artists.name
|
{{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}
|
||||||
}}{{ artistsindex < (playMusic.ar || playMusic.song.artists).length - 1 ? ' / ' : '' }}
|
|
||||||
</span>
|
</span>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,9 +156,11 @@ import { useStore } from 'vuex';
|
|||||||
import SongItem from '@/components/common/SongItem.vue';
|
import SongItem from '@/components/common/SongItem.vue';
|
||||||
import {
|
import {
|
||||||
allTime,
|
allTime,
|
||||||
|
artistList,
|
||||||
isLyricWindowOpen,
|
isLyricWindowOpen,
|
||||||
nowTime,
|
nowTime,
|
||||||
openLyric,
|
openLyric,
|
||||||
|
playMusic,
|
||||||
sound,
|
sound,
|
||||||
textColors
|
textColors
|
||||||
} from '@/hooks/MusicHook';
|
} from '@/hooks/MusicHook';
|
||||||
@@ -174,12 +172,11 @@ import MusicFull from './MusicFull.vue';
|
|||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
// 播放的音乐信息
|
|
||||||
const playMusic = computed(() => store.state.playMusic as SongResult);
|
|
||||||
// 是否播放
|
// 是否播放
|
||||||
const play = computed(() => store.state.play as boolean);
|
const play = computed(() => store.state.play as boolean);
|
||||||
|
// 播放列表
|
||||||
const playList = computed(() => store.state.playList as SongResult[]);
|
const playList = computed(() => store.state.playList as SongResult[]);
|
||||||
|
// 背景颜色
|
||||||
const background = ref('#000');
|
const background = ref('#000');
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -190,7 +187,7 @@ watch(
|
|||||||
{ immediate: true, deep: true }
|
{ immediate: true, deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// 使用 useThrottleFn 创建节流版本的 seek 函数
|
// 节流版本的 seek 函数
|
||||||
const throttledSeek = useThrottleFn((value: number) => {
|
const throttledSeek = useThrottleFn((value: number) => {
|
||||||
if (!sound.value) return;
|
if (!sound.value) return;
|
||||||
sound.value.seek(value);
|
sound.value.seek(value);
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
<div id="title-bar" @mousedown="drag">
|
<div id="title-bar" @mousedown="drag">
|
||||||
<div id="title">Alger Music</div>
|
<div id="title">Alger Music</div>
|
||||||
<div id="buttons">
|
<div id="buttons">
|
||||||
<button @click="minimize">
|
<div class="button" @click="minimize">
|
||||||
<i class="iconfont icon-minisize"></i>
|
<i class="iconfont icon-minisize"></i>
|
||||||
</button>
|
</div>
|
||||||
<button @click="close">
|
<div class="button" @click="close">
|
||||||
<i class="iconfont icon-close"></i>
|
<i class="iconfont icon-close"></i>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ const drag = (event: MouseEvent) => {
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.button {
|
||||||
@apply text-gray-600 dark:text-gray-400 hover:text-green-500;
|
@apply text-gray-600 dark:text-gray-400 hover:text-green-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,52 +121,79 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 播放控制相关
|
// 播放控制相关
|
||||||
play(url: string, track: SongResult) {
|
play(url: string, track: SongResult): Promise<Howl> {
|
||||||
// Howler.unload();
|
return new Promise((resolve, reject) => {
|
||||||
if (this.currentSound) {
|
let retryCount = 0;
|
||||||
this.currentSound.unload();
|
const maxRetries = 3;
|
||||||
}
|
|
||||||
this.currentSound = null;
|
|
||||||
this.currentTrack = track;
|
|
||||||
|
|
||||||
this.currentSound = new Howl({
|
const tryPlay = () => {
|
||||||
src: [url],
|
if (this.currentSound) {
|
||||||
html5: true,
|
this.currentSound.unload();
|
||||||
autoplay: true,
|
}
|
||||||
volume: localStorage.getItem('volume')
|
this.currentSound = null;
|
||||||
? parseFloat(localStorage.getItem('volume') as string)
|
this.currentTrack = track;
|
||||||
: 1
|
|
||||||
|
this.currentSound = new Howl({
|
||||||
|
src: [url],
|
||||||
|
html5: true,
|
||||||
|
autoplay: true,
|
||||||
|
volume: localStorage.getItem('volume')
|
||||||
|
? parseFloat(localStorage.getItem('volume') as string)
|
||||||
|
: 1,
|
||||||
|
onloaderror: () => {
|
||||||
|
console.error('Audio load error');
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
retryCount++;
|
||||||
|
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
|
||||||
|
setTimeout(tryPlay, 1000 * retryCount);
|
||||||
|
} else {
|
||||||
|
reject(new Error('音频加载失败,请尝试切换其他歌曲'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onplayerror: () => {
|
||||||
|
console.error('Audio play error');
|
||||||
|
if (retryCount < maxRetries) {
|
||||||
|
retryCount++;
|
||||||
|
console.log(`Retrying playback (${retryCount}/${maxRetries})...`);
|
||||||
|
setTimeout(tryPlay, 1000 * retryCount);
|
||||||
|
} else {
|
||||||
|
reject(new Error('音频播放失败,请尝试切换其他歌曲'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新媒体会话元数据
|
||||||
|
this.updateMediaSessionMetadata(track);
|
||||||
|
|
||||||
|
// 设置音频事件监听
|
||||||
|
this.currentSound.on('play', () => {
|
||||||
|
this.updateMediaSessionState(true);
|
||||||
|
this.emit('play');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSound.on('pause', () => {
|
||||||
|
this.updateMediaSessionState(false);
|
||||||
|
this.emit('pause');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSound.on('end', () => {
|
||||||
|
this.emit('end');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSound.on('seek', () => {
|
||||||
|
this.updateMediaSessionPositionState();
|
||||||
|
this.emit('seek');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.currentSound.on('load', () => {
|
||||||
|
this.updateMediaSessionPositionState();
|
||||||
|
this.emit('load');
|
||||||
|
resolve(this.currentSound as Howl);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
tryPlay();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 更新媒体会话元数据
|
|
||||||
this.updateMediaSessionMetadata(track);
|
|
||||||
|
|
||||||
// 设置音频事件监听
|
|
||||||
this.currentSound.on('play', () => {
|
|
||||||
this.updateMediaSessionState(true);
|
|
||||||
this.emit('play');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentSound.on('pause', () => {
|
|
||||||
this.updateMediaSessionState(false);
|
|
||||||
this.emit('pause');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentSound.on('end', () => {
|
|
||||||
this.emit('end');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentSound.on('seek', () => {
|
|
||||||
this.updateMediaSessionPositionState();
|
|
||||||
this.emit('seek');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentSound.on('load', () => {
|
|
||||||
this.updateMediaSessionPositionState();
|
|
||||||
this.emit('load');
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.currentSound;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentSound() {
|
getCurrentSound() {
|
||||||
@@ -202,6 +229,10 @@ class AudioService {
|
|||||||
this.updateMediaSessionPositionState();
|
this.updateMediaSessionPositionState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearAllListeners() {
|
||||||
|
this.callbacks = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const audioService = new AudioService();
|
export const audioService = new AudioService();
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ interface Album {
|
|||||||
picId_str: string;
|
picId_str: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Artist {
|
export interface Artist {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
id: number;
|
||||||
picId: number;
|
picId: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user