mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-14 14:50:50 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7891bf45fd | ||
|
|
2f339b1373 | ||
|
|
749a2a69c4 | ||
|
|
5b97010b32 | ||
|
|
694dff425b | ||
|
|
e8cf253567 | ||
|
|
4d831777f1 | ||
|
|
95c255d2ba | ||
|
|
d739a6701b |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -1,15 +1,27 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v4.8.2
|
||||||
|
### 🎨 优化
|
||||||
|
- 重新设计pc端歌词页面Mini播放栏
|
||||||
|
- 添加清除歌曲自定义解析功能
|
||||||
|
|
||||||
|
### 🐛 Bug 修复
|
||||||
|
- 修复歌曲单独解析失败问题
|
||||||
|
- 修复歌词页面加入歌单抽屉被遮挡问题
|
||||||
|
|
||||||
|
|
||||||
|
## v4.8.1
|
||||||
|
|
||||||
|
### 🐛 Bug 修复
|
||||||
|
- 修复无法快捷键调整问题
|
||||||
|
|
||||||
|
### 🎨 优化
|
||||||
|
- 优化音乐资源解析
|
||||||
|
- 去除无用代码,优化加载速度
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## v4.8.0
|
## v4.8.0
|
||||||
> 如果更新遇到问题,请前往 <a href="http://donate.alger.fun/download" target="_blank">下载 AlgerMusicPlayer</a>
|
|
||||||
|
|
||||||
> 请我喝咖啡(支持作者) ☕️ <a href="http://donate.alger.fun/donate" target="_blank" style="color: red; font-weight: bold;">赏你</a>
|
|
||||||
|
|
||||||
> 帮我点个 star <a href="https://github.com/algerkong/AlgerMusicPlayer" target="_blank">github star</a>
|
|
||||||
|
|
||||||
> 微信公众号 微信搜索 <span style="font-weight: bold;">AlgerMusic</span>
|
|
||||||
|
|
||||||
> QQ频道 AlgerMusic <a href="https://pd.qq.com/s/cs056n33q?b=5" target="_blank">加入频道</a>
|
|
||||||
|
|
||||||
### ✨ 新功能
|
### ✨ 新功能
|
||||||
- 增强移动端播放页面效果,添加播放模式选择,添加横屏模式,添加播放列表功能 ([81b61e4](https://github.com/algerkong/AlgerMusicPlayer/commit/81b61e4)),([0d89e15](https://github.com/algerkong/AlgerMusicPlayer/commit/0d89e15)),([9345805](https://github.com/algerkong/AlgerMusicPlayer/commit/9345805))
|
- 增强移动端播放页面效果,添加播放模式选择,添加横屏模式,添加播放列表功能 ([81b61e4](https://github.com/algerkong/AlgerMusicPlayer/commit/81b61e4)),([0d89e15](https://github.com/algerkong/AlgerMusicPlayer/commit/0d89e15)),([9345805](https://github.com/algerkong/AlgerMusicPlayer/commit/9345805))
|
||||||
|
|||||||
@@ -44,12 +44,11 @@
|
|||||||
|
|
||||||
- 🎼 音乐功能
|
- 🎼 音乐功能
|
||||||
- 支持歌单、MV、专辑等完整音乐服务
|
- 支持歌单、MV、专辑等完整音乐服务
|
||||||
- 灰色音乐资源解析(基于 @unblockneteasemusic/server)
|
- 音乐资源解析(基于 @unblockneteasemusic/server)
|
||||||
- 音乐单独解析
|
|
||||||
- EQ均衡器
|
- EQ均衡器
|
||||||
- 定时播放 远程控制播放 倍速播放
|
- 定时播放 远程控制播放 倍速播放
|
||||||
- 高品质音乐
|
- 高品质音乐
|
||||||
- 音乐文件下载(支持右键下载和批量下载, 附带歌词封面等信息)
|
- 音乐文件下载
|
||||||
- 搜索 MV 音乐 专辑 歌单 bilibili
|
- 搜索 MV 音乐 专辑 歌单 bilibili
|
||||||
- 音乐单独选择音源解析
|
- 音乐单独选择音源解析
|
||||||
- 🚀 技术特性
|
- 🚀 技术特性
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "AlgerMusicPlayer",
|
"name": "AlgerMusicPlayer",
|
||||||
"version": "4.8.0",
|
"version": "4.8.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",
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export default {
|
|||||||
failed: 'Reparse failed',
|
failed: 'Reparse failed',
|
||||||
warning: 'Please select a music source',
|
warning: 'Please select a music source',
|
||||||
bilibiliNotSupported: 'Bilibili videos do not support reparsing',
|
bilibiliNotSupported: 'Bilibili videos do not support reparsing',
|
||||||
processing: 'Processing...'
|
processing: 'Processing...',
|
||||||
|
clear: 'Clear Custom Source'
|
||||||
},
|
},
|
||||||
playBar: {
|
playBar: {
|
||||||
expand: 'Expand Lyrics',
|
expand: 'Expand Lyrics',
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ export default {
|
|||||||
failed: '重新解析失败',
|
failed: '重新解析失败',
|
||||||
warning: '请选择一个音源',
|
warning: '请选择一个音源',
|
||||||
bilibiliNotSupported: 'B站视频不支持重新解析',
|
bilibiliNotSupported: 'B站视频不支持重新解析',
|
||||||
processing: '解析中...'
|
processing: '解析中...',
|
||||||
|
clear: '清除自定义音源'
|
||||||
},
|
},
|
||||||
playBar: {
|
playBar: {
|
||||||
expand: '展开歌词',
|
expand: '展开歌词',
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { initializeFileManager } from './modules/fileManager';
|
|||||||
import { initializeFonts } from './modules/fonts';
|
import { initializeFonts } from './modules/fonts';
|
||||||
import { initializeRemoteControl } from './modules/remoteControl';
|
import { initializeRemoteControl } from './modules/remoteControl';
|
||||||
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
import { initializeShortcuts, registerShortcuts } from './modules/shortcuts';
|
||||||
import { initializeStats, setupStatsHandlers } from './modules/statsService';
|
|
||||||
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';
|
||||||
import { setupUpdateHandlers } from './modules/update';
|
import { setupUpdateHandlers } from './modules/update';
|
||||||
import { createMainWindow, initializeWindowManager } from './modules/window';
|
import { createMainWindow, initializeWindowManager } from './modules/window';
|
||||||
@@ -51,12 +50,6 @@ function initialize() {
|
|||||||
// 初始化托盘
|
// 初始化托盘
|
||||||
initializeTray(iconPath, mainWindow);
|
initializeTray(iconPath, mainWindow);
|
||||||
|
|
||||||
// 初始化统计服务
|
|
||||||
initializeStats();
|
|
||||||
|
|
||||||
// 设置统计相关的IPC处理程序
|
|
||||||
setupStatsHandlers(ipcMain);
|
|
||||||
|
|
||||||
// 启动音乐API
|
// 启动音乐API
|
||||||
startMusicApi();
|
startMusicApi();
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { app } from 'electron';
|
|
||||||
import Store from 'electron-store';
|
|
||||||
|
|
||||||
import { getDeviceId, getSystemInfo } from './deviceInfo';
|
|
||||||
|
|
||||||
const store = new Store();
|
|
||||||
|
|
||||||
// 统计服务配置
|
|
||||||
const STATS_API_URL = 'http://donate.alger.fun/state/api/stats';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录应用安装/启动
|
|
||||||
*/
|
|
||||||
export async function recordInstallation(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const deviceId = getDeviceId();
|
|
||||||
const systemInfo = getSystemInfo();
|
|
||||||
|
|
||||||
// 发送请求到统计服务器
|
|
||||||
await axios.post(`${STATS_API_URL}/installation`, {
|
|
||||||
deviceId,
|
|
||||||
osType: systemInfo.osType,
|
|
||||||
osVersion: systemInfo.osVersion,
|
|
||||||
appVersion: systemInfo.appVersion
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('应用启动统计已记录');
|
|
||||||
|
|
||||||
// 记录最后一次启动时间
|
|
||||||
store.set('lastStartTime', new Date().toISOString());
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录应用启动统计失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置 IPC 处理程序以接收渲染进程的统计请求
|
|
||||||
* @param ipcMain Electron IPC主对象
|
|
||||||
*/
|
|
||||||
export function setupStatsHandlers(ipcMain: Electron.IpcMain): void {
|
|
||||||
// 处理页面访问统计
|
|
||||||
ipcMain.handle('record-visit', async (_, page: string, userId?: string) => {
|
|
||||||
try {
|
|
||||||
const deviceId = getDeviceId();
|
|
||||||
|
|
||||||
await axios.post(`${STATS_API_URL}/visit`, {
|
|
||||||
deviceId,
|
|
||||||
userId,
|
|
||||||
page
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录页面访问统计失败:', error);
|
|
||||||
return { success: false, error: (error as Error).message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 处理播放统计
|
|
||||||
ipcMain.handle(
|
|
||||||
'record-play',
|
|
||||||
async (
|
|
||||||
_,
|
|
||||||
songData: {
|
|
||||||
userId: string | null;
|
|
||||||
songId: string | number;
|
|
||||||
songName: string;
|
|
||||||
artistName: string;
|
|
||||||
duration?: number;
|
|
||||||
completedPlay?: boolean;
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const { songId, songName, artistName, duration = 0, completedPlay = false } = songData;
|
|
||||||
const deviceId = getDeviceId();
|
|
||||||
|
|
||||||
await axios.post(`${STATS_API_URL}/play`, {
|
|
||||||
deviceId,
|
|
||||||
userId: songData.userId,
|
|
||||||
songId: songId.toString(),
|
|
||||||
songName,
|
|
||||||
artistName,
|
|
||||||
duration,
|
|
||||||
completedPlay
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录播放统计失败:', error);
|
|
||||||
return { success: false, error: (error as Error).message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 处理获取统计摘要
|
|
||||||
ipcMain.handle('get-stats-summary', async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`${STATS_API_URL}/summary`);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取统计摘要失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 应用启动时初始化统计服务
|
|
||||||
*/
|
|
||||||
export function initializeStats(): void {
|
|
||||||
// 记录应用启动统计
|
|
||||||
recordInstallation().catch((error) => {
|
|
||||||
console.error('初始化统计服务失败:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 注册应用退出时的回调
|
|
||||||
app.on('will-quit', () => {
|
|
||||||
// 可以在这里添加应用退出时的统计逻辑
|
|
||||||
console.log('应用退出');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"alwaysShowDownloadButton": false,
|
"alwaysShowDownloadButton": false,
|
||||||
"unlimitedDownload": false,
|
"unlimitedDownload": false,
|
||||||
"enableMusicUnblock": true,
|
"enableMusicUnblock": true,
|
||||||
"enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili", "kuwo"],
|
"enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili"],
|
||||||
"showTopAction": false,
|
"showTopAction": false,
|
||||||
"contentZoomFactor": 1
|
"contentZoomFactor": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import match from '@unblockneteasemusic/server';
|
import match from '@unblockneteasemusic/server';
|
||||||
|
|
||||||
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili';
|
type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'bilibili';
|
||||||
|
|
||||||
interface SongData {
|
interface SongData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -30,7 +30,50 @@ interface UnblockResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 所有可用平台
|
// 所有可用平台
|
||||||
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili'];
|
export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保对象数据结构完整,处理null或undefined的情况
|
||||||
|
* @param data 需要处理的数据对象
|
||||||
|
*/
|
||||||
|
function ensureDataStructure(data: any): any {
|
||||||
|
// 如果数据本身为空,则返回一个基本结构
|
||||||
|
if (!data) {
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
artists: [],
|
||||||
|
album: { name: '' }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保name字段存在
|
||||||
|
if (data.name === undefined || data.name === null) {
|
||||||
|
data.name = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保artists字段存在且为数组
|
||||||
|
if (!data.artists || !Array.isArray(data.artists)) {
|
||||||
|
data.artists = data.ar && Array.isArray(data.ar) ? data.ar : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保artists中的每个元素都有name属性
|
||||||
|
if (data.artists.length > 0) {
|
||||||
|
data.artists = data.artists.map(artist => {
|
||||||
|
return artist ? { name: artist.name || '' } : { name: '' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保album对象存在并有name属性
|
||||||
|
if (!data.album || typeof data.album !== 'object') {
|
||||||
|
data.album = data.al && typeof data.al === 'object' ? data.al : { name: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.album.name) {
|
||||||
|
data.album.name = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 音乐解析函数
|
* 音乐解析函数
|
||||||
@@ -46,16 +89,18 @@ const unblockMusic = async (
|
|||||||
retryCount = 1,
|
retryCount = 1,
|
||||||
enabledPlatforms?: Platform[]
|
enabledPlatforms?: Platform[]
|
||||||
): Promise<UnblockResult> => {
|
): Promise<UnblockResult> => {
|
||||||
|
|
||||||
// 过滤 enabledPlatforms,确保只包含 ALL_PLATFORMS 中存在的平台
|
// 过滤 enabledPlatforms,确保只包含 ALL_PLATFORMS 中存在的平台
|
||||||
const filteredPlatforms = enabledPlatforms
|
const filteredPlatforms = enabledPlatforms
|
||||||
? enabledPlatforms.filter(platform => ALL_PLATFORMS.includes(platform))
|
? enabledPlatforms.filter(platform => ALL_PLATFORMS.includes(platform))
|
||||||
: ALL_PLATFORMS;
|
: ALL_PLATFORMS;
|
||||||
|
|
||||||
songData.album = songData.album || songData.al;
|
// 处理歌曲数据,确保数据结构完整
|
||||||
songData.artists = songData.artists || songData.ar;
|
const processedSongData = ensureDataStructure(songData);
|
||||||
|
|
||||||
const retry = async (attempt: number): Promise<UnblockResult> => {
|
const retry = async (attempt: number): Promise<UnblockResult> => {
|
||||||
try {
|
try {
|
||||||
const data = await match(parseInt(String(id), 10), filteredPlatforms, songData);
|
const data = await match(parseInt(String(id), 10), filteredPlatforms, processedSongData);
|
||||||
const result: UnblockResult = {
|
const result: UnblockResult = {
|
||||||
data: {
|
data: {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -47,11 +47,7 @@ const api = {
|
|||||||
'get-system-fonts',
|
'get-system-fonts',
|
||||||
'get-cached-lyric',
|
'get-cached-lyric',
|
||||||
'cache-lyric',
|
'cache-lyric',
|
||||||
'clear-lyric-cache',
|
'clear-lyric-cache'
|
||||||
// 统计相关
|
|
||||||
'record-visit',
|
|
||||||
'record-play',
|
|
||||||
'get-stats-summary'
|
|
||||||
];
|
];
|
||||||
if (validChannels.includes(channel)) {
|
if (validChannels.includes(channel)) {
|
||||||
return ipcRenderer.invoke(channel, ...args);
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
|
<traffic-warning-drawer v-if="!isElectron"></traffic-warning-drawer>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
@@ -17,6 +18,8 @@ import { computed, nextTick, onMounted, watch } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
import TrafficWarningDrawer from '@/components/TrafficWarningDrawer.vue';
|
||||||
|
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
import { useMenuStore } from '@/store/modules/menu';
|
import { useMenuStore } from '@/store/modules/menu';
|
||||||
import { usePlayerStore } from '@/store/modules/player';
|
import { usePlayerStore } from '@/store/modules/player';
|
||||||
|
|||||||
@@ -73,9 +73,8 @@ export const parseFromGDMusic = async (
|
|||||||
throw new Error('搜索查询过短');
|
throw new Error('搜索查询过短');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有可用的音乐源 netease、kuwo、joox、tidal
|
// 所有可用的音乐源 netease、joox、tidal
|
||||||
const allSources = [
|
const allSources = ['joox', 'tidal', 'netease'
|
||||||
'kuwo', 'joox', 'tidal', 'netease'
|
|
||||||
] as MusicSourceType[];
|
] as MusicSourceType[];
|
||||||
|
|
||||||
console.log('GD音乐台开始搜索:', searchQuery);
|
console.log('GD音乐台开始搜索:', searchQuery);
|
||||||
|
|||||||
@@ -82,55 +82,36 @@ export const getMusicLrc = async (id: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getParsingMusicUrl = async (id: number, data: SongResult) => {
|
/**
|
||||||
const settingStore = useSettingsStore();
|
* 从Bilibili获取音频URL
|
||||||
|
* @param data 歌曲数据
|
||||||
// 如果禁用了音乐解析功能,则直接返回空结果
|
* @returns 解析结果
|
||||||
if (!settingStore.setData.enableMusicUnblock) {
|
*/
|
||||||
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
|
const getBilibiliAudio = async (data: SongResult) => {
|
||||||
}
|
|
||||||
|
|
||||||
// 获取音源设置,优先使用歌曲自定义音源
|
|
||||||
const songId = String(id);
|
|
||||||
const savedSource = localStorage.getItem(`song_source_${songId}`);
|
|
||||||
let enabledSources: any[] = [];
|
|
||||||
|
|
||||||
// 如果有歌曲自定义音源,使用自定义音源
|
|
||||||
if (savedSource) {
|
|
||||||
try {
|
|
||||||
enabledSources = JSON.parse(savedSource);
|
|
||||||
console.log(`使用歌曲 ${id} 自定义音源:`, enabledSources);
|
|
||||||
if(enabledSources.includes('bilibili')){
|
|
||||||
// 构建搜索关键词,依次判断歌曲名称、歌手名称和专辑名称是否存在
|
|
||||||
const songName = data?.name || '';
|
const songName = data?.name || '';
|
||||||
const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';
|
const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';
|
||||||
const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';
|
const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';
|
||||||
const name = [songName, artistName, albumName].filter(Boolean).join(' ').trim();
|
|
||||||
console.log('开始搜索bilibili音频', name);
|
const searchQuery = [songName, artistName, albumName].filter(Boolean).join(' ').trim();
|
||||||
|
console.log('开始搜索bilibili音频:', searchQuery);
|
||||||
|
|
||||||
|
const url = await searchAndGetBilibiliAudioUrl(searchQuery);
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
code: 200,
|
code: 200,
|
||||||
message: 'success',
|
message: 'success',
|
||||||
data: {
|
data: { url }
|
||||||
url: await searchAndGetBilibiliAudioUrl(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('e',e)
|
|
||||||
console.error('解析自定义音源失败, 使用全局设置', e);
|
|
||||||
enabledSources = settingStore.setData.enabledMusicSources || [];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 没有自定义音源,使用全局音源设置
|
|
||||||
enabledSources = settingStore.setData.enabledMusicSources || [];
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// 检查是否选择了GD音乐台解析
|
/**
|
||||||
|
* 从GD音乐台获取音频URL
|
||||||
if (enabledSources.includes('gdmusic')) {
|
* @param id 歌曲ID
|
||||||
// 获取音质设置并转换为GD音乐台格式
|
* @param data 歌曲数据
|
||||||
|
* @returns 解析结果,失败时返回null
|
||||||
|
*/
|
||||||
|
const getGDMusicAudio = async (id: number, data: SongResult) => {
|
||||||
try {
|
try {
|
||||||
const gdResult = await parseFromGDMusic(id, data, '999');
|
const gdResult = await parseFromGDMusic(id, data, '999');
|
||||||
if (gdResult) {
|
if (gdResult) {
|
||||||
@@ -139,16 +120,81 @@ export const getParsingMusicUrl = async (id: number, data: SongResult) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('GD音乐台解析失败:', error);
|
console.error('GD音乐台解析失败:', error);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
console.log('GD音乐台所有音源均解析失败,尝试使用unblockMusic');
|
/**
|
||||||
}
|
* 使用unblockMusic解析音频URL
|
||||||
|
* @param id 歌曲ID
|
||||||
// 如果GD音乐台解析失败或者未启用,尝试使用unblockMusic
|
* @param data 歌曲数据
|
||||||
if (isElectron) {
|
* @param sources 音源列表
|
||||||
const filteredSources = enabledSources.filter(source => source !== 'gdmusic');
|
* @returns 解析结果
|
||||||
|
*/
|
||||||
|
const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => {
|
||||||
|
const filteredSources = sources.filter(source => source !== 'gdmusic');
|
||||||
|
console.log(`使用unblockMusic解析,音源:`, filteredSources);
|
||||||
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
|
return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取解析后的音乐URL
|
||||||
|
* @param id 歌曲ID
|
||||||
|
* @param data 歌曲数据
|
||||||
|
* @returns 解析结果
|
||||||
|
*/
|
||||||
|
export const getParsingMusicUrl = async (id: number, data: SongResult) => {
|
||||||
|
const settingStore = useSettingsStore();
|
||||||
|
|
||||||
|
// 如果禁用了音乐解析功能,则直接返回空结果
|
||||||
|
if (!settingStore.setData.enableMusicUnblock) {
|
||||||
|
return Promise.resolve({ data: { code: 404, message: '音乐解析功能已禁用' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1. 确定使用的音源列表(自定义或全局)
|
||||||
|
const songId = String(id);
|
||||||
|
const savedSourceStr = localStorage.getItem(`song_source_${songId}`);
|
||||||
|
let musicSources: any[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (savedSourceStr) {
|
||||||
|
// 使用自定义音源
|
||||||
|
musicSources = JSON.parse(savedSourceStr);
|
||||||
|
console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);
|
||||||
|
} else {
|
||||||
|
// 使用全局音源设置
|
||||||
|
musicSources = settingStore.setData.enabledMusicSources || [];
|
||||||
|
console.log(`使用全局音源设置:`, musicSources);
|
||||||
|
if (isElectron && musicSources.length > 0) {
|
||||||
|
return getUnblockMusicAudio(id, data, musicSources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析音源设置失败,使用全局设置', e);
|
||||||
|
musicSources = settingStore.setData.enabledMusicSources || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 按优先级解析
|
||||||
|
|
||||||
|
// 2.1 Bilibili解析(优先级最高)
|
||||||
|
if (musicSources.includes('bilibili')) {
|
||||||
|
return await getBilibiliAudio(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.2 GD音乐台解析
|
||||||
|
if (musicSources.includes('gdmusic')) {
|
||||||
|
const gdResult = await getGDMusicAudio(id, data);
|
||||||
|
if (gdResult) return gdResult;
|
||||||
|
// GD解析失败,继续下一步
|
||||||
|
console.log('GD音乐台解析失败,尝试使用其他音源');
|
||||||
|
}
|
||||||
|
console.log('musicSources',musicSources)
|
||||||
|
// 2.3 使用unblockMusic解析其他音源
|
||||||
|
if (isElectron && musicSources.length > 0) {
|
||||||
|
return getUnblockMusicAudio(id, data, musicSources);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 后备方案:使用API请求
|
||||||
|
console.log('无可用音源或不在Electron环境中,使用API请求');
|
||||||
return requestMusic.get<any>('/music', { params: { id } });
|
return requestMusic.get<any>('/music', { params: { id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { isElectron } from '@/utils';
|
|
||||||
|
|
||||||
import { useUserStore } from '../store/modules/user';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户ID
|
|
||||||
* @returns 用户ID或null
|
|
||||||
*/
|
|
||||||
function getUserId(): string | null {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
return userStore.user?.userId?.toString() || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录页面访问
|
|
||||||
* @param page 页面名称或路径
|
|
||||||
*/
|
|
||||||
export async function recordVisit(page: string): Promise<void> {
|
|
||||||
if (!isElectron) return;
|
|
||||||
try {
|
|
||||||
const userId = getUserId();
|
|
||||||
await window.api.invoke('record-visit', page, userId);
|
|
||||||
console.log(`页面访问已记录: ${page}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录页面访问失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 记录歌曲播放
|
|
||||||
* @param songId 歌曲ID
|
|
||||||
* @param songName 歌曲名称
|
|
||||||
* @param artistName 艺术家名称
|
|
||||||
* @param duration 时长(秒)
|
|
||||||
* @param completedPlay 是否完整播放
|
|
||||||
*/
|
|
||||||
export async function recordPlay(
|
|
||||||
songId: string | number,
|
|
||||||
songName: string,
|
|
||||||
artistName: string,
|
|
||||||
duration: number = 0,
|
|
||||||
completedPlay: boolean = false
|
|
||||||
): Promise<void> {
|
|
||||||
if (!isElectron) return;
|
|
||||||
try {
|
|
||||||
const userId = getUserId();
|
|
||||||
|
|
||||||
await window.api.invoke('record-play', {
|
|
||||||
userId,
|
|
||||||
songId,
|
|
||||||
songName,
|
|
||||||
artistName,
|
|
||||||
duration,
|
|
||||||
completedPlay
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`歌曲播放已记录: ${songName}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('记录歌曲播放失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取统计摘要
|
|
||||||
* @returns 统计数据摘要
|
|
||||||
*/
|
|
||||||
export async function getStatsSummary(): Promise<any> {
|
|
||||||
if (!isElectron) return null;
|
|
||||||
try {
|
|
||||||
return await window.api.invoke('get-stats-summary');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取统计摘要失败:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 43 KiB |
BIN
src/renderer/assets/gzh.png
Normal file
BIN
src/renderer/assets/gzh.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 43 KiB |
411
src/renderer/components/TrafficWarningDrawer.vue
Normal file
411
src/renderer/components/TrafficWarningDrawer.vue
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
<template>
|
||||||
|
<div class="traffic-warning-trigger">
|
||||||
|
<n-button circle secondary class="mac-style-button" @click="showDrawer = true">
|
||||||
|
<template #icon>
|
||||||
|
<i class="iconfont ri-information-line"></i>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-drawer
|
||||||
|
v-model:show="showDrawer"
|
||||||
|
:width="isMobile ? '100%' : '800px'"
|
||||||
|
:height="isMobile ? '100%' : '100%'"
|
||||||
|
:placement="isMobile ? 'bottom' : 'right'"
|
||||||
|
@after-leave="handleDrawerClose"
|
||||||
|
:z-index="999999999"
|
||||||
|
:mask-closable="false"
|
||||||
|
>
|
||||||
|
<n-drawer-content
|
||||||
|
title="欢迎使用 AlgerMusicPlayer"
|
||||||
|
closable
|
||||||
|
:native-scrollbar="false"
|
||||||
|
class="mac-style-drawer"
|
||||||
|
>
|
||||||
|
<div class="drawer-container">
|
||||||
|
<div class="warning-content">
|
||||||
|
<div class="warning-message">
|
||||||
|
<h3>获取完整体验</h3>
|
||||||
|
<p class="platform-support">
|
||||||
|
<span>
|
||||||
|
<i class="ri-window-line mr-1"></i>Windows 10+
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<i class="ri-apple-line mr-1"></i>macOS
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<i class="ri-ubuntu-line mr-1"></i>Linux
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<i class="ri-android-line mr-1"></i>Android
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="description">
|
||||||
|
下载桌面应用以获得最佳音乐体验,包含完整功能与更高音质。
|
||||||
|
目前无iOS版本,请使用安卓应用或网页版。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-links">
|
||||||
|
<a href="https://mp.weixin.qq.com/s/9pr1XQB36gShM_-TG2LBdg" target="_blank" class="doc-link">
|
||||||
|
<i class="ri-file-text-line mr-1"></i> 查看使用文档
|
||||||
|
</a>
|
||||||
|
<a href="http://donate.alger.fun/download" target="_blank" class="download-link">
|
||||||
|
<i class="ri-download-2-line mr-1"></i> 立即下载
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="qrcode-section">
|
||||||
|
<img class="qrcode" src="@/assets/gzh.png" alt="公众号" />
|
||||||
|
<p>关注公众号获取最新版本与更新信息</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="support-section">
|
||||||
|
<h4>支持项目</h4>
|
||||||
|
<p class="support-desc">您的支持是我们持续改进的动力</p>
|
||||||
|
<div class="payment-options">
|
||||||
|
<div class="payment-option">
|
||||||
|
<div class="payment-icon wechat">
|
||||||
|
<img src="@/assets/wechat.png" alt="微信支付" />
|
||||||
|
</div>
|
||||||
|
<span>微信支付</span>
|
||||||
|
</div>
|
||||||
|
<div class="payment-option">
|
||||||
|
<div class="payment-icon alipay">
|
||||||
|
<img src="@/assets/alipay.png" alt="支付宝" />
|
||||||
|
</div>
|
||||||
|
<span>支付宝</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-actions">
|
||||||
|
<n-button secondary class="action-button" @click="markAsDonated">已支持</n-button>
|
||||||
|
<n-button type="primary" class="action-button primary" @click="remindLater">稍后提醒</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { isMobile } from '@/utils';
|
||||||
|
|
||||||
|
// 控制抽屉显示状态
|
||||||
|
const showDrawer = ref(false);
|
||||||
|
|
||||||
|
// 处理抽屉关闭后的操作
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
// 抽屉关闭后的逻辑
|
||||||
|
};
|
||||||
|
|
||||||
|
// 一天后提醒
|
||||||
|
const remindLater = () => {
|
||||||
|
const now = new Date();
|
||||||
|
localStorage.setItem('trafficDonated4RemindLater', now.toISOString());
|
||||||
|
showDrawer.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标记为已捐赠(永久不再提示)
|
||||||
|
const markAsDonated = () => {
|
||||||
|
localStorage.setItem('trafficDonated4Never', '1');
|
||||||
|
showDrawer.value = false;
|
||||||
|
};
|
||||||
|
// 组件挂载时检查是否需要显示
|
||||||
|
onMounted(() => {
|
||||||
|
// 优先判断是否永久不再提示
|
||||||
|
if (localStorage.getItem('trafficDonated4Never')) return;
|
||||||
|
|
||||||
|
// 判断一天后提醒
|
||||||
|
const remindLaterTime = localStorage.getItem('trafficDonated4RemindLater');
|
||||||
|
if (remindLaterTime) {
|
||||||
|
const lastRemind = new Date(remindLaterTime);
|
||||||
|
const now = new Date();
|
||||||
|
const hoursDiff = (now.getTime() - lastRemind.getTime()) / (1000 * 60 * 60);
|
||||||
|
if (hoursDiff < 24) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟20秒显示
|
||||||
|
setTimeout(() => {
|
||||||
|
showDrawer.value = true;
|
||||||
|
}, 20000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.traffic-warning-trigger {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.mac-style-button {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mac-style-drawer {
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-container {
|
||||||
|
padding: 20px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-message {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 520px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-support {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #444;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 6px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&.doc-link {
|
||||||
|
color: #555;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.download-link {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #007aff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0062cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-section {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.qrcode {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #0062cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-section {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.support-desc {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 100px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.payment-icon {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 30px;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
z-index: 999999999;
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
min-width: 110px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background-color: #007aff;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #0062cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.warning-message {
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-support {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrcode-section {
|
||||||
|
.qrcode {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-option {
|
||||||
|
.payment-icon {
|
||||||
|
width: 190px;
|
||||||
|
height: 190px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #fff;
|
||||||
|
z-index: 999999999;
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
placement="right"
|
placement="right"
|
||||||
@update:show="$emit('update:modelValue', $event)"
|
@update:show="$emit('update:modelValue', $event)"
|
||||||
:unstable-show-mask="false"
|
:unstable-show-mask="false"
|
||||||
|
:show-mask="false"
|
||||||
>
|
>
|
||||||
<n-drawer-content :title="t('comp.playlistDrawer.title')" class="mac-style-drawer">
|
<n-drawer-content :title="t('comp.playlistDrawer.title')" class="mac-style-drawer">
|
||||||
<n-scrollbar class="h-full">
|
<n-scrollbar class="h-full">
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ import { IDayRecommend } from '@/type/day_recommend';
|
|||||||
import { Playlist } from '@/type/list';
|
import { Playlist } from '@/type/list';
|
||||||
import type { IListDetail } from '@/type/listDetail';
|
import type { IListDetail } from '@/type/listDetail';
|
||||||
import { SongResult } from '@/type/music';
|
import { SongResult } from '@/type/music';
|
||||||
import type { Artist, IHotSinger } from '@/type/singer';
|
import type { IHotSinger } from '@/type/singer';
|
||||||
import {
|
import {
|
||||||
getImgUrl,
|
getImgUrl,
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -150,7 +150,6 @@ import {
|
|||||||
setAnimationDelay,
|
setAnimationDelay,
|
||||||
setBackgroundImg
|
setBackgroundImg
|
||||||
} from '@/utils';
|
} from '@/utils';
|
||||||
import { getArtistDetail } from '@/api/artist';
|
|
||||||
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
|
import { navigateToMusicList } from '@/components/common/MusicListNavigator';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
@@ -233,20 +232,6 @@ onMounted(async () => {
|
|||||||
loadNonUserData();
|
loadNonUserData();
|
||||||
});
|
});
|
||||||
|
|
||||||
const JayChouId = 6452;
|
|
||||||
const loadArtistData = async () => {
|
|
||||||
try {
|
|
||||||
const { data: artistData }: { data: { data: { artist: Artist } } } = await getArtistDetail(JayChouId);
|
|
||||||
console.log('artistData', artistData);
|
|
||||||
if (hotSingerData.value) {
|
|
||||||
// 将周杰伦数据放在第一位
|
|
||||||
hotSingerData.value.artists = [artistData.data.artist, ...hotSingerData.value.artists];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取周杰伦数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 提取每日推荐加载逻辑到单独的函数
|
// 提取每日推荐加载逻辑到单独的函数
|
||||||
const loadDayRecommendData = async () => {
|
const loadDayRecommendData = async () => {
|
||||||
@@ -276,7 +261,6 @@ const loadNonUserData = async () => {
|
|||||||
const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
|
const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });
|
||||||
hotSingerData.value = singerData;
|
hotSingerData.value = singerData;
|
||||||
|
|
||||||
await loadArtistData();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载热门歌手数据失败:', error);
|
console.error('加载热门歌手数据失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,11 +68,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</n-ellipsis>
|
</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
<mini-play-bar
|
<simple-play-bar
|
||||||
v-if="!config.hideMiniPlayBar"
|
v-if="!config.hideMiniPlayBar"
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
:pure-mode-enabled="config.pureModeEnabled"
|
:pure-mode-enabled="config.pureModeEnabled"
|
||||||
component
|
:isDark=" textColors.theme === 'dark'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,6 @@
|
|||||||
@mouseleave="mouseLeaveLayout"
|
@mouseleave="mouseLeaveLayout"
|
||||||
>
|
>
|
||||||
<!-- 歌曲信息 -->
|
<!-- 歌曲信息 -->
|
||||||
|
|
||||||
<div ref="lrcContainer" class="music-lrc-container">
|
<div ref="lrcContainer" class="music-lrc-container">
|
||||||
<div
|
<div
|
||||||
v-if="config.hideCover"
|
v-if="config.hideCover"
|
||||||
@@ -153,7 +152,7 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import LyricSettings from '@/components/lyric/LyricSettings.vue';
|
import LyricSettings from '@/components/lyric/LyricSettings.vue';
|
||||||
import MiniPlayBar from '@/components/player/MiniPlayBar.vue';
|
import SimplePlayBar from '@/components/player/SimplePlayBar.vue';
|
||||||
import LyricCorrectionControl from '@/components/lyric/LyricCorrectionControl.vue';
|
import LyricCorrectionControl from '@/components/lyric/LyricCorrectionControl.vue';
|
||||||
import {
|
import {
|
||||||
artistList,
|
artistList,
|
||||||
|
|||||||
@@ -343,7 +343,12 @@ const playMusicEvent = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const musicFullVisible = ref(false);
|
const musicFullVisible = computed({
|
||||||
|
get: () => playerStore.musicFull,
|
||||||
|
set: (value) => {
|
||||||
|
playerStore.setMusicFull(value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 设置musicFull
|
// 设置musicFull
|
||||||
const setMusicFull = () => {
|
const setMusicFull = () => {
|
||||||
|
|||||||
@@ -52,12 +52,21 @@
|
|||||||
<div v-if="playMusic.source === 'bilibili'" class="text-red-500 text-sm">
|
<div v-if="playMusic.source === 'bilibili'" class="text-red-500 text-sm">
|
||||||
{{ t('player.reparse.bilibiliNotSupported') }}
|
{{ t('player.reparse.bilibiliNotSupported') }}
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 清除自定义音源 -->
|
||||||
|
<div class="text-red-500 text-sm flex items-center bg-light-200 dark:bg-dark-200 rounded-lg p-2 cursor-pointer" @click="clearCustomSource">
|
||||||
|
<div class="flex items-center justify-center w-6 h-6 mr-3 text-lg">
|
||||||
|
<i class="ri-close-circle-line"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ t('player.reparse.clear') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-popover>
|
</n-popover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
import { playMusic } from '@/hooks/MusicHook';
|
import { playMusic } from '@/hooks/MusicHook';
|
||||||
@@ -76,18 +85,13 @@ const currentReparsingSource = ref<Platform | null>(null);
|
|||||||
// 实际存储选中音源的值
|
// 实际存储选中音源的值
|
||||||
const selectedSourcesValue = ref<Platform[]>([]);
|
const selectedSourcesValue = ref<Platform[]>([]);
|
||||||
|
|
||||||
// 判断当前歌曲是否有自定义解析记录
|
const isReparse = ref(localStorage.getItem(`song_source_${String(playMusic.value.id)}`) !== null);
|
||||||
const isReparse = computed(() => {
|
|
||||||
const songId = String(playMusic.value.id);
|
|
||||||
return localStorage.getItem(`song_source_${songId}`) !== null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 可选音源列表
|
// 可选音源列表
|
||||||
const musicSourceOptions = ref([
|
const musicSourceOptions = ref([
|
||||||
{ label: 'MiGu', value: 'migu' as Platform },
|
{ label: 'MiGu', value: 'migu' as Platform },
|
||||||
{ label: 'KuGou', value: 'kugou' as Platform },
|
{ label: 'KuGou', value: 'kugou' as Platform },
|
||||||
{ label: 'pyncmd', value: 'pyncmd' as Platform },
|
{ label: 'pyncmd', value: 'pyncmd' as Platform },
|
||||||
{ label: 'KuWo', value: 'kuwo' as Platform },
|
|
||||||
{ label: 'Bilibili', value: 'bilibili' as Platform },
|
{ label: 'Bilibili', value: 'bilibili' as Platform },
|
||||||
{ label: 'GdMuisc', value: 'gdmusic' as Platform }
|
{ label: 'GdMuisc', value: 'gdmusic' as Platform }
|
||||||
]);
|
]);
|
||||||
@@ -102,7 +106,6 @@ const getSourceIcon = (source: Platform) => {
|
|||||||
const iconMap: Record<Platform, string> = {
|
const iconMap: Record<Platform, string> = {
|
||||||
'migu': 'ri-music-2-fill',
|
'migu': 'ri-music-2-fill',
|
||||||
'kugou': 'ri-music-fill',
|
'kugou': 'ri-music-fill',
|
||||||
'kuwo': 'ri-album-fill',
|
|
||||||
'qq': 'ri-qq-fill',
|
'qq': 'ri-qq-fill',
|
||||||
'joox': 'ri-disc-fill',
|
'joox': 'ri-disc-fill',
|
||||||
'pyncmd': 'ri-netease-cloud-music-fill',
|
'pyncmd': 'ri-netease-cloud-music-fill',
|
||||||
@@ -129,6 +132,13 @@ const initSelectedSources = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清除自定义音源
|
||||||
|
const clearCustomSource = () => {
|
||||||
|
localStorage.removeItem(`song_source_${String(playMusic.value.id)}`);
|
||||||
|
selectedSourcesValue.value = [];
|
||||||
|
isReparse.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
// 直接重新解析当前歌曲
|
// 直接重新解析当前歌曲
|
||||||
const directReparseMusic = async (source: Platform) => {
|
const directReparseMusic = async (source: Platform) => {
|
||||||
if (isReparsing.value || playMusic.value.source === 'bilibili') {
|
if (isReparsing.value || playMusic.value.source === 'bilibili') {
|
||||||
|
|||||||
509
src/renderer/components/player/SimplePlayBar.vue
Normal file
509
src/renderer/components/player/SimplePlayBar.vue
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
<template>
|
||||||
|
<div class="play-bar" :class="{ 'dark-theme': isDarkMode }" ref="playBarRef">
|
||||||
|
<div class="container">
|
||||||
|
<!-- 顶部进度条和时间 -->
|
||||||
|
<div class="top-section">
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<div class="progress-bar" @click="handleProgressClick">
|
||||||
|
<div class="progress-track"></div>
|
||||||
|
<div class="progress-fill" :style="{ width: `${(nowTime / allTime) * 100}%` }"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 时间显示 -->
|
||||||
|
<div class="time-display">
|
||||||
|
<span class="current-time">{{ formatTime(nowTime) }}</span>
|
||||||
|
<span class="total-time">{{ formatTime(allTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主控制区域 -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<div class="left-controls">
|
||||||
|
<button class="control-btn small-btn" @click="togglePlayMode">
|
||||||
|
<i class="iconfont" :class="playModeIcon"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="center-controls">
|
||||||
|
<!-- 上一首 -->
|
||||||
|
<button class="control-btn" @click="handlePrev">
|
||||||
|
<i class="iconfont icon-prev"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 播放/暂停 -->
|
||||||
|
<button class="control-btn play-btn" @click="playMusicEvent">
|
||||||
|
<i class="iconfont" :class="play ? 'icon-stop' : 'icon-play'"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 下一首 -->
|
||||||
|
<button class="control-btn" @click="handleNext">
|
||||||
|
<i class="iconfont icon-next"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-controls">
|
||||||
|
<!-- 播放列表按钮 -->
|
||||||
|
<button class="control-btn small-btn" @click="openPlayListDrawer">
|
||||||
|
<i class="iconfont icon-list"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部控制区域 -->
|
||||||
|
<div class="bottom-section">
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<!-- 音量控制 -->
|
||||||
|
<div class="volume-control">
|
||||||
|
<i class="iconfont" :class="getVolumeIcon" @click="mute"></i>
|
||||||
|
<div class="volume-slider">
|
||||||
|
<n-slider
|
||||||
|
v-model:value="volumeSlider"
|
||||||
|
:step="1"
|
||||||
|
:tooltip="false"
|
||||||
|
@wheel.prevent="handleVolumeWheel"
|
||||||
|
></n-slider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, onMounted, watch } from 'vue';
|
||||||
|
import { secondToMinute } from '@/utils';
|
||||||
|
import { allTime, nowTime, playMusic } from '@/hooks/MusicHook';
|
||||||
|
import { audioService } from '@/services/audioService';
|
||||||
|
import { usePlayerStore } from '@/store/modules/player';
|
||||||
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
isDark: boolean;
|
||||||
|
}>(), {
|
||||||
|
isDark: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const playerStore = usePlayerStore();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const playBarRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// 播放状态
|
||||||
|
const play = computed(() => playerStore.isPlay);
|
||||||
|
|
||||||
|
// 播放模式
|
||||||
|
const playMode = computed(() => playerStore.playMode);
|
||||||
|
const playModeIcon = computed(() => {
|
||||||
|
switch (playMode.value) {
|
||||||
|
case 0:
|
||||||
|
return 'ri-repeat-2-line';
|
||||||
|
case 1:
|
||||||
|
return 'ri-repeat-one-line';
|
||||||
|
case 2:
|
||||||
|
return 'ri-shuffle-line';
|
||||||
|
default:
|
||||||
|
return 'ri-repeat-2-line';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换播放模式
|
||||||
|
const togglePlayMode = () => {
|
||||||
|
playerStore.togglePlayMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 音量控制
|
||||||
|
const audioVolume = ref(
|
||||||
|
localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const volumeSlider = computed({
|
||||||
|
get: () => audioVolume.value * 100,
|
||||||
|
set: (value) => {
|
||||||
|
localStorage.setItem('volume', (value / 100).toString());
|
||||||
|
audioService.setVolume(value / 100);
|
||||||
|
audioVolume.value = value / 100;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 音量图标
|
||||||
|
const getVolumeIcon = computed(() => {
|
||||||
|
if (audioVolume.value === 0) return 'ri-volume-mute-line';
|
||||||
|
if (audioVolume.value <= 0.5) return 'ri-volume-down-line';
|
||||||
|
return 'ri-volume-up-line';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 静音切换
|
||||||
|
const mute = () => {
|
||||||
|
if (volumeSlider.value === 0) {
|
||||||
|
volumeSlider.value = 30;
|
||||||
|
} else {
|
||||||
|
volumeSlider.value = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 鼠标滚轮调整音量
|
||||||
|
const handleVolumeWheel = (e: WheelEvent) => {
|
||||||
|
const delta = e.deltaY < 0 ? 5 : -5;
|
||||||
|
const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);
|
||||||
|
volumeSlider.value = newValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 播放控制
|
||||||
|
const handlePrev = () => playerStore.prevPlay();
|
||||||
|
const handleNext = () => playerStore.nextPlay();
|
||||||
|
|
||||||
|
const playMusicEvent = async () => {
|
||||||
|
try {
|
||||||
|
await playerStore.setPlay({ ...playMusic.value });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('播放出错:', error);
|
||||||
|
playerStore.nextPlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 进度条控制
|
||||||
|
const handleProgressClick = (e: MouseEvent) => {
|
||||||
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
const percent = (e.clientX - rect.left) / rect.width;
|
||||||
|
audioService.seek(allTime.value * percent);
|
||||||
|
nowTime.value = allTime.value * percent;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
return secondToMinute(seconds);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开播放列表抽屉
|
||||||
|
const openPlayListDrawer = () => {
|
||||||
|
playerStore.setPlayListDrawerVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 深色模式
|
||||||
|
const isDarkMode = computed(() => settingsStore.theme === 'dark' || props.isDark);
|
||||||
|
|
||||||
|
// 主题颜色应用函数
|
||||||
|
const applyThemeColor = (colorValue: string) => {
|
||||||
|
if (!colorValue || !playBarRef.value) return;
|
||||||
|
|
||||||
|
console.log('应用主题色:', colorValue);
|
||||||
|
const playBarElement = playBarRef.value;
|
||||||
|
|
||||||
|
// 解析RGB值
|
||||||
|
const rgbMatch = colorValue.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
||||||
|
|
||||||
|
if (rgbMatch) {
|
||||||
|
const [_, r, g, b] = rgbMatch.map(Number);
|
||||||
|
|
||||||
|
// 计算颜色亮度 (0-255)
|
||||||
|
// 使用加权平均值公式: 0.299*R + 0.587*G + 0.114*B
|
||||||
|
const brightness = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
||||||
|
|
||||||
|
console.log(`主题色亮度: ${brightness}/255`);
|
||||||
|
|
||||||
|
// 设置主色
|
||||||
|
playBarElement.style.setProperty('--fill-color', colorValue);
|
||||||
|
|
||||||
|
// 亮度自适应处理
|
||||||
|
if (brightness > 200) { // 非常亮的颜色
|
||||||
|
// 深化主色以增加对比度
|
||||||
|
const darkenedColor = `rgb(${Math.max(0, r - 60)}, ${Math.max(0, g - 60)}, ${Math.max(0, b - 60)})`;
|
||||||
|
playBarElement.style.setProperty('--fill-color-alt', darkenedColor);
|
||||||
|
playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.5)`); // 提高透明度
|
||||||
|
playBarElement.style.setProperty('--text-on-fill', '#000000'); // 亮色背景上用黑色文字
|
||||||
|
playBarElement.style.setProperty('--high-contrast-color', '#000000'); // 高对比度颜色
|
||||||
|
playBarElement.classList.add('light-theme-color');
|
||||||
|
playBarElement.classList.remove('dark-theme-color');
|
||||||
|
} else if (brightness < 50) { // 非常暗的颜色
|
||||||
|
// 提亮主色以增加可见性
|
||||||
|
const lightenedColor = `rgb(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 60)})`;
|
||||||
|
playBarElement.style.setProperty('--fill-color-alt', lightenedColor);
|
||||||
|
playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.7)`); // 提高透明度
|
||||||
|
playBarElement.style.setProperty('--text-on-fill', '#ffffff'); // 暗色背景上用白色文字
|
||||||
|
playBarElement.style.setProperty('--high-contrast-color', '#ffffff'); // 高对比度颜色
|
||||||
|
playBarElement.classList.add('dark-theme-color');
|
||||||
|
playBarElement.classList.remove('light-theme-color');
|
||||||
|
} else {
|
||||||
|
// 计算辅助色和高亮色
|
||||||
|
// 普通亮度颜色,正常处理
|
||||||
|
playBarElement.style.setProperty('--fill-color-alt', colorValue); // 保持一致
|
||||||
|
playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.25)`);
|
||||||
|
// 根据亮度决定文本颜色
|
||||||
|
const textColor = brightness > 125 ? '#000000' : '#ffffff';
|
||||||
|
playBarElement.style.setProperty('--text-on-fill', textColor);
|
||||||
|
playBarElement.style.setProperty('--high-contrast-color', textColor);
|
||||||
|
playBarElement.classList.remove('light-theme-color');
|
||||||
|
playBarElement.classList.remove('dark-theme-color');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置亮色(用于高亮效果)
|
||||||
|
const lightenedColor = `rgb(${Math.min(255, r + 40)}, ${Math.min(255, g + 40)}, ${Math.min(255, b + 40)})`;
|
||||||
|
playBarElement.style.setProperty('--fill-color-light', lightenedColor);
|
||||||
|
} else {
|
||||||
|
// 无法解析RGB值时的默认设置
|
||||||
|
playBarElement.style.setProperty('--fill-color', colorValue);
|
||||||
|
playBarElement.style.setProperty('--fill-color-transparent', `${colorValue}40`);
|
||||||
|
playBarElement.style.setProperty('--fill-color-light', `${colorValue}80`);
|
||||||
|
playBarElement.style.setProperty('--fill-color-alt', colorValue);
|
||||||
|
playBarElement.style.setProperty('--text-on-fill', '#ffffff');
|
||||||
|
playBarElement.style.setProperty('--high-contrast-color', '#ffffff');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听主题色变化
|
||||||
|
watch(() => playerStore.playMusic.primaryColor, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
applyThemeColor(newVal);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (playerStore.playMusic?.primaryColor) {
|
||||||
|
setTimeout(() => {
|
||||||
|
applyThemeColor(playerStore.playMusic.primaryColor as string);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.play-bar {
|
||||||
|
@apply w-full;
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
/* 默认变量 */
|
||||||
|
--text-on-fill: #ffffff;
|
||||||
|
--high-contrast-color: #ffffff;
|
||||||
|
|
||||||
|
&.dark-theme {
|
||||||
|
--text-color: #333333;
|
||||||
|
--muted-color: rgba(0, 0, 0, 0.6);
|
||||||
|
--track-color: rgba(0, 0, 0, 0.2);
|
||||||
|
--track-color-hover: rgba(0, 0, 0, 0.4);
|
||||||
|
--fill-color: #1ed760;
|
||||||
|
--fill-color-alt: #1ed760;
|
||||||
|
--fill-color-transparent: rgba(30, 215, 96, 0.25);
|
||||||
|
--fill-color-light: rgba(30, 215, 96, 0.5);
|
||||||
|
--button-bg: rgba(0, 0, 0, 0.1);
|
||||||
|
--button-hover: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.dark-theme) {
|
||||||
|
--text-color: #f1f1f1;
|
||||||
|
--muted-color: rgba(255, 255, 255, 0.6);
|
||||||
|
--track-color: rgba(255, 255, 255, 0.1);
|
||||||
|
--track-color-hover: rgba(255, 255, 255, 0.2);
|
||||||
|
--fill-color: #73e49a;
|
||||||
|
--fill-color-alt: #73e49a;
|
||||||
|
--fill-color-transparent: rgba(115, 228, 154, 0.25);
|
||||||
|
--fill-color-light: rgba(115, 228, 154, 0.5);
|
||||||
|
--button-bg: rgba(255, 255, 255, 0.05);
|
||||||
|
--button-hover: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 极亮主题色适配 */
|
||||||
|
&.light-theme-color {
|
||||||
|
.progress-fill {
|
||||||
|
box-shadow: 0 0 8px var(--fill-color-transparent), inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.play-btn {
|
||||||
|
box-shadow: 0 3px 8px var(--fill-color-transparent), 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
color: var(--text-on-fill);
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-control .iconfont:hover {
|
||||||
|
color: var(--fill-color-alt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 极暗主题色适配 */
|
||||||
|
&.dark-theme-color {
|
||||||
|
.progress-fill {
|
||||||
|
box-shadow: 0 0 10px var(--fill-color-transparent), inset 0 0 0 1px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.play-btn {
|
||||||
|
box-shadow: 0 3px 12px var(--fill-color-transparent), 0 0 0 1px rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
text-shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-control .iconfont:hover {
|
||||||
|
color: var(--fill-color-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-section {
|
||||||
|
@apply mb-3;
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
@apply relative cursor-pointer h-2 mb-2 w-full;
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
@apply absolute inset-0 rounded-full transition-all duration-150;
|
||||||
|
background-color: var(--track-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
@apply absolute top-0 left-0 h-full rounded-full transition-all duration-150;
|
||||||
|
background: linear-gradient(90deg, var(--fill-color), var(--fill-color-light));
|
||||||
|
box-shadow: 0 0 8px var(--fill-color-transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.progress-track{
|
||||||
|
background-color: var(--track-color-hover);
|
||||||
|
}
|
||||||
|
.progress-track, .progress-fill {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
box-shadow: 0 0 12px var(--fill-color-transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-display {
|
||||||
|
@apply flex justify-between text-base;
|
||||||
|
color: var(--muted-color);
|
||||||
|
|
||||||
|
.time-separator {
|
||||||
|
@apply mx-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-time {
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section {
|
||||||
|
@apply flex items-center justify-between mb-4;
|
||||||
|
|
||||||
|
.left-controls, .right-controls {
|
||||||
|
@apply flex items-center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-controls {
|
||||||
|
@apply flex items-center justify-center space-x-6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-section {
|
||||||
|
@apply flex items-center justify-between mt-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
@apply flex items-center justify-center rounded-full outline-none border-0 transition-all duration-200;
|
||||||
|
color: var(--text-color);
|
||||||
|
background: transparent;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--button-bg);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.play-btn {
|
||||||
|
background: linear-gradient(145deg, var(--fill-color), var(--fill-color-alt));
|
||||||
|
color: var(--text-on-fill);
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
box-shadow: 0 3px 8px var(--fill-color-transparent);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 12px var(--fill-color-transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.small-btn {
|
||||||
|
@apply text-2xl;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply text-2xl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-control {
|
||||||
|
@apply flex items-center space-x-2;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
.iconfont {
|
||||||
|
@apply cursor-pointer text-base;
|
||||||
|
transition: transform 0.2s ease, color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
color: var(--fill-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.volume-slider {
|
||||||
|
@apply w-24;
|
||||||
|
|
||||||
|
:deep(.n-slider) {
|
||||||
|
--n-rail-height: 3px;
|
||||||
|
--n-fill-color: var(--fill-color);
|
||||||
|
--n-rail-color: var(--track-color);
|
||||||
|
--n-handle-size: 12px;
|
||||||
|
|
||||||
|
.n-slider-rail {
|
||||||
|
@apply rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-rail__fill {
|
||||||
|
background: linear-gradient(90deg, var(--fill-color), var(--fill-color-light));
|
||||||
|
box-shadow: 0 0 6px var(--fill-color-transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-slider-handle {
|
||||||
|
@apply opacity-0 transition-opacity duration-200;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 0 6px var(--fill-color-transparent), 0 0 0 1px var(--high-contrast-color);
|
||||||
|
border: 2px solid var(--fill-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .n-slider-handle {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.like-active {
|
||||||
|
color: var(--fill-color);
|
||||||
|
text-shadow: 0 0 8px var(--fill-color-transparent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -56,7 +56,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
sources: {
|
sources: {
|
||||||
type: Array as () => Platform[],
|
type: Array as () => Platform[],
|
||||||
default: () => ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo']
|
default: () => ['migu', 'kugou', 'pyncmd', 'bilibili']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,11 +67,10 @@ const visible = ref(props.show);
|
|||||||
const selectedSources = ref<Platform[]>(props.sources);
|
const selectedSources = ref<Platform[]>(props.sources);
|
||||||
|
|
||||||
const musicSourceOptions = ref([
|
const musicSourceOptions = ref([
|
||||||
{ label: 'MiGu音乐', value: 'migu' },
|
{ label: 'MG', value: 'migu' },
|
||||||
{ label: '酷狗音乐', value: 'kugou' },
|
{ label: 'KG', value: 'kugou' },
|
||||||
{ label: 'pyncmd', value: 'pyncmd' },
|
{ label: 'pyncmd', value: 'pyncmd' },
|
||||||
{ label: '酷我音乐', value: 'kuwo' },
|
{ label: 'Bilibili', value: 'bilibili' },
|
||||||
{ label: 'Bilibili音乐', value: 'bilibili' },
|
|
||||||
{ label: 'GD音乐台', value: 'gdmusic' }
|
{ label: 'GD音乐台', value: 'gdmusic' }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ watch(
|
|||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
// 确保至少选择一个音源
|
// 确保至少选择一个音源
|
||||||
const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo'];
|
const defaultPlatforms = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||||
const valuesToEmit = selectedSources.value.length > 0
|
const valuesToEmit = selectedSources.value.length > 0
|
||||||
? [...new Set(selectedSources.value)]
|
? [...new Set(selectedSources.value)]
|
||||||
: defaultPlatforms;
|
: defaultPlatforms;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// musicHistoryHooks
|
// musicHistoryHooks
|
||||||
import { useLocalStorage } from '@vueuse/core';
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
import { recordPlay } from '@/api/stats';
|
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
|
|
||||||
export const useMusicHistory = () => {
|
export const useMusicHistory = () => {
|
||||||
@@ -15,25 +14,6 @@ export const useMusicHistory = () => {
|
|||||||
} else {
|
} else {
|
||||||
musicHistory.value.unshift({ ...music, count: 1 });
|
musicHistory.value.unshift({ ...music, count: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录播放统计
|
|
||||||
if (music?.id && music?.name) {
|
|
||||||
// 获取艺术家名称
|
|
||||||
let artistName = '未知艺术家';
|
|
||||||
|
|
||||||
if (music.ar) {
|
|
||||||
artistName = music.ar.map((artist) => artist.name).join('/');
|
|
||||||
} else if (music.song?.artists && music.song.artists.length > 0) {
|
|
||||||
artistName = music.song.artists.map((artist) => artist.name).join('/');
|
|
||||||
} else if (music.artists) {
|
|
||||||
artistName = music.artists.map((artist) => artist.name).join('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送播放统计
|
|
||||||
recordPlay(music.id, music.name, artistName).catch((error) =>
|
|
||||||
console.error('记录播放统计失败:', error)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const delMusic = (music: SongResult) => {
|
const delMusic = (music: SongResult) => {
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ const currentSongId = ref<number | undefined>();
|
|||||||
const openPlaylistDrawer = (songId: number, isOpen: boolean = true) => {
|
const openPlaylistDrawer = (songId: number, isOpen: boolean = true) => {
|
||||||
currentSongId.value = songId;
|
currentSongId.value = songId;
|
||||||
showPlaylistDrawer.value = isOpen;
|
showPlaylistDrawer.value = isOpen;
|
||||||
|
playerStore.setMusicFull(false);
|
||||||
|
playerStore.setPlayListDrawerVisible(!isOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 将方法提供给全局
|
// 将方法提供给全局
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||||
|
|
||||||
import { recordVisit } from '@/api/stats';
|
import { useUserStore } from '../store/modules/user';
|
||||||
import AppLayout from '@/layout/AppLayout.vue';
|
import AppLayout from '@/layout/AppLayout.vue';
|
||||||
import MiniLayout from '@/layout/MiniLayout.vue';
|
import MiniLayout from '@/layout/MiniLayout.vue';
|
||||||
import homeRouter from '@/router/home';
|
import homeRouter from '@/router/home';
|
||||||
import otherRouter from '@/router/other';
|
import otherRouter from '@/router/other';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
|
|
||||||
|
function getUserId(): string | null {
|
||||||
|
const userStore = useUserStore();
|
||||||
|
return userStore.user?.userId?.toString() || null;
|
||||||
|
}
|
||||||
|
|
||||||
// 由于 Vue Router 守卫在创建前不能直接使用组合式 API
|
// 由于 Vue Router 守卫在创建前不能直接使用组合式 API
|
||||||
// 我们创建一个辅助函数来获取 store 实例
|
// 我们创建一个辅助函数来获取 store 实例
|
||||||
let _settingsStore: ReturnType<typeof useSettingsStore> | null = null;
|
let _settingsStore: ReturnType<typeof useSettingsStore> | null = null;
|
||||||
@@ -76,7 +81,8 @@ router.afterEach((to) => {
|
|||||||
const pageName = to.name?.toString() || to.path;
|
const pageName = to.name?.toString() || to.path;
|
||||||
// 使用setTimeout避免阻塞路由导航
|
// 使用setTimeout避免阻塞路由导航
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
recordVisit(pageName).catch((error) => console.error('记录页面访问失败:', error));
|
const userId = getUserId();
|
||||||
|
console.log('pageName', pageName, userId);
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -802,6 +802,8 @@ class AudioService {
|
|||||||
// 立即设置音量
|
// 立即设置音量
|
||||||
this.gainNode.gain.cancelScheduledValues(this.context!.currentTime);
|
this.gainNode.gain.cancelScheduledValues(this.context!.currentTime);
|
||||||
this.gainNode.gain.setValueAtTime(linearVolume, this.context!.currentTime);
|
this.gainNode.gain.setValueAtTime(linearVolume, this.context!.currentTime);
|
||||||
|
} else {
|
||||||
|
this.currentSound?.volume(linearVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存值
|
// 保存值
|
||||||
@@ -809,6 +811,40 @@ class AudioService {
|
|||||||
|
|
||||||
console.log('Volume applied (linear):', linearVolume);
|
console.log('Volume applied (linear):', linearVolume);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加方法检查当前音频是否在加载状态
|
||||||
|
isLoading(): boolean {
|
||||||
|
if (!this.currentSound) return false;
|
||||||
|
|
||||||
|
// 检查Howl对象的内部状态
|
||||||
|
// 如果状态为1表示已经加载但未完成,状态为2表示正在加载
|
||||||
|
const state = (this.currentSound as any)._state;
|
||||||
|
// 如果操作锁激活也认为是加载状态
|
||||||
|
return this.operationLock || (state === 'loading' || state === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查音频是否真正在播放
|
||||||
|
isActuallyPlaying(): boolean {
|
||||||
|
if (!this.currentSound) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 综合判断:
|
||||||
|
// 1. Howler API是否报告正在播放
|
||||||
|
// 2. 是否不在加载状态
|
||||||
|
// 3. 确保音频上下文状态正常
|
||||||
|
const isPlaying = this.currentSound.playing();
|
||||||
|
const isLoading = this.isLoading();
|
||||||
|
const contextRunning = Howler.ctx && Howler.ctx.state === 'running';
|
||||||
|
|
||||||
|
console.log(`实际播放状态检查: playing=${isPlaying}, loading=${isLoading}, contextRunning=${contextRunning}`);
|
||||||
|
|
||||||
|
// 只有在三个条件都满足时才认为是真正在播放
|
||||||
|
return isPlaying && !isLoading && contextRunning;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查播放状态出错:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const audioService = new AudioService();
|
export const audioService = new AudioService();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
import { useThrottleFn } from '@vueuse/core';
|
||||||
|
|
||||||
import i18n from '@/../i18n/renderer';
|
import i18n from '@/../i18n/renderer';
|
||||||
import { getBilibiliAudioUrl } from '@/api/bilibili';
|
import { getBilibiliAudioUrl } from '@/api/bilibili';
|
||||||
@@ -556,16 +557,95 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加用户意图跟踪变量
|
||||||
|
const userPlayIntent = ref(true);
|
||||||
|
|
||||||
|
let checkPlayTime: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
// 添加独立的播放状态检测函数
|
||||||
|
const checkPlaybackState = (song: SongResult, timeout: number = 4000) => {
|
||||||
|
if(checkPlayTime) {
|
||||||
|
clearTimeout(checkPlayTime);
|
||||||
|
}
|
||||||
|
const sound = audioService.getCurrentSound();
|
||||||
|
if (!sound) return;
|
||||||
|
|
||||||
|
// 使用audioService的事件系统监听播放状态
|
||||||
|
// 添加一次性播放事件监听器
|
||||||
|
const onPlayHandler = () => {
|
||||||
|
// 播放事件触发,表示成功播放
|
||||||
|
console.log('播放事件触发,歌曲成功开始播放');
|
||||||
|
audioService.off('play', onPlayHandler);
|
||||||
|
audioService.off('playerror', onPlayErrorHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加一次性播放错误事件监听器
|
||||||
|
const onPlayErrorHandler = async () => {
|
||||||
|
console.log('播放错误事件触发,尝试重新获取URL');
|
||||||
|
audioService.off('play', onPlayHandler);
|
||||||
|
audioService.off('playerror', onPlayErrorHandler);
|
||||||
|
|
||||||
|
// 只有用户仍然希望播放时才重试
|
||||||
|
if (userPlayIntent.value && play.value) {
|
||||||
|
// 重置URL并重新播放
|
||||||
|
playMusic.value.playMusicUrl = undefined;
|
||||||
|
// 保持播放状态,但强制重新获取URL
|
||||||
|
const refreshedSong = { ...song, isFirstPlay: true };
|
||||||
|
await handlePlayMusic(refreshedSong, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 注册事件监听器
|
||||||
|
audioService.on('play', onPlayHandler);
|
||||||
|
audioService.on('playerror', onPlayErrorHandler);
|
||||||
|
|
||||||
|
// 额外的安全检查:如果指定时间后仍未播放也未触发错误,且用户仍希望播放
|
||||||
|
checkPlayTime = setTimeout(() => {
|
||||||
|
// 使用更准确的方法检查是否真正在播放
|
||||||
|
if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) {
|
||||||
|
console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放,尝试重新获取URL`);
|
||||||
|
// 移除事件监听器
|
||||||
|
audioService.off('play', onPlayHandler);
|
||||||
|
audioService.off('playerror', onPlayErrorHandler);
|
||||||
|
|
||||||
|
// 重置URL并重新播放
|
||||||
|
playMusic.value.playMusicUrl = undefined;
|
||||||
|
// 保持播放状态,强制重新获取URL
|
||||||
|
(async () => {
|
||||||
|
const refreshedSong = { ...song, isFirstPlay: true };
|
||||||
|
await handlePlayMusic(refreshedSong, true);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
};
|
||||||
|
|
||||||
const setPlay = async (song: SongResult) => {
|
const setPlay = async (song: SongResult) => {
|
||||||
try {
|
try {
|
||||||
|
// 检查URL是否已过期
|
||||||
|
if (song.expiredAt && song.expiredAt < Date.now()) {
|
||||||
|
console.info(`歌曲URL已过期,重新获取: ${song.name}`);
|
||||||
|
song.playMusicUrl = undefined;
|
||||||
|
// 重置过期时间,以便重新获取
|
||||||
|
song.expiredAt = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
// 如果是当前正在播放的音乐,则切换播放/暂停状态
|
||||||
if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl && !song.isFirstPlay) {
|
if (playMusic.value.id === song.id && playMusic.value.playMusicUrl === song.playMusicUrl && !song.isFirstPlay) {
|
||||||
if (play.value) {
|
if (play.value) {
|
||||||
setPlayMusic(false);
|
setPlayMusic(false);
|
||||||
audioService.getCurrentSound()?.pause();
|
audioService.getCurrentSound()?.pause();
|
||||||
|
// 设置用户意图为暂停
|
||||||
|
userPlayIntent.value = false;
|
||||||
} else {
|
} else {
|
||||||
setPlayMusic(true);
|
setPlayMusic(true);
|
||||||
audioService.getCurrentSound()?.play();
|
// 设置用户意图为播放
|
||||||
|
userPlayIntent.value = true;
|
||||||
|
const sound = audioService.getCurrentSound();
|
||||||
|
if (sound) {
|
||||||
|
sound.play();
|
||||||
|
// 使用独立的播放状态检测函数
|
||||||
|
checkPlaybackState(playMusic.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -600,10 +680,14 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
const setPlayMusic = async (value: boolean | SongResult) => {
|
const setPlayMusic = async (value: boolean | SongResult) => {
|
||||||
if (typeof value === 'boolean') {
|
if (typeof value === 'boolean') {
|
||||||
setIsPlay(value);
|
setIsPlay(value);
|
||||||
|
// 记录用户的播放意图
|
||||||
|
userPlayIntent.value = value;
|
||||||
} else {
|
} else {
|
||||||
await handlePlayMusic(value);
|
await handlePlayMusic(value);
|
||||||
play.value = true;
|
play.value = true;
|
||||||
isPlay.value = true;
|
isPlay.value = true;
|
||||||
|
// 设置为播放意图
|
||||||
|
userPlayIntent.value = true;
|
||||||
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
localStorage.setItem('currentPlayMusic', JSON.stringify(playMusic.value));
|
||||||
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
|
localStorage.setItem('currentPlayMusicUrl', playMusicUrl.value);
|
||||||
}
|
}
|
||||||
@@ -803,8 +887,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改nextPlay方法,改进播放逻辑
|
const _nextPlay = async () => {
|
||||||
const nextPlay = async () => {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -927,9 +1010,10 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改 prevPlay 方法,使用与 nextPlay 相似的逻辑改进
|
// 节流
|
||||||
const prevPlay = async () => {
|
const nextPlay = useThrottleFn(_nextPlay, 500);
|
||||||
|
|
||||||
|
const _prevPlay = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -1010,6 +1094,9 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 节流
|
||||||
|
const prevPlay = useThrottleFn(_prevPlay, 500);
|
||||||
|
|
||||||
const togglePlayMode = () => {
|
const togglePlayMode = () => {
|
||||||
playMode.value = (playMode.value + 1) % 3;
|
playMode.value = (playMode.value + 1) % 3;
|
||||||
localStorage.setItem('playMode', JSON.stringify(playMode.value));
|
localStorage.setItem('playMode', JSON.stringify(playMode.value));
|
||||||
@@ -1185,6 +1272,12 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
// 播放新音频,传递是否应该播放的状态
|
// 播放新音频,传递是否应该播放的状态
|
||||||
console.log('调用audioService.play,播放状态:', shouldPlay);
|
console.log('调用audioService.play,播放状态:', shouldPlay);
|
||||||
const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay, initialPosition || 0);
|
const newSound = await audioService.play(playMusicUrl.value, playMusic.value, shouldPlay, initialPosition || 0);
|
||||||
|
|
||||||
|
// 添加播放状态检测(仅当需要播放时)
|
||||||
|
if (shouldPlay) {
|
||||||
|
checkPlaybackState(playMusic.value);
|
||||||
|
}
|
||||||
|
|
||||||
// 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器
|
// 发布音频就绪事件,让 MusicHook.ts 来处理设置监听器
|
||||||
window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } }));
|
window.dispatchEvent(new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } }));
|
||||||
|
|
||||||
@@ -1215,6 +1308,8 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
|
|
||||||
// 延迟较长时间,确保锁已完全释放
|
// 延迟较长时间,确保锁已完全释放
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// 如果用户仍希望播放
|
||||||
|
if (userPlayIntent.value && play.value) {
|
||||||
// 直接重试当前歌曲,而不是切换到下一首
|
// 直接重试当前歌曲,而不是切换到下一首
|
||||||
playAudio().catch(e => {
|
playAudio().catch(e => {
|
||||||
console.error('重试播放失败,切换到下一首:', e);
|
console.error('重试播放失败,切换到下一首:', e);
|
||||||
@@ -1224,6 +1319,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
nextPlay();
|
nextPlay();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
// 其他错误,切换到下一首
|
// 其他错误,切换到下一首
|
||||||
@@ -1253,7 +1349,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存用户选择的音源
|
// 保存用户选择的音源(作为数组传递,确保unblockMusic可以使用)
|
||||||
const songId = String(currentSong.id);
|
const songId = String(currentSong.id);
|
||||||
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform]));
|
localStorage.setItem(`song_source_${songId}`, JSON.stringify([sourcePlatform]));
|
||||||
|
|
||||||
@@ -1268,10 +1364,16 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
? parseInt(currentSong.id, 10)
|
? parseInt(currentSong.id, 10)
|
||||||
: currentSong.id;
|
: currentSong.id;
|
||||||
|
|
||||||
const res = await getParsingMusicUrl(numericId, cloneDeep(currentSong));
|
console.log(`使用音源 ${sourcePlatform} 重新解析歌曲 ${numericId}`);
|
||||||
|
|
||||||
|
// 克隆一份歌曲数据,防止修改原始数据
|
||||||
|
const songData = cloneDeep(currentSong);
|
||||||
|
|
||||||
|
const res = await getParsingMusicUrl(numericId, songData);
|
||||||
if (res && res.data && res.data.data && res.data.data.url) {
|
if (res && res.data && res.data.data && res.data.data.url) {
|
||||||
// 更新URL
|
// 更新URL
|
||||||
const newUrl = res.data.data.url;
|
const newUrl = res.data.data.url;
|
||||||
|
console.log(`解析成功,获取新URL: ${newUrl.substring(0, 50)}...`);
|
||||||
|
|
||||||
// 使用新URL更新播放
|
// 使用新URL更新播放
|
||||||
const updatedMusic = {
|
const updatedMusic = {
|
||||||
@@ -1286,6 +1388,7 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
console.warn(`使用音源 ${sourcePlatform} 解析失败`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1307,6 +1410,8 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
currentSound.pause();
|
currentSound.pause();
|
||||||
}
|
}
|
||||||
setPlayMusic(false);
|
setPlayMusic(false);
|
||||||
|
// 明确设置用户意图为暂停
|
||||||
|
userPlayIntent.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('暂停播放失败:', error);
|
console.error('暂停播放失败:', error);
|
||||||
}
|
}
|
||||||
@@ -1345,8 +1450,8 @@ export const usePlayerStore = defineStore('player', () => {
|
|||||||
clearPlayAll,
|
clearPlayAll,
|
||||||
setPlay,
|
setPlay,
|
||||||
setIsPlay,
|
setIsPlay,
|
||||||
nextPlay,
|
nextPlay: nextPlay as unknown as typeof _nextPlay,
|
||||||
prevPlay,
|
prevPlay: prevPlay as unknown as typeof _prevPlay,
|
||||||
setPlayMusic,
|
setPlayMusic,
|
||||||
setMusicFull,
|
setMusicFull,
|
||||||
setPlayList,
|
setPlayList,
|
||||||
|
|||||||
@@ -248,7 +248,6 @@ export interface IArtists {
|
|||||||
export type MusicSourceType =
|
export type MusicSourceType =
|
||||||
| 'tencent'
|
| 'tencent'
|
||||||
| 'kugou'
|
| 'kugou'
|
||||||
| 'kuwo'
|
|
||||||
| 'migu'
|
| 'migu'
|
||||||
| 'netease'
|
| 'netease'
|
||||||
| 'joox'
|
| 'joox'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// 音乐平台类型
|
// 音乐平台类型
|
||||||
export type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'gdmusic';
|
export type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'bilibili' | 'gdmusic';
|
||||||
|
|
||||||
// 默认平台列表
|
// 默认平台列表
|
||||||
export const DEFAULT_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo'];
|
export const DEFAULT_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||||
@@ -67,7 +67,6 @@ export async function handleShortcutAction(action: string) {
|
|||||||
const playerStore = usePlayerStore();
|
const playerStore = usePlayerStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
|
||||||
const currentSound = audioService.getCurrentSound();
|
|
||||||
const showToast = (message: string, iconName: string) => {
|
const showToast = (message: string, iconName: string) => {
|
||||||
if (settingsStore.isMiniMode) {
|
if (settingsStore.isMiniMode) {
|
||||||
return;
|
return;
|
||||||
@@ -95,19 +94,25 @@ export async function handleShortcutAction(action: string) {
|
|||||||
showToast(t('player.playBar.next'), 'ri-skip-forward-line');
|
showToast(t('player.playBar.next'), 'ri-skip-forward-line');
|
||||||
break;
|
break;
|
||||||
case 'volumeUp':
|
case 'volumeUp':
|
||||||
if (currentSound && currentSound?.volume() < 1) {
|
// 从localStorage获取当前音量
|
||||||
currentSound?.volume((currentSound?.volume() || 0) + 0.1);
|
const currentVolumeUp = parseFloat(localStorage.getItem('volume') || '1');
|
||||||
|
if (currentVolumeUp < 1) {
|
||||||
|
const newVolume = Math.min(1, currentVolumeUp + 0.1);
|
||||||
|
await audioService.setVolume(newVolume);
|
||||||
showToast(
|
showToast(
|
||||||
`${t('player.playBar.volume')}${Math.round((currentSound?.volume() || 0) * 100)}%`,
|
`${t('player.playBar.volume')}${Math.round(newVolume * 100)}%`,
|
||||||
'ri-volume-up-line'
|
'ri-volume-up-line'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'volumeDown':
|
case 'volumeDown':
|
||||||
if (currentSound && currentSound?.volume() > 0) {
|
// 从localStorage获取当前音量
|
||||||
currentSound?.volume((currentSound?.volume() || 0) - 0.1);
|
const currentVolumeDown = parseFloat(localStorage.getItem('volume') || '1');
|
||||||
|
if (currentVolumeDown > 0) {
|
||||||
|
const newVolume = Math.max(0, currentVolumeDown - 0.1);
|
||||||
|
await audioService.setVolume(newVolume);
|
||||||
showToast(
|
showToast(
|
||||||
`${t('player.playBar.volume')}${Math.round((currentSound?.volume() || 0) * 100)}%`,
|
`${t('player.playBar.volume')}${Math.round(newVolume * 100)}%`,
|
||||||
'ri-volume-down-line'
|
'ri-volume-down-line'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,7 +171,6 @@
|
|||||||
<a class="text-green-400 hover:text-green-500" href="https://music.163.com/store/vip" target="_blank">网易云音乐会员</a>
|
<a class="text-green-400 hover:text-green-500" href="https://music.163.com/store/vip" target="_blank">网易云音乐会员</a>
|
||||||
<a class="text-green-400 hover:text-green-500" href="https://y.qq.com/portal/vipportal/" target="_blank">QQ音乐会员</a>
|
<a class="text-green-400 hover:text-green-500" href="https://y.qq.com/portal/vipportal/" target="_blank">QQ音乐会员</a>
|
||||||
<a class="text-green-400 hover:text-green-500" href="https://vip.kugou.com/" target="_blank">酷狗音乐会员</a>
|
<a class="text-green-400 hover:text-green-500" href="https://vip.kugou.com/" target="_blank">酷狗音乐会员</a>
|
||||||
<a class="text-green-400 hover:text-green-500" href="https://vip1.kuwo.cn/" target="_blank">酷我音乐会员</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -522,7 +521,7 @@ import { type Platform } from '@/types/music';
|
|||||||
import config from '../../../../package.json';
|
import config from '../../../../package.json';
|
||||||
|
|
||||||
// 所有平台默认值
|
// 所有平台默认值
|
||||||
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo'];
|
const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili'];
|
||||||
|
|
||||||
const platform = window.electron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';
|
const platform = window.electron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user