Compare commits
	
		
			4 Commits
		
	
	
		
			main
			...
			feature/ne
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 456a0d33ec | |||
| 4f0b897b4c | |||
| f1fb5330a9 | |||
| fd6253e626 | 
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| 
						 | 
					@ -8,6 +8,7 @@
 | 
				
			||||||
			"name": "msr-mod",
 | 
								"name": "msr-mod",
 | 
				
			||||||
			"version": "0.0.0",
 | 
								"version": "0.0.0",
 | 
				
			||||||
			"dependencies": {
 | 
								"dependencies": {
 | 
				
			||||||
 | 
									"@astrian/music-surge-revolution": "^0.0.0-20250831052313",
 | 
				
			||||||
				"@tailwindcss/vite": "^4.1.7",
 | 
									"@tailwindcss/vite": "^4.1.7",
 | 
				
			||||||
				"axios": "^1.9.0",
 | 
									"axios": "^1.9.0",
 | 
				
			||||||
				"gsap": "^3.13.0",
 | 
									"gsap": "^3.13.0",
 | 
				
			||||||
| 
						 | 
					@ -42,6 +43,11 @@
 | 
				
			||||||
				"node": ">=6.0.0"
 | 
									"node": ">=6.0.0"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							"node_modules/@astrian/music-surge-revolution": {
 | 
				
			||||||
 | 
								"version": "0.0.0-20250903052637",
 | 
				
			||||||
 | 
								"resolved": "https://registry.npmjs.org/@astrian/music-surge-revolution/-/music-surge-revolution-0.0.0-20250903052637.tgz",
 | 
				
			||||||
 | 
								"integrity": "sha512-P/cuDEseY1Q/UU5NAcbi53vYGEsC/mlM6If7+gjXqayMiOrTdmHTPypp1A0CrsodAsR0NtUohbeaZHCHgGAd/A=="
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		"node_modules/@babel/helper-string-parser": {
 | 
							"node_modules/@babel/helper-string-parser": {
 | 
				
			||||||
			"version": "7.27.1",
 | 
								"version": "7.27.1",
 | 
				
			||||||
			"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
 | 
								"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@
 | 
				
			||||||
		"prebuild:safari": "node scripts/prebuild-safari.js"
 | 
							"prebuild:safari": "node scripts/prebuild-safari.js"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"dependencies": {
 | 
						"dependencies": {
 | 
				
			||||||
 | 
							"@astrian/music-surge-revolution": "^0.0.0-20250831052313",
 | 
				
			||||||
		"@tailwindcss/vite": "^4.1.7",
 | 
							"@tailwindcss/vite": "^4.1.7",
 | 
				
			||||||
		"axios": "^1.9.0",
 | 
							"axios": "^1.9.0",
 | 
				
			||||||
		"gsap": "^3.13.0",
 | 
							"gsap": "^3.13.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
console.log("aaaa")
 | 
					console.log('aaaa')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 兼容 Chrome 和 Firefox
 | 
					// 兼容 Chrome 和 Firefox
 | 
				
			||||||
const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
 | 
					const browserAPI = typeof browser !== 'undefined' ? browser : chrome
 | 
				
			||||||
 | 
					
 | 
				
			||||||
browserAPI.webRequest.onBeforeRequest.addListener(
 | 
					browserAPI.webRequest.onBeforeRequest.addListener(
 | 
				
			||||||
	async (details) => {
 | 
						async (details) => {
 | 
				
			||||||
| 
						 | 
					@ -16,12 +16,18 @@ browserAPI.webRequest.onBeforeRequest.addListener(
 | 
				
			||||||
		console.log('recived request for fontset api, redirecting to index.html')
 | 
							console.log('recived request for fontset api, redirecting to index.html')
 | 
				
			||||||
		const pref = await browserAPI.storage.sync.get('preferences')
 | 
							const pref = await browserAPI.storage.sync.get('preferences')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (pref === undefined || pref.preferences === undefined || pref.preferences.autoRedirect === undefined || pref.preferences.autoRedirect === true) {
 | 
							if (
 | 
				
			||||||
			const isChrome = typeof browserAPI.runtime.getBrowserInfo === 'undefined';
 | 
								pref === undefined ||
 | 
				
			||||||
 | 
								pref.preferences === undefined ||
 | 
				
			||||||
 | 
								pref.preferences.autoRedirect === undefined ||
 | 
				
			||||||
 | 
								pref.preferences.autoRedirect === true
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								const isChrome = typeof browserAPI.runtime.getBrowserInfo === 'undefined'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (isChrome) {
 | 
								if (isChrome) {
 | 
				
			||||||
				if (
 | 
									if (
 | 
				
			||||||
					details.url === 'https://monster-siren.hypergryph.com/manifest.json' &&
 | 
										details.url ===
 | 
				
			||||||
 | 
											'https://monster-siren.hypergryph.com/manifest.json' &&
 | 
				
			||||||
					details.type === 'other' &&
 | 
										details.type === 'other' &&
 | 
				
			||||||
					details.frameId === 0
 | 
										details.frameId === 0
 | 
				
			||||||
				) {
 | 
									) {
 | 
				
			||||||
| 
						 | 
					@ -32,15 +38,22 @@ browserAPI.webRequest.onBeforeRequest.addListener(
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				// Firefox: 直接在当前标签页导航
 | 
									// Firefox: 直接在当前标签页导航
 | 
				
			||||||
				browserAPI.tabs.update(details.tabId, { url: browserAPI.runtime.getURL('index.html') })
 | 
									browserAPI.tabs.update(details.tabId, {
 | 
				
			||||||
 | 
										url: browserAPI.runtime.getURL('index.html'),
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json'] },
 | 
						{
 | 
				
			||||||
 | 
							urls: [
 | 
				
			||||||
 | 
								'https://monster-siren.hypergryph.com/api/fontset',
 | 
				
			||||||
 | 
								'https://monster-siren.hypergryph.com/manifest.json',
 | 
				
			||||||
 | 
							],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 兼容新旧版本的 API
 | 
					// 兼容新旧版本的 API
 | 
				
			||||||
const actionAPI = browserAPI.action || browserAPI.browserAction;
 | 
					const actionAPI = browserAPI.action || browserAPI.browserAction
 | 
				
			||||||
if (actionAPI) {
 | 
					if (actionAPI) {
 | 
				
			||||||
	actionAPI.onClicked.addListener(() => {
 | 
						actionAPI.onClicked.addListener(() => {
 | 
				
			||||||
		browserAPI.tabs.create({ url: browserAPI.runtime.getURL('index.html') })
 | 
							browserAPI.tabs.create({ url: browserAPI.runtime.getURL('index.html') })
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,12 +5,8 @@
 | 
				
			||||||
	"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
 | 
						"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
 | 
				
			||||||
	"content_scripts": [
 | 
						"content_scripts": [
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			"matches": [
 | 
								"matches": ["https://monster-siren.hypergryph.com/"],
 | 
				
			||||||
				"https://monster-siren.hypergryph.com/"
 | 
								"js": ["content.js"],
 | 
				
			||||||
			],
 | 
					 | 
				
			||||||
			"js": [
 | 
					 | 
				
			||||||
				"content.js"
 | 
					 | 
				
			||||||
			],
 | 
					 | 
				
			||||||
			"run_at": "document_end"
 | 
								"run_at": "document_end"
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
| 
						 | 
					@ -36,11 +32,7 @@
 | 
				
			||||||
	"background": {
 | 
						"background": {
 | 
				
			||||||
		"service_worker": "background.js"
 | 
							"service_worker": "background.js"
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	"permissions": [
 | 
						"permissions": ["tabs", "webRequest", "storage"],
 | 
				
			||||||
		"tabs",
 | 
					 | 
				
			||||||
		"webRequest",
 | 
					 | 
				
			||||||
		"storage"
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
	"content_security_policy": {
 | 
						"content_security_policy": {
 | 
				
			||||||
		"extension_pages": "default-src 'self'; script-src 'self' http://localhost:5173; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:5173 https://monster-siren.hypergryph.com https://web.hycdn.cn https://res01.hycdn.cn; img-src 'self' https://web.hycdn.cn; media-src 'self' https://res01.hycdn.cn;",
 | 
							"extension_pages": "default-src 'self'; script-src 'self' http://localhost:5173; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:5173 https://monster-siren.hypergryph.com https://web.hycdn.cn https://res01.hycdn.cn; img-src 'self' https://web.hycdn.cn; media-src 'self' https://res01.hycdn.cn;",
 | 
				
			||||||
		"sandbox": "sandbox"
 | 
							"sandbox": "sandbox"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,61 +1,65 @@
 | 
				
			||||||
import fs from 'fs';
 | 
					import fs from 'fs'
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path'
 | 
				
			||||||
import { fileURLToPath } from 'url';
 | 
					import { fileURLToPath } from 'url'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const __filename = fileURLToPath(import.meta.url);
 | 
					const __filename = fileURLToPath(import.meta.url)
 | 
				
			||||||
const __dirname = path.dirname(__filename);
 | 
					const __dirname = path.dirname(__filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理 manifest.json
 | 
					// 处理 manifest.json
 | 
				
			||||||
function processManifest() {
 | 
					function processManifest() {
 | 
				
			||||||
  const manifestPath = path.join(__dirname, '../public/manifest.json');
 | 
						const manifestPath = path.join(__dirname, '../public/manifest.json')
 | 
				
			||||||
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
 | 
						const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除本地调试相关的配置
 | 
						// 移除本地调试相关的配置
 | 
				
			||||||
	if (manifest.host_permissions) {
 | 
						if (manifest.host_permissions) {
 | 
				
			||||||
		manifest.host_permissions = manifest.host_permissions.filter(
 | 
							manifest.host_permissions = manifest.host_permissions.filter(
 | 
				
			||||||
      permission => !permission.includes('localhost')
 | 
								(permission) => !permission.includes('localhost'),
 | 
				
			||||||
    );
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) {
 | 
						if (
 | 
				
			||||||
 | 
							manifest.content_security_policy &&
 | 
				
			||||||
 | 
							manifest.content_security_policy.extension_pages
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
		// 移除 CSP 中的本地开发相关配置
 | 
							// 移除 CSP 中的本地开发相关配置
 | 
				
			||||||
    manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages
 | 
							manifest.content_security_policy.extension_pages =
 | 
				
			||||||
 | 
								manifest.content_security_policy.extension_pages
 | 
				
			||||||
				.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
 | 
									.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
 | 
				
			||||||
				.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
									.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
				
			||||||
				.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
									.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
				
			||||||
				.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
									.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
				
			||||||
				.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
									.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
				
			||||||
      .trim();
 | 
									.trim()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
						fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 | 
				
			||||||
  console.log('✅ Manifest.json processed');
 | 
						console.log('✅ Manifest.json processed')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理 index.html
 | 
					// 处理 index.html
 | 
				
			||||||
function processIndexHtml() {
 | 
					function processIndexHtml() {
 | 
				
			||||||
  const indexPath = path.join(__dirname, '../index.html');
 | 
						const indexPath = path.join(__dirname, '../index.html')
 | 
				
			||||||
  let content = fs.readFileSync(indexPath, 'utf8');
 | 
						let content = fs.readFileSync(indexPath, 'utf8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 替换脚本地址
 | 
						// 替换脚本地址
 | 
				
			||||||
	content = content.replace(
 | 
						content = content.replace(
 | 
				
			||||||
		/src="[^"]*\/src\/main\.ts"/g,
 | 
							/src="[^"]*\/src\/main\.ts"/g,
 | 
				
			||||||
    'src="./src/main.ts"'
 | 
							'src="./src/main.ts"',
 | 
				
			||||||
  );
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除 crossorigin 属性
 | 
						// 移除 crossorigin 属性
 | 
				
			||||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
						content = content.replace(/\s+crossorigin/g, '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fs.writeFileSync(indexPath, content);
 | 
						fs.writeFileSync(indexPath, content)
 | 
				
			||||||
  console.log('✅ Index.html processed');
 | 
						console.log('✅ Index.html processed')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 执行处理
 | 
					// 执行处理
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
  processManifest();
 | 
						processManifest()
 | 
				
			||||||
  processIndexHtml();
 | 
						processIndexHtml()
 | 
				
			||||||
  console.log('🎉 Build preparation completed!');
 | 
						console.log('🎉 Build preparation completed!')
 | 
				
			||||||
} catch (error) {
 | 
					} catch (error) {
 | 
				
			||||||
  console.error('❌ Error during build preparation:', error);
 | 
						console.error('❌ Error during build preparation:', error)
 | 
				
			||||||
  process.exit(1);
 | 
						process.exit(1)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,42 +1,49 @@
 | 
				
			||||||
import fs from 'fs';
 | 
					import fs from 'fs'
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path'
 | 
				
			||||||
import { fileURLToPath } from 'url';
 | 
					import { fileURLToPath } from 'url'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const __filename = fileURLToPath(import.meta.url);
 | 
					const __filename = fileURLToPath(import.meta.url)
 | 
				
			||||||
const __dirname = path.dirname(__filename);
 | 
					const __dirname = path.dirname(__filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理 manifest.json
 | 
					// 处理 manifest.json
 | 
				
			||||||
function processManifest() {
 | 
					function processManifest() {
 | 
				
			||||||
  const manifestPath = path.join(__dirname, '../public/manifest.json');
 | 
						const manifestPath = path.join(__dirname, '../public/manifest.json')
 | 
				
			||||||
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
 | 
						const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除本地调试相关的配置
 | 
						// 移除本地调试相关的配置
 | 
				
			||||||
	if (manifest.host_permissions) {
 | 
						if (manifest.host_permissions) {
 | 
				
			||||||
		manifest.host_permissions = manifest.host_permissions.filter(
 | 
							manifest.host_permissions = manifest.host_permissions.filter(
 | 
				
			||||||
      permission => !permission.includes('localhost')
 | 
								(permission) => !permission.includes('localhost'),
 | 
				
			||||||
    );
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) {
 | 
						if (
 | 
				
			||||||
 | 
							manifest.content_security_policy &&
 | 
				
			||||||
 | 
							manifest.content_security_policy.extension_pages
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
		// 移除 CSP 中的本地开发相关配置
 | 
							// 移除 CSP 中的本地开发相关配置
 | 
				
			||||||
    manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages
 | 
							manifest.content_security_policy.extension_pages =
 | 
				
			||||||
 | 
								manifest.content_security_policy.extension_pages
 | 
				
			||||||
				.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
 | 
									.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
 | 
				
			||||||
				.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
									.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
				
			||||||
				.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
									.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
				
			||||||
				.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
									.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
				
			||||||
				.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
									.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
				
			||||||
      .trim();
 | 
									.trim()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除 CSP 中的 sandbox 配置(Firefox 不支持)
 | 
						// 移除 CSP 中的 sandbox 配置(Firefox 不支持)
 | 
				
			||||||
  if (manifest.content_security_policy && manifest.content_security_policy.sandbox) {
 | 
						if (
 | 
				
			||||||
    delete manifest.content_security_policy.sandbox;
 | 
							manifest.content_security_policy &&
 | 
				
			||||||
 | 
							manifest.content_security_policy.sandbox
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							delete manifest.content_security_policy.sandbox
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除 background.service_worker,替换为 background.scripts
 | 
						// 移除 background.service_worker,替换为 background.scripts
 | 
				
			||||||
	if (manifest.background && manifest.background.service_worker) {
 | 
						if (manifest.background && manifest.background.service_worker) {
 | 
				
			||||||
    manifest.background.scripts = [manifest.background.service_worker];
 | 
							manifest.background.scripts = [manifest.background.service_worker]
 | 
				
			||||||
    delete manifest.background.service_worker;
 | 
							delete manifest.background.service_worker
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 添加 firefox 特有配置
 | 
						// 添加 firefox 特有配置
 | 
				
			||||||
| 
						 | 
					@ -44,37 +51,37 @@ function processManifest() {
 | 
				
			||||||
		gecko: {
 | 
							gecko: {
 | 
				
			||||||
			id: 'msr-mod@firefox-addon.astrian.moe',
 | 
								id: 'msr-mod@firefox-addon.astrian.moe',
 | 
				
			||||||
			strict_min_version: '115.0',
 | 
								strict_min_version: '115.0',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
						fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 | 
				
			||||||
  console.log('✅ Manifest.json processed');
 | 
						console.log('✅ Manifest.json processed')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理 index.html
 | 
					// 处理 index.html
 | 
				
			||||||
function processIndexHtml() {
 | 
					function processIndexHtml() {
 | 
				
			||||||
  const indexPath = path.join(__dirname, '../index.html');
 | 
						const indexPath = path.join(__dirname, '../index.html')
 | 
				
			||||||
  let content = fs.readFileSync(indexPath, 'utf8');
 | 
						let content = fs.readFileSync(indexPath, 'utf8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 替换脚本地址
 | 
						// 替换脚本地址
 | 
				
			||||||
	content = content.replace(
 | 
						content = content.replace(
 | 
				
			||||||
		/src="[^"]*\/src\/main\.ts"/g,
 | 
							/src="[^"]*\/src\/main\.ts"/g,
 | 
				
			||||||
    'src="./src/main.ts"'
 | 
							'src="./src/main.ts"',
 | 
				
			||||||
  );
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除 crossorigin 属性
 | 
						// 移除 crossorigin 属性
 | 
				
			||||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
						content = content.replace(/\s+crossorigin/g, '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fs.writeFileSync(indexPath, content);
 | 
						fs.writeFileSync(indexPath, content)
 | 
				
			||||||
  console.log('✅ Index.html processed');
 | 
						console.log('✅ Index.html processed')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 执行处理
 | 
					// 执行处理
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
  processManifest();
 | 
						processManifest()
 | 
				
			||||||
  processIndexHtml();
 | 
						processIndexHtml()
 | 
				
			||||||
  console.log('🎉 Build preparation completed!');
 | 
						console.log('🎉 Build preparation completed!')
 | 
				
			||||||
} catch (error) {
 | 
					} catch (error) {
 | 
				
			||||||
  console.error('❌ Error during build preparation:', error);
 | 
						console.error('❌ Error during build preparation:', error)
 | 
				
			||||||
  process.exit(1);
 | 
						process.exit(1)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,39 +1,43 @@
 | 
				
			||||||
import fs from 'fs';
 | 
					import fs from 'fs'
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path'
 | 
				
			||||||
import { fileURLToPath } from 'url';
 | 
					import { fileURLToPath } from 'url'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const __filename = fileURLToPath(import.meta.url);
 | 
					const __filename = fileURLToPath(import.meta.url)
 | 
				
			||||||
const __dirname = path.dirname(__filename);
 | 
					const __dirname = path.dirname(__filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理 manifest.json for Safari
 | 
					// 处理 manifest.json for Safari
 | 
				
			||||||
function processManifest() {
 | 
					function processManifest() {
 | 
				
			||||||
  const manifestPath = path.join(__dirname, '../public/manifest.json');
 | 
						const manifestPath = path.join(__dirname, '../public/manifest.json')
 | 
				
			||||||
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
 | 
						const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除本地调试相关的配置
 | 
						// 移除本地调试相关的配置
 | 
				
			||||||
	if (manifest.host_permissions) {
 | 
						if (manifest.host_permissions) {
 | 
				
			||||||
		manifest.host_permissions = manifest.host_permissions.filter(
 | 
							manifest.host_permissions = manifest.host_permissions.filter(
 | 
				
			||||||
      permission => !permission.includes('localhost')
 | 
								(permission) => !permission.includes('localhost'),
 | 
				
			||||||
    );
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) {
 | 
						if (
 | 
				
			||||||
 | 
							manifest.content_security_policy &&
 | 
				
			||||||
 | 
							manifest.content_security_policy.extension_pages
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
		// 移除 CSP 中的本地开发相关配置
 | 
							// 移除 CSP 中的本地开发相关配置
 | 
				
			||||||
    manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages
 | 
							manifest.content_security_policy.extension_pages =
 | 
				
			||||||
 | 
								manifest.content_security_policy.extension_pages
 | 
				
			||||||
				.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
 | 
									.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
 | 
				
			||||||
				.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
									.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
				
			||||||
				.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
									.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
				
			||||||
				.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
									.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
				
			||||||
				.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
									.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
				
			||||||
      .trim();
 | 
									.trim()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Safari 特殊处理:添加 appShell.html 到 content scripts 匹配
 | 
						// Safari 特殊处理:添加 appShell.html 到 content scripts 匹配
 | 
				
			||||||
	if (manifest.content_scripts && manifest.content_scripts[0]) {
 | 
						if (manifest.content_scripts && manifest.content_scripts[0]) {
 | 
				
			||||||
		// 添加 appShell.html 的匹配规则
 | 
							// 添加 appShell.html 的匹配规则
 | 
				
			||||||
    const existingMatches = manifest.content_scripts[0].matches;
 | 
							const existingMatches = manifest.content_scripts[0].matches
 | 
				
			||||||
    if (!existingMatches.includes("https://monster-siren.hypergryph.com/")) {
 | 
							if (!existingMatches.includes('https://monster-siren.hypergryph.com/')) {
 | 
				
			||||||
      existingMatches.push("https://monster-siren.hypergryph.com/");
 | 
								existingMatches.push('https://monster-siren.hypergryph.com/')
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,13 +46,13 @@ function processManifest() {
 | 
				
			||||||
		// Safari 扩展在 Manifest V3 中必须使用 persistent: false
 | 
							// Safari 扩展在 Manifest V3 中必须使用 persistent: false
 | 
				
			||||||
		// 但为了调试,我们暂时设为 true 来确保页面加载
 | 
							// 但为了调试,我们暂时设为 true 来确保页面加载
 | 
				
			||||||
		manifest.background = {
 | 
							manifest.background = {
 | 
				
			||||||
      page: "background.html",
 | 
								page: 'background.html',
 | 
				
			||||||
      persistent: true
 | 
								persistent: true,
 | 
				
			||||||
    };
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 创建 background.html 文件用于 Safari
 | 
						// 创建 background.html 文件用于 Safari
 | 
				
			||||||
  const backgroundHtmlPath = path.join(__dirname, '../public/background.html');
 | 
						const backgroundHtmlPath = path.join(__dirname, '../public/background.html')
 | 
				
			||||||
	const backgroundHtmlContent = `<!DOCTYPE html>
 | 
						const backgroundHtmlContent = `<!DOCTYPE html>
 | 
				
			||||||
<html>
 | 
					<html>
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
| 
						 | 
					@ -102,16 +106,16 @@ function processManifest() {
 | 
				
			||||||
    log('=== After background.js script tag ===');
 | 
					    log('=== After background.js script tag ===');
 | 
				
			||||||
  </script>
 | 
					  </script>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
</html>`;
 | 
					</html>`
 | 
				
			||||||
  fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent);
 | 
						fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 创建 Safari 兼容的 background.js
 | 
						// 创建 Safari 兼容的 background.js
 | 
				
			||||||
  const backgroundJsPath = path.join(__dirname, '../public/background.js');
 | 
						const backgroundJsPath = path.join(__dirname, '../public/background.js')
 | 
				
			||||||
  let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8');
 | 
						let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检查是否已经添加过 Safari 代码,避免重复
 | 
						// 检查是否已经添加过 Safari 代码,避免重复
 | 
				
			||||||
	if (backgroundJsContent.includes('=== Safari background.js starting ===')) {
 | 
						if (backgroundJsContent.includes('=== Safari background.js starting ===')) {
 | 
				
			||||||
    console.log('Safari background.js already processed, skipping...');
 | 
							console.log('Safari background.js already processed, skipping...')
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// 在开头添加 Safari 调试信息(只添加一次)
 | 
							// 在开头添加 Safari 调试信息(只添加一次)
 | 
				
			||||||
		const safariDebugCode = `
 | 
							const safariDebugCode = `
 | 
				
			||||||
| 
						 | 
					@ -168,38 +172,40 @@ if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage)
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`;
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 替换 Safari 的重定向 URL 监听
 | 
							// 替换 Safari 的重定向 URL 监听
 | 
				
			||||||
		backgroundJsContent = backgroundJsContent.replace(
 | 
							backgroundJsContent = backgroundJsContent.replace(
 | 
				
			||||||
			/{ urls: \['https:\/\/monster-siren\.hypergryph\.com\/api\/fontset', 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'\] }/g,
 | 
								/{ urls: \['https:\/\/monster-siren\.hypergryph\.com\/api\/fontset', 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'\] }/g,
 | 
				
			||||||
      "{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json', 'https://monster-siren.hypergryph.com/'] }"
 | 
								"{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json', 'https://monster-siren.hypergryph.com/'] }",
 | 
				
			||||||
    );
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 替换 Safari 的重定向判断逻辑
 | 
							// 替换 Safari 的重定向判断逻辑
 | 
				
			||||||
		backgroundJsContent = backgroundJsContent.replace(
 | 
							backgroundJsContent = backgroundJsContent.replace(
 | 
				
			||||||
			/details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'/g,
 | 
								/details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'/g,
 | 
				
			||||||
      "(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')"
 | 
								"(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')",
 | 
				
			||||||
    );
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 清理可能的重复条件
 | 
							// 清理可能的重复条件
 | 
				
			||||||
		backgroundJsContent = backgroundJsContent.replace(
 | 
							backgroundJsContent = backgroundJsContent.replace(
 | 
				
			||||||
			/\(\(details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json' \|\| details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/'\) \|\| details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/'\)/g,
 | 
								/\(\(details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json' \|\| details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/'\) \|\| details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/'\)/g,
 | 
				
			||||||
      "(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')"
 | 
								"(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')",
 | 
				
			||||||
    );
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    backgroundJsContent = safariDebugCode + backgroundJsContent;
 | 
							backgroundJsContent = safariDebugCode + backgroundJsContent
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
  fs.writeFileSync(backgroundJsPath, backgroundJsContent);
 | 
						fs.writeFileSync(backgroundJsPath, backgroundJsContent)
 | 
				
			||||||
  console.log('✅ Safari-compatible background.js created');
 | 
						console.log('✅ Safari-compatible background.js created')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 创建 Safari 专用的 content.js
 | 
						// 创建 Safari 专用的 content.js
 | 
				
			||||||
  const contentJsPath = path.join(__dirname, '../public/content.js');
 | 
						const contentJsPath = path.join(__dirname, '../public/content.js')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检查是否已经处理过 content.js
 | 
						// 检查是否已经处理过 content.js
 | 
				
			||||||
  const existingContentJs = fs.existsSync(contentJsPath) ? fs.readFileSync(contentJsPath, 'utf8') : '';
 | 
						const existingContentJs = fs.existsSync(contentJsPath)
 | 
				
			||||||
 | 
							? fs.readFileSync(contentJsPath, 'utf8')
 | 
				
			||||||
 | 
							: ''
 | 
				
			||||||
	if (existingContentJs.includes('checkRedirectPreference')) {
 | 
						if (existingContentJs.includes('checkRedirectPreference')) {
 | 
				
			||||||
    console.log('Safari content.js already processed, skipping...');
 | 
							console.log('Safari content.js already processed, skipping...')
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		const contentJsContent = `
 | 
							const contentJsContent = `
 | 
				
			||||||
// Safari 扩展 content script for redirect
 | 
					// Safari 扩展 content script for redirect
 | 
				
			||||||
| 
						 | 
					@ -307,53 +313,53 @@ async function main() {
 | 
				
			||||||
main().catch(error => {
 | 
					main().catch(error => {
 | 
				
			||||||
  console.error('Error in main function:', error);
 | 
					  console.error('Error in main function:', error);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
`;
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fs.writeFileSync(contentJsPath, contentJsContent);
 | 
							fs.writeFileSync(contentJsPath, contentJsContent)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
  console.log('✅ Safari-compatible content.js created');
 | 
						console.log('✅ Safari-compatible content.js created')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Safari 可能需要额外的权限
 | 
						// Safari 可能需要额外的权限
 | 
				
			||||||
	if (!manifest.permissions.includes('activeTab')) {
 | 
						if (!manifest.permissions.includes('activeTab')) {
 | 
				
			||||||
    manifest.permissions.push('activeTab');
 | 
							manifest.permissions.push('activeTab')
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 添加 Safari 特有配置
 | 
						// 添加 Safari 特有配置
 | 
				
			||||||
	manifest.browser_specific_settings = {
 | 
						manifest.browser_specific_settings = {
 | 
				
			||||||
		safari: {
 | 
							safari: {
 | 
				
			||||||
      minimum_version: "14.0"
 | 
								minimum_version: '14.0',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
						fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 | 
				
			||||||
  console.log('✅ Safari Manifest.json processed');
 | 
						console.log('✅ Safari Manifest.json processed')
 | 
				
			||||||
  console.log('✅ Background.html created for Safari');
 | 
						console.log('✅ Background.html created for Safari')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理 index.html
 | 
					// 处理 index.html
 | 
				
			||||||
function processIndexHtml() {
 | 
					function processIndexHtml() {
 | 
				
			||||||
  const indexPath = path.join(__dirname, '../index.html');
 | 
						const indexPath = path.join(__dirname, '../index.html')
 | 
				
			||||||
  let content = fs.readFileSync(indexPath, 'utf8');
 | 
						let content = fs.readFileSync(indexPath, 'utf8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 替换脚本地址
 | 
						// 替换脚本地址
 | 
				
			||||||
	content = content.replace(
 | 
						content = content.replace(
 | 
				
			||||||
		/src="[^"]*\/src\/main\.ts"/g,
 | 
							/src="[^"]*\/src\/main\.ts"/g,
 | 
				
			||||||
    'src="./src/main.ts"'
 | 
							'src="./src/main.ts"',
 | 
				
			||||||
  );
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 移除 crossorigin 属性
 | 
						// 移除 crossorigin 属性
 | 
				
			||||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
						content = content.replace(/\s+crossorigin/g, '')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  fs.writeFileSync(indexPath, content);
 | 
						fs.writeFileSync(indexPath, content)
 | 
				
			||||||
  console.log('✅ Index.html processed for Safari');
 | 
						console.log('✅ Index.html processed for Safari')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 执行处理
 | 
					// 执行处理
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
  processManifest();
 | 
						processManifest()
 | 
				
			||||||
  processIndexHtml();
 | 
						processIndexHtml()
 | 
				
			||||||
  console.log('🎉 Safari build preparation completed!');
 | 
						console.log('🎉 Safari build preparation completed!')
 | 
				
			||||||
} catch (error) {
 | 
					} catch (error) {
 | 
				
			||||||
  console.error('❌ Error during Safari build preparation:', error);
 | 
						console.error('❌ Error during Safari build preparation:', error)
 | 
				
			||||||
  process.exit(1);
 | 
						process.exit(1)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										13
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/App.vue
									
									
									
									
									
								
							| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { useRoute, useRouter } from 'vue-router'
 | 
					import { useRoute, useRouter } from 'vue-router'
 | 
				
			||||||
import Player from './components/Player.vue'
 | 
					 | 
				
			||||||
import PreferencePanel from './components/PreferencePanel.vue'
 | 
					import PreferencePanel from './components/PreferencePanel.vue'
 | 
				
			||||||
import { ref } from 'vue'
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,16 +9,19 @@ import CorgIcon from './assets/icons/corg.vue'
 | 
				
			||||||
import { watch } from 'vue'
 | 
					import { watch } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import UpdatePopup from './components/UpdatePopup.vue'
 | 
					import UpdatePopup from './components/UpdatePopup.vue'
 | 
				
			||||||
 | 
					import MiniPlayer from './components/MiniPlayer.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const presentPreferencePanel = ref(false)
 | 
					const presentPreferencePanel = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const route = useRoute()
 | 
					const route = useRoute()
 | 
				
			||||||
const router = useRouter()
 | 
					const router = useRouter()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(() => presentPreferencePanel, (value) => {
 | 
					watch(
 | 
				
			||||||
 | 
						() => presentPreferencePanel,
 | 
				
			||||||
 | 
						(value) => {
 | 
				
			||||||
		console.log(value)
 | 
							console.log(value)
 | 
				
			||||||
})
 | 
						},
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					@ -72,8 +74,7 @@ watch(() => presentPreferencePanel, (value) => {
 | 
				
			||||||
							@click="presentPreferencePanel = true">
 | 
												@click="presentPreferencePanel = true">
 | 
				
			||||||
							<CorgIcon :size="4" />
 | 
												<CorgIcon :size="4" />
 | 
				
			||||||
						</button>
 | 
											</button>
 | 
				
			||||||
 | 
											<MiniPlayer />
 | 
				
			||||||
						<Player />
 | 
					 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,33 +9,41 @@ export default {
 | 
				
			||||||
		const songs: {
 | 
							const songs: {
 | 
				
			||||||
			data: ApiResponse
 | 
								data: ApiResponse
 | 
				
			||||||
		} = await msrInstance.get('songs')
 | 
							} = await msrInstance.get('songs')
 | 
				
			||||||
		if (songs.data.code !== 0) { throw new Error(`Cannot get songs: ${songs.data.msg}`) }
 | 
							if (songs.data.code !== 0) {
 | 
				
			||||||
 | 
								throw new Error(`Cannot get songs: ${songs.data.msg}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return { songs: songs.data.data as { list: SongList } }
 | 
							return { songs: songs.data.data as { list: SongList } }
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	async getSong(cid: string) {
 | 
						async getSong(cid: string) {
 | 
				
			||||||
		const song: {
 | 
							const song: {
 | 
				
			||||||
			data: ApiResponse
 | 
								data: ApiResponse
 | 
				
			||||||
		} = await msrInstance.get(`song/${cid}`)
 | 
							} = await msrInstance.get(`song/${cid}`)
 | 
				
			||||||
		if (song.data.code!== 0) { throw new Error(`Cannot get song: ${song.data.msg}`) }
 | 
							if (song.data.code !== 0) {
 | 
				
			||||||
 | 
								throw new Error(`Cannot get song: ${song.data.msg}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return song.data.data as Song
 | 
							return song.data.data as Song
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	async getAlbums() {
 | 
						async getAlbums() {
 | 
				
			||||||
		const albums: {
 | 
							const albums: {
 | 
				
			||||||
			data: ApiResponse
 | 
								data: ApiResponse
 | 
				
			||||||
		} = await msrInstance.get('albums')
 | 
							} = await msrInstance.get('albums')
 | 
				
			||||||
		if (albums.data.code!== 0) { throw new Error(`Cannot get albums: ${albums.data.msg}`) }
 | 
							if (albums.data.code !== 0) {
 | 
				
			||||||
 | 
								throw new Error(`Cannot get albums: ${albums.data.msg}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return albums.data.data as AlbumList
 | 
							return albums.data.data as AlbumList
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	async getAlbum(cid: string) {
 | 
						async getAlbum(cid: string) {
 | 
				
			||||||
		const album: {
 | 
							const album: {
 | 
				
			||||||
			data: ApiResponse
 | 
								data: ApiResponse
 | 
				
			||||||
		} = await msrInstance.get(`album/${cid}/detail`)
 | 
							} = await msrInstance.get(`album/${cid}/detail`)
 | 
				
			||||||
		if (album.data.code!== 0) { throw new Error(`Cannot get album: ${album.data.msg}`) }
 | 
							if (album.data.code !== 0) {
 | 
				
			||||||
 | 
								throw new Error(`Cannot get album: ${album.data.msg}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		const albumMeta: {
 | 
							const albumMeta: {
 | 
				
			||||||
			data: ApiResponse
 | 
								data: ApiResponse
 | 
				
			||||||
		} = await msrInstance.get(`album/${cid}/data`)
 | 
							} = await msrInstance.get(`album/${cid}/data`)
 | 
				
			||||||
		let data = album.data.data as Album
 | 
							let data = album.data.data as Album
 | 
				
			||||||
		data.artistes = (albumMeta.data.data as Album).artistes
 | 
							data.artistes = (albumMeta.data.data as Album).artistes
 | 
				
			||||||
		return data
 | 
							return data
 | 
				
			||||||
	}
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ import { ref, watch, nextTick } from 'vue'
 | 
				
			||||||
import { gsap } from 'gsap'
 | 
					import { gsap } from 'gsap'
 | 
				
			||||||
import apis from '../apis'
 | 
					import apis from '../apis'
 | 
				
			||||||
import { artistsOrganize } from '../utils'
 | 
					import { artistsOrganize } from '../utils'
 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
import TrackItem from './TrackItem.vue'
 | 
					import TrackItem from './TrackItem.vue'
 | 
				
			||||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
					import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,9 +17,7 @@ const props = defineProps<{
 | 
				
			||||||
	present: boolean
 | 
						present: boolean
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits<{
 | 
					const emit = defineEmits<(e: 'dismiss') => void>()
 | 
				
			||||||
	(e: 'dismiss'): void
 | 
					 | 
				
			||||||
}>()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const album = ref<Album>()
 | 
					const album = ref<Album>()
 | 
				
			||||||
const dialogBackdrop = ref<HTMLElement>()
 | 
					const dialogBackdrop = ref<HTMLElement>()
 | 
				
			||||||
| 
						 | 
					@ -28,7 +26,9 @@ const closeButton = ref<HTMLElement>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Animation functions
 | 
					// Animation functions
 | 
				
			||||||
const animateIn = async () => {
 | 
					const animateIn = async () => {
 | 
				
			||||||
	if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) return
 | 
						if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set initial states
 | 
						// Set initial states
 | 
				
			||||||
	gsap.set(dialogBackdrop.value, { opacity: 0 })
 | 
						gsap.set(dialogBackdrop.value, { opacity: 0 })
 | 
				
			||||||
| 
						 | 
					@ -41,108 +41,141 @@ const animateIn = async () => {
 | 
				
			||||||
	tl.to(dialogBackdrop.value, {
 | 
						tl.to(dialogBackdrop.value, {
 | 
				
			||||||
		opacity: 1,
 | 
							opacity: 1,
 | 
				
			||||||
		duration: 0.3,
 | 
							duration: 0.3,
 | 
				
			||||||
		ease: "power2.out"
 | 
							ease: 'power2.out',
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
		.to(dialogContent.value, {
 | 
							.to(
 | 
				
			||||||
 | 
								dialogContent.value,
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
				y: 0,
 | 
									y: 0,
 | 
				
			||||||
				opacity: 1,
 | 
									opacity: 1,
 | 
				
			||||||
				scale: 1,
 | 
									scale: 1,
 | 
				
			||||||
				duration: 0.4,
 | 
									duration: 0.4,
 | 
				
			||||||
			ease: "power3.out"
 | 
									ease: 'power3.out',
 | 
				
			||||||
		}, "-=0.1")
 | 
								},
 | 
				
			||||||
		.to(closeButton.value, {
 | 
								'-=0.1',
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.to(
 | 
				
			||||||
 | 
								closeButton.value,
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
				scale: 1,
 | 
									scale: 1,
 | 
				
			||||||
				rotation: 0,
 | 
									rotation: 0,
 | 
				
			||||||
				duration: 0.3,
 | 
									duration: 0.3,
 | 
				
			||||||
			ease: "back.out(1.7)"
 | 
									ease: 'back.out(1.7)',
 | 
				
			||||||
		}, "-=0.2")
 | 
								},
 | 
				
			||||||
 | 
								'-=0.2',
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const animateOut = () => {
 | 
					const animateOut = () => {
 | 
				
			||||||
	if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) return
 | 
						if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const tl = gsap.timeline({
 | 
						const tl = gsap.timeline({
 | 
				
			||||||
		onComplete: () => emit('dismiss')
 | 
							onComplete: () => emit('dismiss'),
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tl.to(closeButton.value, {
 | 
						tl.to(closeButton.value, {
 | 
				
			||||||
		scale: 0,
 | 
							scale: 0,
 | 
				
			||||||
		rotation: 180,
 | 
							rotation: 180,
 | 
				
			||||||
		duration: 0.2,
 | 
							duration: 0.2,
 | 
				
			||||||
		ease: "power2.in"
 | 
							ease: 'power2.in',
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
		.to(dialogContent.value, {
 | 
							.to(
 | 
				
			||||||
 | 
								dialogContent.value,
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
				y: 30,
 | 
									y: 30,
 | 
				
			||||||
				opacity: 0,
 | 
									opacity: 0,
 | 
				
			||||||
				scale: 0.95,
 | 
									scale: 0.95,
 | 
				
			||||||
				duration: 0.3,
 | 
									duration: 0.3,
 | 
				
			||||||
			ease: "power2.in"
 | 
									ease: 'power2.in',
 | 
				
			||||||
		}, "-=0.1")
 | 
								},
 | 
				
			||||||
		.to(dialogBackdrop.value, {
 | 
								'-=0.1',
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.to(
 | 
				
			||||||
 | 
								dialogBackdrop.value,
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
				opacity: 0,
 | 
									opacity: 0,
 | 
				
			||||||
				duration: 0.2,
 | 
									duration: 0.2,
 | 
				
			||||||
			ease: "power2.in"
 | 
									ease: 'power2.in',
 | 
				
			||||||
		}, "-=0.1")
 | 
								},
 | 
				
			||||||
 | 
								'-=0.1',
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used inside <template>
 | 
				
			||||||
const handleClose = () => {
 | 
					const handleClose = () => {
 | 
				
			||||||
	animateOut()
 | 
						animateOut()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(() => props.present, async (newVal) => {
 | 
					watch(
 | 
				
			||||||
 | 
						() => props.present,
 | 
				
			||||||
 | 
						async (newVal) => {
 | 
				
			||||||
		if (newVal) {
 | 
							if (newVal) {
 | 
				
			||||||
			await nextTick()
 | 
								await nextTick()
 | 
				
			||||||
			animateIn()
 | 
								animateIn()
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
})
 | 
						},
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(() => props.albumCid, async () => {
 | 
					watch(
 | 
				
			||||||
	console.log("AlbumDetailDialog mounted with albumCid:", props.albumCid)
 | 
						() => props.albumCid,
 | 
				
			||||||
 | 
						async () => {
 | 
				
			||||||
 | 
							console.log('AlbumDetailDialog mounted with albumCid:', props.albumCid)
 | 
				
			||||||
		album.value = undefined // Reset album when cid changes
 | 
							album.value = undefined // Reset album when cid changes
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
		let res = await apis.getAlbum(props.albumCid)
 | 
								const res = await apis.getAlbum(props.albumCid)
 | 
				
			||||||
			for (const track in res.songs) {
 | 
								for (const track in res.songs) {
 | 
				
			||||||
			res.songs[parseInt(track)] = await apis.getSong(res.songs[parseInt(track)].cid)
 | 
									res.songs[Number.parseInt(track, 10)] = await apis.getSong(
 | 
				
			||||||
 | 
										res.songs[Number.parseInt(track, 10)].cid,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			album.value = res
 | 
								album.value = res
 | 
				
			||||||
		} catch (error) {
 | 
							} catch (error) {
 | 
				
			||||||
			console.error(error)
 | 
								console.error(error)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
})
 | 
						},
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const playQueue = usePlayQueueStore()
 | 
					const playQueue = usePlayStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function playTheAlbum(from: number = 0) {
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
 | 
					function playTheAlbum(from = 0) {
 | 
				
			||||||
	if (playQueue.queueReplaceLock) {
 | 
						if (playQueue.queueReplaceLock) {
 | 
				
			||||||
		if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
 | 
							if (
 | 
				
			||||||
 | 
								!confirm(
 | 
				
			||||||
 | 
									'当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?',
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		playQueue.queueReplaceLock = false
 | 
							playQueue.queueReplaceLock = false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let newPlayQueue = []
 | 
						const newPlayQueue = []
 | 
				
			||||||
	for (const track of album.value?.songs ?? []) {
 | 
						for (const track of album.value?.songs ?? []) {
 | 
				
			||||||
		console.log(track)
 | 
							console.log(track)
 | 
				
			||||||
		newPlayQueue.push({
 | 
							newPlayQueue.push({
 | 
				
			||||||
			song: track,
 | 
								song: track,
 | 
				
			||||||
			album: album.value
 | 
								album: album.value,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	playQueue.list = newPlayQueue
 | 
						playQueue.replaceQueue(newPlayQueue)
 | 
				
			||||||
	playQueue.currentIndex = from
 | 
						// playQueue.currentIndex = from
 | 
				
			||||||
	playQueue.isPlaying = true
 | 
						playQueue.togglePlay(true)
 | 
				
			||||||
	playQueue.isBuffering = true
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used inside <template>
 | 
				
			||||||
function shuffle() {
 | 
					function shuffle() {
 | 
				
			||||||
	playTheAlbum()
 | 
						// playTheAlbum()
 | 
				
			||||||
	playQueue.shuffleCurrent = true
 | 
						// playQueue.shuffleCurrent = true
 | 
				
			||||||
	playQueue.playMode.shuffle = false
 | 
						// playQueue.playMode.shuffle = false
 | 
				
			||||||
	setTimeout(() => {
 | 
						// setTimeout(() => {
 | 
				
			||||||
		playQueue.playMode.shuffle = true
 | 
						// 	playQueue.playMode.shuffle = true
 | 
				
			||||||
		playQueue.isPlaying = true
 | 
						// 	playQueue.isPlaying = true
 | 
				
			||||||
		playQueue.isBuffering = true
 | 
						// 	playQueue.isBuffering = true
 | 
				
			||||||
	}, 100)
 | 
						// }, 100)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/components/MiniPlayer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/MiniPlayer.vue
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
 | 
					const playQueue = usePlayStore()
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
						<RouterLink to="/playroom" v-if="playQueue.currentTrack">
 | 
				
			||||||
 | 
							<div
 | 
				
			||||||
 | 
								class="h-9 w-52 bg-neutral-800/80 border border-[#ffffff39] rounded-full backdrop-blur-3xl flex items-center justify-between select-none overflow-hidden">
 | 
				
			||||||
 | 
								<div class="flex items-center gap-2">
 | 
				
			||||||
 | 
									<div class="rounded-full w-9 h-9 bg-gray-600 overflow-hidden">
 | 
				
			||||||
 | 
										<img :src="playQueue.currentTrack.metadata?.artwork?.[0].src ?? ''" />
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
									<div class="text-white">{{playQueue.currentTrack.metadata?.title ?? "未知歌曲"}}</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</RouterLink>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
| 
						 | 
					@ -5,18 +5,19 @@ import { useFavourites } from '../stores/useFavourites'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import StarSlashIcon from '../assets/icons/starslash.vue'
 | 
					import StarSlashIcon from '../assets/icons/starslash.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const favourites = useFavourites()
 | 
					const favourites = useFavourites()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const hover = ref(false)
 | 
					const hover = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
defineProps<{
 | 
					defineProps<{
 | 
				
			||||||
	item: QueueItem
 | 
						item: InternalQueueItem
 | 
				
			||||||
	index: number
 | 
						index: number
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const emit = defineEmits<{
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
	(e: 'play', index: number): void
 | 
					const emit = defineEmits<(e: 'play', index: number) => void>()
 | 
				
			||||||
}>()
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
import { artistsOrganize, supportsWebAudioVisualization } from '../utils'
 | 
					import { artistsOrganize, supportsWebAudioVisualization } from '../utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import XIcon from '../assets/icons/x.vue'
 | 
					import XIcon from '../assets/icons/x.vue'
 | 
				
			||||||
| 
						 | 
					@ -10,18 +10,21 @@ import SoundwaveIcon from '../assets/icons/soundwave.vue'
 | 
				
			||||||
import { ref } from 'vue'
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
	queueItem: QueueItem
 | 
						queueItem: InternalQueueItem
 | 
				
			||||||
	isCurrent: boolean
 | 
						isCurrent: boolean
 | 
				
			||||||
	index: number
 | 
						index: number
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const playQueueStore = usePlayQueueStore()
 | 
					const playStore = usePlayStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const hover = ref(false)
 | 
					const hover = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 检查浏览器是否支持音频可视化
 | 
					// 检查浏览器是否支持音频可视化
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
 | 
					const isAudioVisualizationSupported = supportsWebAudioVisualization()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function moveUp() {
 | 
					function moveUp() {
 | 
				
			||||||
	if (props.index === 0) return
 | 
						if (props.index === 0) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,8 +61,11 @@ function moveUp() {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function moveDown() {
 | 
					function moveDown() {
 | 
				
			||||||
	const listLength = playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length
 | 
						const listLength = playQueueStore.playMode.shuffle
 | 
				
			||||||
 | 
							? playQueueStore.shuffleList.length
 | 
				
			||||||
 | 
							: playQueueStore.list.length
 | 
				
			||||||
	if (props.index === listLength - 1) return
 | 
						if (props.index === listLength - 1) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	playQueueStore.queueReplaceLock = true
 | 
						playQueueStore.queueReplaceLock = true
 | 
				
			||||||
| 
						 | 
					@ -95,6 +101,7 @@ function moveDown() {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function removeItem() {
 | 
					function removeItem() {
 | 
				
			||||||
	playQueueStore.queueReplaceLock = true
 | 
						playQueueStore.queueReplaceLock = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -109,7 +116,10 @@ function removeItem() {
 | 
				
			||||||
			playQueueStore.currentIndex--
 | 
								playQueueStore.currentIndex--
 | 
				
			||||||
		} else if (props.index === playQueueStore.currentIndex) {
 | 
							} else if (props.index === playQueueStore.currentIndex) {
 | 
				
			||||||
			if (queue.length > 0) {
 | 
								if (queue.length > 0) {
 | 
				
			||||||
				playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
 | 
									playQueueStore.currentIndex = Math.min(
 | 
				
			||||||
 | 
										playQueueStore.currentIndex,
 | 
				
			||||||
 | 
										queue.length - 1,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				playQueueStore.currentIndex = 0
 | 
									playQueueStore.currentIndex = 0
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -140,7 +150,10 @@ function removeItem() {
 | 
				
			||||||
			playQueueStore.currentIndex--
 | 
								playQueueStore.currentIndex--
 | 
				
			||||||
		} else if (props.index === playQueueStore.currentIndex) {
 | 
							} else if (props.index === playQueueStore.currentIndex) {
 | 
				
			||||||
			if (queue.length > 0) {
 | 
								if (queue.length > 0) {
 | 
				
			||||||
				playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
 | 
									playQueueStore.currentIndex = Math.min(
 | 
				
			||||||
 | 
										playQueueStore.currentIndex,
 | 
				
			||||||
 | 
										queue.length - 1,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				playQueueStore.currentIndex = 0
 | 
									playQueueStore.currentIndex = 0
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -152,8 +165,8 @@ function removeItem() {
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
	<button class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2 flex gap-2 items-center" @click="() => {
 | 
						<button class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2 flex gap-2 items-center" @click="() => {
 | 
				
			||||||
		if (isCurrent) { return }
 | 
							if (isCurrent) { return }
 | 
				
			||||||
		playQueueStore.currentIndex = index
 | 
							// playStore.currentIndex = index
 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
							// playStore.isPlaying = true
 | 
				
			||||||
	}" @mouseenter="hover = true" @mouseleave="hover = false">
 | 
						}" @mouseenter="hover = true" @mouseleave="hover = false">
 | 
				
			||||||
		<div class="flex gap-2 flex-auto w-0">
 | 
							<div class="flex gap-2 flex-auto w-0">
 | 
				
			||||||
			<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
 | 
								<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
 | 
				
			||||||
| 
						 | 
					@ -162,7 +175,7 @@ function removeItem() {
 | 
				
			||||||
					v-if="isCurrent">
 | 
										v-if="isCurrent">
 | 
				
			||||||
					<!-- 在支持的浏览器上显示可视化,否则显示音波图标 -->
 | 
										<!-- 在支持的浏览器上显示可视化,否则显示音波图标 -->
 | 
				
			||||||
					<div v-if="isAudioVisualizationSupported" style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
										<div v-if="isAudioVisualizationSupported" style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
				
			||||||
						<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
											<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playStore.visualizer"
 | 
				
			||||||
							:key="index" :style="{
 | 
												:key="index" :style="{
 | 
				
			||||||
								height: `${Math.max(10, bar)}%`
 | 
													height: `${Math.max(10, bar)}%`
 | 
				
			||||||
							}" />
 | 
												}" />
 | 
				
			||||||
| 
						 | 
					@ -189,8 +202,8 @@ function removeItem() {
 | 
				
			||||||
			<button
 | 
								<button
 | 
				
			||||||
				class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
 | 
									class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
 | 
				
			||||||
				@click.stop="moveDown"
 | 
									@click.stop="moveDown"
 | 
				
			||||||
				:disabled="index === (playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length) - 1"
 | 
									:disabled="index === (playStore.playMode.shuffle ? playStore.shuffleList.length : playStore.list.length) - 1"
 | 
				
			||||||
				v-if="index !== (playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length) - 1">
 | 
									v-if="index !== (playStore.playMode.shuffle ? playStore.shuffleList.length : playStore.list.length) - 1">
 | 
				
			||||||
				<DownHyphenIcon :size="4" />
 | 
									<DownHyphenIcon :size="4" />
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,535 +0,0 @@
 | 
				
			||||||
<!-- Player.vue - 添加预加载功能 -->
 | 
					 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
 | 
					 | 
				
			||||||
import { useRoute } from 'vue-router'
 | 
					 | 
				
			||||||
import { useFavourites } from '../stores/useFavourites'
 | 
					 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
					 | 
				
			||||||
import PlayIcon from '../assets/icons/play.vue'
 | 
					 | 
				
			||||||
import PauseIcon from '../assets/icons/pause.vue'
 | 
					 | 
				
			||||||
import { audioVisualizer, checkAndRefreshSongResource, supportsWebAudioVisualization } from '../utils'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const playQueueStore = usePlayQueueStore()
 | 
					 | 
				
			||||||
const favourites = useFavourites()
 | 
					 | 
				
			||||||
const route = useRoute()
 | 
					 | 
				
			||||||
const player = useTemplateRef('playerRef')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// [调试] 检查 store 方法类型
 | 
					 | 
				
			||||||
console.log('[Player] 检查 store 方法:', {
 | 
					 | 
				
			||||||
	preloadNext: typeof playQueueStore.preloadNext,
 | 
					 | 
				
			||||||
	getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
 | 
					 | 
				
			||||||
	clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio,
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 获取当前歌曲的计算属性
 | 
					 | 
				
			||||||
const currentTrack = computed(() => {
 | 
					 | 
				
			||||||
	if (
 | 
					 | 
				
			||||||
		playQueueStore.playMode.shuffle &&
 | 
					 | 
				
			||||||
		playQueueStore.shuffleList.length > 0
 | 
					 | 
				
			||||||
	) {
 | 
					 | 
				
			||||||
		return playQueueStore.list[
 | 
					 | 
				
			||||||
			playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
					 | 
				
			||||||
		]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return playQueueStore.list[playQueueStore.currentIndex]
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 获取当前歌曲的音频源
 | 
					 | 
				
			||||||
const currentAudioSrc = computed(() => {
 | 
					 | 
				
			||||||
	const track = currentTrack.value
 | 
					 | 
				
			||||||
	return track ? track.song.sourceUrl : ''
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => playQueueStore.isPlaying,
 | 
					 | 
				
			||||||
	(newValue) => {
 | 
					 | 
				
			||||||
		if (newValue) {
 | 
					 | 
				
			||||||
			player.value?.play()
 | 
					 | 
				
			||||||
			setMetadata()
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			player.value?.pause()
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听当前索引变化,处理预加载逻辑
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => playQueueStore.currentIndex,
 | 
					 | 
				
			||||||
	async () => {
 | 
					 | 
				
			||||||
		console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 检查是否可以使用预加载的音频
 | 
					 | 
				
			||||||
		const track = currentTrack.value
 | 
					 | 
				
			||||||
		if (track) {
 | 
					 | 
				
			||||||
			const songId = track.song.cid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				// 首先检查和刷新当前歌曲的资源
 | 
					 | 
				
			||||||
				console.log('[Player] 检查当前歌曲资源:', track.song.name)
 | 
					 | 
				
			||||||
				const updatedSong = await checkAndRefreshSongResource(
 | 
					 | 
				
			||||||
					track.song,
 | 
					 | 
				
			||||||
					(updated) => {
 | 
					 | 
				
			||||||
						// 更新播放队列中的歌曲信息
 | 
					 | 
				
			||||||
						// 在随机播放模式下,currentIndex 是 shuffleList 的索引
 | 
					 | 
				
			||||||
						// 需要通过 shuffleList[currentIndex] 获取实际的 list 索引
 | 
					 | 
				
			||||||
						const actualIndex =
 | 
					 | 
				
			||||||
							playQueueStore.playMode.shuffle &&
 | 
					 | 
				
			||||||
							playQueueStore.shuffleList.length > 0
 | 
					 | 
				
			||||||
								? playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
					 | 
				
			||||||
								: playQueueStore.currentIndex
 | 
					 | 
				
			||||||
						if (playQueueStore.list[actualIndex]) {
 | 
					 | 
				
			||||||
							playQueueStore.list[actualIndex].song = updated
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
						// 如果歌曲在收藏夹中,也更新收藏夹
 | 
					 | 
				
			||||||
						favourites.updateSongInFavourites(songId, updated)
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// 使用更新后的歌曲信息
 | 
					 | 
				
			||||||
				const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) {
 | 
					 | 
				
			||||||
					console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// 直接使用预加载的音频数据
 | 
					 | 
				
			||||||
					if (player.value) {
 | 
					 | 
				
			||||||
						// 复制预加载音频的状态到主播放器
 | 
					 | 
				
			||||||
						player.value.src = preloadedAudio.src
 | 
					 | 
				
			||||||
						player.value.currentTime = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						// 清理使用过的预加载音频
 | 
					 | 
				
			||||||
						playQueueStore.clearPreloadedAudio(songId)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						// 如果正在播放状态,立即播放
 | 
					 | 
				
			||||||
						if (playQueueStore.isPlaying) {
 | 
					 | 
				
			||||||
							await nextTick()
 | 
					 | 
				
			||||||
							player.value.play().catch(console.error)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
						playQueueStore.isBuffering = false
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					console.log(`[Player] 正常加载音频: ${track.song.name}`)
 | 
					 | 
				
			||||||
					playQueueStore.isBuffering = true
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// 如果资源地址已更新,清除旧的预加载音频
 | 
					 | 
				
			||||||
					if (updatedSong.sourceUrl !== track.song.sourceUrl) {
 | 
					 | 
				
			||||||
						playQueueStore.clearPreloadedAudio(songId)
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} catch (error) {
 | 
					 | 
				
			||||||
				console.error('[Player] 处理预加载音频时出错:', error)
 | 
					 | 
				
			||||||
				playQueueStore.isBuffering = true
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		setMetadata()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 延迟预加载下一首歌,避免影响当前歌曲加载
 | 
					 | 
				
			||||||
		setTimeout(async () => {
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				console.log('[Player] 尝试预加载下一首歌')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				// 检查函数是否存在
 | 
					 | 
				
			||||||
				if (typeof playQueueStore.preloadNext === 'function') {
 | 
					 | 
				
			||||||
					await playQueueStore.preloadNext()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// 预加载完成后,检查播放队列是否有更新,同步到收藏夹
 | 
					 | 
				
			||||||
					playQueueStore.list.forEach((item) => {
 | 
					 | 
				
			||||||
						if (favourites.isFavourite(item.song.cid)) {
 | 
					 | 
				
			||||||
							favourites.updateSongInFavourites(item.song.cid, item.song)
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					playQueueStore.limitPreloadCache()
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					console.error('[Player] preloadNext 不是一个函数')
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} catch (error) {
 | 
					 | 
				
			||||||
				console.error('[Player] 预加载失败:', error)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}, 1000)
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function artistsOrganize(list: string[]) {
 | 
					 | 
				
			||||||
	if (list.length === 0) {
 | 
					 | 
				
			||||||
		return '未知音乐人'
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return list
 | 
					 | 
				
			||||||
		.map((artist) => {
 | 
					 | 
				
			||||||
			return artist
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		.join(' / ')
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function setMetadata() {
 | 
					 | 
				
			||||||
	if ('mediaSession' in navigator) {
 | 
					 | 
				
			||||||
		const current = currentTrack.value
 | 
					 | 
				
			||||||
		if (!current) return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		navigator.mediaSession.metadata = new MediaMetadata({
 | 
					 | 
				
			||||||
			title: current.song.name,
 | 
					 | 
				
			||||||
			artist: artistsOrganize(current.song.artists ?? []),
 | 
					 | 
				
			||||||
			album: current.album?.name,
 | 
					 | 
				
			||||||
			artwork: [
 | 
					 | 
				
			||||||
				{
 | 
					 | 
				
			||||||
					src: current.album?.coverUrl ?? '',
 | 
					 | 
				
			||||||
					sizes: '500x500',
 | 
					 | 
				
			||||||
					type: 'image/png',
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			],
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
 | 
					 | 
				
			||||||
		navigator.mediaSession.setActionHandler('nexttrack', playNext)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		playQueueStore.duration = player.value?.duration ?? 0
 | 
					 | 
				
			||||||
		playQueueStore.currentTime = player.value?.currentTime ?? 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	watch(
 | 
					 | 
				
			||||||
		() => playQueueStore.updatedCurrentTime,
 | 
					 | 
				
			||||||
		(newValue) => {
 | 
					 | 
				
			||||||
			if (newValue === null) {
 | 
					 | 
				
			||||||
				return
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if (player.value) player.value.currentTime = newValue
 | 
					 | 
				
			||||||
			playQueueStore.updatedCurrentTime = null
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function playNext() {
 | 
					 | 
				
			||||||
	if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
 | 
					 | 
				
			||||||
		console.log('at the bottom, pause')
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex = 0
 | 
					 | 
				
			||||||
		if (playQueueStore.playMode.repeat === 'all') {
 | 
					 | 
				
			||||||
			playQueueStore.currentIndex = 0
 | 
					 | 
				
			||||||
			playQueueStore.isPlaying = true
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			player.value?.pause()
 | 
					 | 
				
			||||||
			playQueueStore.isPlaying = false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex++
 | 
					 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function playPrevious() {
 | 
					 | 
				
			||||||
	if (
 | 
					 | 
				
			||||||
		player.value &&
 | 
					 | 
				
			||||||
		(player.value.currentTime ?? 0) < 5 &&
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex > 0
 | 
					 | 
				
			||||||
	) {
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex--
 | 
					 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		if (player.value) {
 | 
					 | 
				
			||||||
			player.value.currentTime = 0
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function updateCurrentTime() {
 | 
					 | 
				
			||||||
	playQueueStore.currentTime = player.value?.currentTime ?? 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 智能预加载策略:支持动态配置
 | 
					 | 
				
			||||||
	if (playQueueStore.duration > 0) {
 | 
					 | 
				
			||||||
		const progress = playQueueStore.currentTime / playQueueStore.duration
 | 
					 | 
				
			||||||
		const remainingTime = playQueueStore.duration - playQueueStore.currentTime
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 从 localStorage 获取配置,如果没有则使用默认值
 | 
					 | 
				
			||||||
		const config = JSON.parse(localStorage.getItem('preloadConfig') || '{}')
 | 
					 | 
				
			||||||
		const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
 | 
					 | 
				
			||||||
		const remainingTimeThreshold = config.remainingTimeThreshold || 30
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (
 | 
					 | 
				
			||||||
			(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
 | 
					 | 
				
			||||||
			!playQueueStore.isPreloading
 | 
					 | 
				
			||||||
		) {
 | 
					 | 
				
			||||||
			try {
 | 
					 | 
				
			||||||
				if (typeof playQueueStore.preloadNext === 'function') {
 | 
					 | 
				
			||||||
					playQueueStore.preloadNext()
 | 
					 | 
				
			||||||
				} else {
 | 
					 | 
				
			||||||
					console.error('[Player] preloadNext 不是一个函数')
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			} catch (error) {
 | 
					 | 
				
			||||||
				console.error('[Player] 智能预加载失败:', error)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 检查浏览器是否支持音频可视化
 | 
					 | 
				
			||||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
 | 
					 | 
				
			||||||
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 只在支持的浏览器上初始化音频可视化
 | 
					 | 
				
			||||||
let barHeights = ref<number[]>([0, 0, 0, 0, 0, 0])
 | 
					 | 
				
			||||||
let connectAudio = (_audio: HTMLAudioElement) => {}
 | 
					 | 
				
			||||||
let isAnalyzing = ref(false)
 | 
					 | 
				
			||||||
let error = ref<string | null>(null)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if (isAudioVisualizationSupported) {
 | 
					 | 
				
			||||||
	console.log('[Player] 初始化 audioVisualizer')
 | 
					 | 
				
			||||||
	const visualizer = audioVisualizer({
 | 
					 | 
				
			||||||
		sensitivity: 1.5,
 | 
					 | 
				
			||||||
		barCount: 6,
 | 
					 | 
				
			||||||
		maxDecibels: -10,
 | 
					 | 
				
			||||||
		bassBoost: 0.8,
 | 
					 | 
				
			||||||
		midBoost: 1.2,
 | 
					 | 
				
			||||||
		trebleBoost: 1.4,
 | 
					 | 
				
			||||||
		threshold: 0,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	barHeights = visualizer.barHeights
 | 
					 | 
				
			||||||
	connectAudio = visualizer.connectAudio
 | 
					 | 
				
			||||||
	isAnalyzing = visualizer.isAnalyzing
 | 
					 | 
				
			||||||
	error = visualizer.error
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	console.log('[Player] audioVisualizer 返回值:', {
 | 
					 | 
				
			||||||
		barHeights: barHeights.value,
 | 
					 | 
				
			||||||
		isAnalyzing: isAnalyzing.value,
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
} else {
 | 
					 | 
				
			||||||
	console.log('[Player] 音频可视化被禁用(Safari 或不支持的浏览器)')
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听播放列表变化
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => playQueueStore.list.length,
 | 
					 | 
				
			||||||
	async (newLength) => {
 | 
					 | 
				
			||||||
		console.log('[Player] 播放列表长度变化:', newLength)
 | 
					 | 
				
			||||||
		if (newLength === 0) {
 | 
					 | 
				
			||||||
			console.log('[Player] 播放列表为空,跳过连接')
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 等待下一帧,确保 audio 元素已经渲染
 | 
					 | 
				
			||||||
		await nextTick()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (player.value) {
 | 
					 | 
				
			||||||
			if (isAudioVisualizationSupported) {
 | 
					 | 
				
			||||||
				console.log('[Player] 连接音频元素到可视化器')
 | 
					 | 
				
			||||||
				console.log('[Player] 音频元素状态:', {
 | 
					 | 
				
			||||||
					src: player.value.src?.substring(0, 50) + '...',
 | 
					 | 
				
			||||||
					readyState: player.value.readyState,
 | 
					 | 
				
			||||||
					paused: player.value.paused,
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
				connectAudio(player.value)
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				console.log('[Player] 跳过音频可视化连接(不支持的浏览器)')
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			console.log('[Player] ❌ 音频元素不存在')
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		playQueueStore.visualizer = barHeights.value
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 开始预加载第一首歌的下一首
 | 
					 | 
				
			||||||
		setTimeout(() => {
 | 
					 | 
				
			||||||
			playQueueStore.preloadNext()
 | 
					 | 
				
			||||||
		}, 2000)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 初始化音量
 | 
					 | 
				
			||||||
		if (player.value) {
 | 
					 | 
				
			||||||
			initializeVolume()
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听音频元素变化
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => player.value,
 | 
					 | 
				
			||||||
	(audioElement) => {
 | 
					 | 
				
			||||||
		if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) {
 | 
					 | 
				
			||||||
			connectAudio(audioElement)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听可视化器数据变化
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => barHeights.value,
 | 
					 | 
				
			||||||
	(newHeights) => {
 | 
					 | 
				
			||||||
		playQueueStore.visualizer = newHeights
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	{ deep: true },
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听错误
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => error.value,
 | 
					 | 
				
			||||||
	(newError) => {
 | 
					 | 
				
			||||||
		if (newError) {
 | 
					 | 
				
			||||||
			console.error('[Player] 可视化器错误:', newError)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 切换播放模式
 | 
					 | 
				
			||||||
watch(
 | 
					 | 
				
			||||||
	() => playQueueStore.playMode.shuffle,
 | 
					 | 
				
			||||||
	(isShuffle) => {
 | 
					 | 
				
			||||||
		if (isShuffle) {
 | 
					 | 
				
			||||||
			const currentIndex = playQueueStore.currentIndex
 | 
					 | 
				
			||||||
			const trackCount = playQueueStore.list.length
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 1. 已播放部分:不变
 | 
					 | 
				
			||||||
			let shuffledList = [...Array(currentIndex).keys()]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 2. 构建待打乱的列表
 | 
					 | 
				
			||||||
			const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
 | 
					 | 
				
			||||||
				playQueueStore.shuffleCurrent
 | 
					 | 
				
			||||||
					? index >= currentIndex
 | 
					 | 
				
			||||||
					: index > currentIndex,
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 3. 随机打乱
 | 
					 | 
				
			||||||
			shuffleSpace.sort(() => Math.random() - 0.5)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
 | 
					 | 
				
			||||||
			if (!playQueueStore.shuffleCurrent) {
 | 
					 | 
				
			||||||
				shuffledList.push(currentIndex)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
 | 
					 | 
				
			||||||
			shuffledList = shuffledList.concat(shuffleSpace)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 6. 应用 shuffleList
 | 
					 | 
				
			||||||
			playQueueStore.shuffleList = shuffledList
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 清除 shuffleCurrent 状态
 | 
					 | 
				
			||||||
			playQueueStore.shuffleCurrent = undefined
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// 退出随机播放:恢复当前播放曲目的原索引
 | 
					 | 
				
			||||||
			playQueueStore.currentIndex =
 | 
					 | 
				
			||||||
				playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 切换播放模式后重新预加载
 | 
					 | 
				
			||||||
		setTimeout(() => {
 | 
					 | 
				
			||||||
			playQueueStore.clearAllPreloadedAudio()
 | 
					 | 
				
			||||||
			playQueueStore.preloadNext()
 | 
					 | 
				
			||||||
		}, 500)
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function getCurrentTrack() {
 | 
					 | 
				
			||||||
	return currentTrack.value
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 初始化音量
 | 
					 | 
				
			||||||
function initializeVolume() {
 | 
					 | 
				
			||||||
	if (player.value) {
 | 
					 | 
				
			||||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
					 | 
				
			||||||
		if (savedVolume) {
 | 
					 | 
				
			||||||
			const volumeValue = Number.parseFloat(savedVolume)
 | 
					 | 
				
			||||||
			player.value.volume = volumeValue
 | 
					 | 
				
			||||||
			console.log('[Player] 初始化音量:', volumeValue)
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// 设置默认音量
 | 
					 | 
				
			||||||
			player.value.volume = 1
 | 
					 | 
				
			||||||
			localStorage.setItem('audioVolume', '1')
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听音量变化事件
 | 
					 | 
				
			||||||
function handleVolumeChange(event: Event) {
 | 
					 | 
				
			||||||
	const target = event.target as HTMLAudioElement
 | 
					 | 
				
			||||||
	if (target) {
 | 
					 | 
				
			||||||
		// 保存音量变化到localStorage
 | 
					 | 
				
			||||||
		localStorage.setItem('audioVolume', target.volume.toString())
 | 
					 | 
				
			||||||
		console.log('[Player] 音量变化:', target.volume)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听localStorage中音量的变化,同步到音频元素
 | 
					 | 
				
			||||||
function syncVolumeFromStorage() {
 | 
					 | 
				
			||||||
	if (player.value) {
 | 
					 | 
				
			||||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
					 | 
				
			||||||
		if (savedVolume) {
 | 
					 | 
				
			||||||
			const volumeValue = Number.parseFloat(savedVolume)
 | 
					 | 
				
			||||||
			if (player.value.volume !== volumeValue) {
 | 
					 | 
				
			||||||
				player.value.volume = volumeValue
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 定期检查音量同步(可选,或者使用storage事件)
 | 
					 | 
				
			||||||
setInterval(syncVolumeFromStorage, 100)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 组件卸载时清理预加载
 | 
					 | 
				
			||||||
// onUnmounted(() => {
 | 
					 | 
				
			||||||
//   playQueueStore.clearAllPreloadedAudio()
 | 
					 | 
				
			||||||
// })
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
	<div>
 | 
					 | 
				
			||||||
		<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
 | 
					 | 
				
			||||||
			v-if="playQueueStore.list.length !== 0" @volumechange="handleVolumeChange" @ended="() => {
 | 
					 | 
				
			||||||
				if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
 | 
					 | 
				
			||||||
				else { playNext() }
 | 
					 | 
				
			||||||
			}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
 | 
					 | 
				
			||||||
				console.log('[Player] 音频开始播放事件')
 | 
					 | 
				
			||||||
				playQueueStore.isBuffering = false
 | 
					 | 
				
			||||||
				setMetadata()
 | 
					 | 
				
			||||||
				initializeVolume()
 | 
					 | 
				
			||||||
			}" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
 | 
					 | 
				
			||||||
				console.log('[Player] 音频数据加载完成')
 | 
					 | 
				
			||||||
				playQueueStore.isBuffering = false
 | 
					 | 
				
			||||||
				initializeVolume()
 | 
					 | 
				
			||||||
			}" @canplay="() => {
 | 
					 | 
				
			||||||
				console.log('[Player] 音频可以播放')
 | 
					 | 
				
			||||||
				playQueueStore.isBuffering = false
 | 
					 | 
				
			||||||
			}" @error="(e) => {
 | 
					 | 
				
			||||||
				console.error('[Player] 音频错误:', e)
 | 
					 | 
				
			||||||
				playQueueStore.isBuffering = false
 | 
					 | 
				
			||||||
			}" crossorigin="anonymous" @timeupdate="updateCurrentTime">
 | 
					 | 
				
			||||||
		</audio>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		<!-- 预加载进度指示器(可选显示) -->
 | 
					 | 
				
			||||||
		<!-- <div v-if="playQueueStore.isPreloading"
 | 
					 | 
				
			||||||
			class="fixed top-4 right-4 bg-black/80 text-white px-3 py-1 rounded text-xs z-50">
 | 
					 | 
				
			||||||
			预加载中... {{ Math.round(playQueueStore.preloadProgress) }}%
 | 
					 | 
				
			||||||
		</div> -->
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		<div
 | 
					 | 
				
			||||||
			class="text-white h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex gap-2 overflow-hidden select-none"
 | 
					 | 
				
			||||||
			v-if="playQueueStore.list.length !== 0 && route.path !== '/playroom'">
 | 
					 | 
				
			||||||
			<RouterLink to="/playroom">
 | 
					 | 
				
			||||||
				<img :src="getCurrentTrack()?.album?.coverUrl ?? ''" class="rounded-full h-8 w-8 mt-[.0625rem]" />
 | 
					 | 
				
			||||||
			</RouterLink>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<RouterLink to="/playroom">
 | 
					 | 
				
			||||||
				<div class="flex items-center w-32 h-9">
 | 
					 | 
				
			||||||
					<span class="truncate text-xs">{{ getCurrentTrack()?.song.name }}</span>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</RouterLink>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			<button class="h-9 w-12 flex justify-center items-center" @click.stop="() => {
 | 
					 | 
				
			||||||
				playQueueStore.isPlaying = !playQueueStore.isPlaying
 | 
					 | 
				
			||||||
			}">
 | 
					 | 
				
			||||||
				<div v-if="playQueueStore.isPlaying">
 | 
					 | 
				
			||||||
					<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
 | 
					 | 
				
			||||||
					<!-- 在支持的浏览器上显示可视化,否则显示暂停图标 -->
 | 
					 | 
				
			||||||
					<div v-else-if="isAudioVisualizationSupported" class="h-4 flex justify-center items-center gap-[.125rem]">
 | 
					 | 
				
			||||||
						<div class="bg-white/75 w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
					 | 
				
			||||||
							:key="index" :style="{
 | 
					 | 
				
			||||||
								height: `${Math.max(10, bar)}%`
 | 
					 | 
				
			||||||
							}" />
 | 
					 | 
				
			||||||
					</div>
 | 
					 | 
				
			||||||
					<PauseIcon v-else :size="4" />
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				<PlayIcon v-else :size="4" />
 | 
					 | 
				
			||||||
			</button>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,4 @@
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 | 
					 | 
				
			||||||
import XIcon from '../assets/icons/x.vue'
 | 
					import XIcon from '../assets/icons/x.vue'
 | 
				
			||||||
import { usePreferences } from '../stores/usePreferences'
 | 
					import { usePreferences } from '../stores/usePreferences'
 | 
				
			||||||
import { computed } from 'vue'
 | 
					import { computed } from 'vue'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -89,7 +89,7 @@
 | 
				
			||||||
import { onMounted, ref, watch, nextTick, computed, onUnmounted } from 'vue'
 | 
					import { onMounted, ref, watch, nextTick, computed, onUnmounted } from 'vue'
 | 
				
			||||||
import axios from 'axios'
 | 
					import axios from 'axios'
 | 
				
			||||||
import gsap from 'gsap'
 | 
					import gsap from 'gsap'
 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 类型定义
 | 
					// 类型定义
 | 
				
			||||||
interface LyricsLine {
 | 
					interface LyricsLine {
 | 
				
			||||||
| 
						 | 
					@ -106,7 +106,7 @@ interface GapLine {
 | 
				
			||||||
	duration?: number
 | 
						duration?: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const playQueueStore = usePlayQueueStore()
 | 
					const playStore = usePlayStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 响应式数据
 | 
					// 响应式数据
 | 
				
			||||||
const parsedLyrics = ref<(LyricsLine | GapLine)[]>([])
 | 
					const parsedLyrics = ref<(LyricsLine | GapLine)[]>([])
 | 
				
			||||||
| 
						 | 
					@ -121,7 +121,7 @@ const lyricsWrapper = ref<HTMLElement>()
 | 
				
			||||||
const lineRefs = ref<(HTMLElement | null)[]>([])
 | 
					const lineRefs = ref<(HTMLElement | null)[]>([])
 | 
				
			||||||
const controlPanel = ref<HTMLElement>()
 | 
					const controlPanel = ref<HTMLElement>()
 | 
				
			||||||
const loadingIndicator = ref<HTMLElement>()
 | 
					const loadingIndicator = ref<HTMLElement>()
 | 
				
			||||||
const noLyricsIndicator = ref<HTMLElement>()
 | 
					//const noLyricsIndicator = ref<HTMLElement>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GSAP 动画实例
 | 
					// GSAP 动画实例
 | 
				
			||||||
let scrollTween: gsap.core.Tween | null = null
 | 
					let scrollTween: gsap.core.Tween | null = null
 | 
				
			||||||
| 
						 | 
					@ -135,19 +135,29 @@ const props = defineProps<{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 滚动指示器相关计算
 | 
					// 滚动指示器相关计算
 | 
				
			||||||
const scrollIndicatorHeight = computed(() => {
 | 
					const scrollIndicatorHeight = computed(() => {
 | 
				
			||||||
	if (parsedLyrics.value.length === 0) return 0
 | 
						if (parsedLyrics.value.length === 0) {
 | 
				
			||||||
	return Math.max(10, 100 / parsedLyrics.value.length * 5) // 显示大约5行的比例
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Math.max(10, (100 / parsedLyrics.value.length) * 5) // 显示大约5行的比例
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const scrollIndicatorPosition = computed(() => {
 | 
					const scrollIndicatorPosition = computed(() => {
 | 
				
			||||||
	if (parsedLyrics.value.length === 0 || currentLineIndex.value < 0) return 0
 | 
						if (parsedLyrics.value.length === 0 || currentLineIndex.value < 0) {
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	const progress = currentLineIndex.value / (parsedLyrics.value.length - 1)
 | 
						const progress = currentLineIndex.value / (parsedLyrics.value.length - 1)
 | 
				
			||||||
	const containerHeight = lyricsContainer.value?.clientHeight || 400
 | 
						const containerHeight = lyricsContainer.value?.clientHeight || 400
 | 
				
			||||||
	const indicatorTrackHeight = containerHeight / 2 // 指示器轨道高度
 | 
						const indicatorTrackHeight = containerHeight / 2 // 指示器轨道高度
 | 
				
			||||||
	return progress * (indicatorTrackHeight - (scrollIndicatorHeight.value / 100 * indicatorTrackHeight))
 | 
						return (
 | 
				
			||||||
 | 
							progress *
 | 
				
			||||||
 | 
							(indicatorTrackHeight -
 | 
				
			||||||
 | 
								(scrollIndicatorHeight.value / 100) * indicatorTrackHeight)
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 设置行引用
 | 
					// 设置行引用
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function setLineRef(el: HTMLElement | null, index: number) {
 | 
					function setLineRef(el: HTMLElement | null, index: number) {
 | 
				
			||||||
	if (el) {
 | 
						if (el) {
 | 
				
			||||||
		lineRefs.value[index] = el
 | 
							lineRefs.value[index] = el
 | 
				
			||||||
| 
						 | 
					@ -155,15 +165,20 @@ function setLineRef(el: HTMLElement | null, index: number) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 歌词解析函数
 | 
					// 歌词解析函数
 | 
				
			||||||
function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine | GapLine)[] {
 | 
					function parseLyrics(
 | 
				
			||||||
	if (!lrcText) return [
 | 
						lrcText: string,
 | 
				
			||||||
 | 
						minGapDuration = 5,
 | 
				
			||||||
 | 
					): (LyricsLine | GapLine)[] {
 | 
				
			||||||
 | 
						if (!lrcText) {
 | 
				
			||||||
 | 
							return [
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				type: 'lyric',
 | 
									type: 'lyric',
 | 
				
			||||||
				time: 0,
 | 
									time: 0,
 | 
				
			||||||
				text: '',
 | 
									text: '',
 | 
				
			||||||
			originalTime: '[00:00]'
 | 
									originalTime: '[00:00]',
 | 
				
			||||||
		}
 | 
								},
 | 
				
			||||||
		]
 | 
							]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const lines = lrcText.split('\n')
 | 
						const lines = lrcText.split('\n')
 | 
				
			||||||
	const tempParsedLines: (LyricsLine | GapLine)[] = []
 | 
						const tempParsedLines: (LyricsLine | GapLine)[] = []
 | 
				
			||||||
| 
						 | 
					@ -172,14 +187,18 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for (const line of lines) {
 | 
						for (const line of lines) {
 | 
				
			||||||
		const matches = [...line.matchAll(timeRegex)]
 | 
							const matches = [...line.matchAll(timeRegex)]
 | 
				
			||||||
		if (matches.length === 0) continue
 | 
							if (matches.length === 0) {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const text = line.replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/g, '').trim()
 | 
							const text = line.replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/g, '').trim()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (const match of matches) {
 | 
							for (const match of matches) {
 | 
				
			||||||
			const minutes = parseInt(match[1])
 | 
								const minutes = Number.parseInt(match[1])
 | 
				
			||||||
			const seconds = parseInt(match[2])
 | 
								const seconds = Number.parseInt(match[2])
 | 
				
			||||||
			const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0
 | 
								const milliseconds = match[3]
 | 
				
			||||||
 | 
									? Number.parseInt(match[3].padEnd(3, '0'))
 | 
				
			||||||
 | 
									: 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const totalSeconds = minutes * 60 + seconds + milliseconds / 1000
 | 
								const totalSeconds = minutes * 60 + seconds + milliseconds / 1000
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -188,13 +207,13 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
 | 
				
			||||||
					type: 'lyric',
 | 
										type: 'lyric',
 | 
				
			||||||
					time: totalSeconds,
 | 
										time: totalSeconds,
 | 
				
			||||||
					text: text,
 | 
										text: text,
 | 
				
			||||||
					originalTime: match[0]
 | 
										originalTime: match[0],
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				tempParsedLines.push({
 | 
									tempParsedLines.push({
 | 
				
			||||||
					type: 'gap',
 | 
										type: 'gap',
 | 
				
			||||||
					time: totalSeconds,
 | 
										time: totalSeconds,
 | 
				
			||||||
					originalTime: match[0]
 | 
										originalTime: match[0],
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -203,14 +222,20 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
 | 
				
			||||||
	tempParsedLines.sort((a, b) => a.time - b.time)
 | 
						tempParsedLines.sort((a, b) => a.time - b.time)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const finalLines: (LyricsLine | GapLine)[] = []
 | 
						const finalLines: (LyricsLine | GapLine)[] = []
 | 
				
			||||||
	const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
 | 
						const lyricLines = tempParsedLines.filter(
 | 
				
			||||||
	const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
 | 
							(line) => line.type === 'lyric',
 | 
				
			||||||
 | 
						) as LyricsLine[]
 | 
				
			||||||
 | 
						const gapLines = tempParsedLines.filter(
 | 
				
			||||||
 | 
							(line) => line.type === 'gap',
 | 
				
			||||||
 | 
						) as GapLine[]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (lyricLines.length === 0) return tempParsedLines
 | 
						if (lyricLines.length === 0) {
 | 
				
			||||||
 | 
							return tempParsedLines
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for (let i = 0; i < gapLines.length; i++) {
 | 
						for (let i = 0; i < gapLines.length; i++) {
 | 
				
			||||||
		const gapLine = gapLines[i]
 | 
							const gapLine = gapLines[i]
 | 
				
			||||||
		const nextLyricLine = lyricLines.find(lyric => lyric.time > gapLine.time)
 | 
							const nextLyricLine = lyricLines.find((lyric) => lyric.time > gapLine.time)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (nextLyricLine) {
 | 
							if (nextLyricLine) {
 | 
				
			||||||
			const duration = nextLyricLine.time - gapLine.time
 | 
								const duration = nextLyricLine.time - gapLine.time
 | 
				
			||||||
| 
						 | 
					@ -229,16 +254,20 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
 | 
				
			||||||
		type: 'lyric',
 | 
							type: 'lyric',
 | 
				
			||||||
		time: 0,
 | 
							time: 0,
 | 
				
			||||||
		text: '',
 | 
							text: '',
 | 
				
			||||||
		originalTime: '[00:00]'
 | 
							originalTime: '[00:00]',
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	return sortedLines
 | 
						return sortedLines
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 查找当前行索引
 | 
					// 查找当前行索引
 | 
				
			||||||
function findCurrentLineIndex(time: number): number {
 | 
					function findCurrentLineIndex(time: number): number {
 | 
				
			||||||
	if (parsedLyrics.value.length === 0) return -1
 | 
						if (parsedLyrics.value.length === 0) {
 | 
				
			||||||
 | 
							return -1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// 如果时间小于第一句歌词,则返回0(空行)
 | 
						// 如果时间小于第一句歌词,则返回0(空行)
 | 
				
			||||||
	if (time < parsedLyrics.value[1]?.time) return 0
 | 
						if (time < parsedLyrics.value[1]?.time) {
 | 
				
			||||||
 | 
							return 0
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	let index = 0
 | 
						let index = 0
 | 
				
			||||||
	for (let i = 1; i < parsedLyrics.value.length; i++) {
 | 
						for (let i = 1; i < parsedLyrics.value.length; i++) {
 | 
				
			||||||
		if (time >= parsedLyrics.value[i].time) {
 | 
							if (time >= parsedLyrics.value[i].time) {
 | 
				
			||||||
| 
						 | 
					@ -252,7 +281,13 @@ function findCurrentLineIndex(time: number): number {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 使用 GSAP 滚动到指定行
 | 
					// 使用 GSAP 滚动到指定行
 | 
				
			||||||
function scrollToLine(lineIndex: number, smooth = true) {
 | 
					function scrollToLine(lineIndex: number, smooth = true) {
 | 
				
			||||||
	if (!lyricsContainer.value || !lyricsWrapper.value || !lineRefs.value[lineIndex]) return
 | 
						if (
 | 
				
			||||||
 | 
							!lyricsContainer.value ||
 | 
				
			||||||
 | 
							!lyricsWrapper.value ||
 | 
				
			||||||
 | 
							!lineRefs.value[lineIndex]
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const container = lyricsContainer.value
 | 
						const container = lyricsContainer.value
 | 
				
			||||||
	const wrapper = lyricsWrapper.value
 | 
						const wrapper = lyricsWrapper.value
 | 
				
			||||||
| 
						 | 
					@ -276,10 +311,10 @@ function scrollToLine(lineIndex: number, smooth = true) {
 | 
				
			||||||
		scrollTween = gsap.to(wrapper, {
 | 
							scrollTween = gsap.to(wrapper, {
 | 
				
			||||||
			y: targetY,
 | 
								y: targetY,
 | 
				
			||||||
			duration: 0.8,
 | 
								duration: 0.8,
 | 
				
			||||||
			ease: "power2.out",
 | 
								ease: 'power2.out',
 | 
				
			||||||
			onComplete: () => {
 | 
								onComplete: () => {
 | 
				
			||||||
				scrollTween = null
 | 
									scrollTween = null
 | 
				
			||||||
			}
 | 
								},
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		gsap.set(wrapper, { y: targetY })
 | 
							gsap.set(wrapper, { y: targetY })
 | 
				
			||||||
| 
						 | 
					@ -288,7 +323,9 @@ function scrollToLine(lineIndex: number, smooth = true) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 高亮当前行动画
 | 
					// 高亮当前行动画
 | 
				
			||||||
function highlightCurrentLine(lineIndex: number) {
 | 
					function highlightCurrentLine(lineIndex: number) {
 | 
				
			||||||
	if (!lineRefs.value[lineIndex]) return
 | 
						if (!lineRefs.value[lineIndex]) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const lineElement = lineRefs.value[lineIndex]
 | 
						const lineElement = lineRefs.value[lineIndex]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -304,7 +341,7 @@ function highlightCurrentLine(lineIndex: number) {
 | 
				
			||||||
				scale: 1,
 | 
									scale: 1,
 | 
				
			||||||
				opacity: index < lineIndex ? 0.6 : 0.4,
 | 
									opacity: index < lineIndex ? 0.6 : 0.4,
 | 
				
			||||||
				duration: 0.3,
 | 
									duration: 0.3,
 | 
				
			||||||
				ease: "power2.out"
 | 
									ease: 'power2.out',
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
| 
						 | 
					@ -314,18 +351,21 @@ function highlightCurrentLine(lineIndex: number) {
 | 
				
			||||||
		scale: 1.05,
 | 
							scale: 1.05,
 | 
				
			||||||
		opacity: 1,
 | 
							opacity: 1,
 | 
				
			||||||
		duration: 0.2,
 | 
							duration: 0.2,
 | 
				
			||||||
		ease: "back.out(1.7)",
 | 
							ease: 'back.out(1.7)',
 | 
				
			||||||
		onComplete: () => {
 | 
							onComplete: () => {
 | 
				
			||||||
			highlightTween = null
 | 
								highlightTween = null
 | 
				
			||||||
		}
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理鼠标滚轮
 | 
					// 处理鼠标滚轮
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function handleWheel(event: WheelEvent) {
 | 
					function handleWheel(event: WheelEvent) {
 | 
				
			||||||
	event.preventDefault()
 | 
						event.preventDefault()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!lyricsWrapper.value || !lyricsContainer.value) return
 | 
						if (!lyricsWrapper.value || !lyricsContainer.value) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	userScrolling.value = true
 | 
						userScrolling.value = true
 | 
				
			||||||
	autoScroll.value = false
 | 
						autoScroll.value = false
 | 
				
			||||||
| 
						 | 
					@ -334,7 +374,7 @@ function handleWheel(event: WheelEvent) {
 | 
				
			||||||
		scrollTween.kill()
 | 
							scrollTween.kill()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const currentY = gsap.getProperty(lyricsWrapper.value, "y") as number
 | 
						const currentY = gsap.getProperty(lyricsWrapper.value, 'y') as number
 | 
				
			||||||
	const newY = currentY - event.deltaY * 0.5
 | 
						const newY = currentY - event.deltaY * 0.5
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 修正滚动范围计算
 | 
						// 修正滚动范围计算
 | 
				
			||||||
| 
						 | 
					@ -347,7 +387,7 @@ function handleWheel(event: WheelEvent) {
 | 
				
			||||||
	gsap.to(lyricsWrapper.value, {
 | 
						gsap.to(lyricsWrapper.value, {
 | 
				
			||||||
		y: limitedY,
 | 
							y: limitedY,
 | 
				
			||||||
		duration: 0.1,
 | 
							duration: 0.1,
 | 
				
			||||||
		ease: "power2.out"
 | 
							ease: 'power2.out',
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (userScrollTimeout) {
 | 
						if (userScrollTimeout) {
 | 
				
			||||||
| 
						 | 
					@ -365,6 +405,7 @@ function handleWheel(event: WheelEvent) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理歌词行点击
 | 
					// 处理歌词行点击
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function handleLineClick(line: LyricsLine | GapLine, index: number) {
 | 
					function handleLineClick(line: LyricsLine | GapLine, index: number) {
 | 
				
			||||||
	if (line.type === 'lyric') {
 | 
						if (line.type === 'lyric') {
 | 
				
			||||||
		console.log('Jump to time:', line.time)
 | 
							console.log('Jump to time:', line.time)
 | 
				
			||||||
| 
						 | 
					@ -377,15 +418,16 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 添加点击反馈动画
 | 
						// 添加点击反馈动画
 | 
				
			||||||
	if (lineRefs.value[index]) {
 | 
						if (lineRefs.value[index]) {
 | 
				
			||||||
		gsap.fromTo(lineRefs.value[index],
 | 
							gsap.fromTo(
 | 
				
			||||||
 | 
								lineRefs.value[index],
 | 
				
			||||||
			{ scale: 1 },
 | 
								{ scale: 1 },
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				scale: 1.1,
 | 
									scale: 1.1,
 | 
				
			||||||
				duration: 0.1,
 | 
									duration: 0.1,
 | 
				
			||||||
				yoyo: true,
 | 
									yoyo: true,
 | 
				
			||||||
				repeat: 1,
 | 
									repeat: 1,
 | 
				
			||||||
				ease: "power2.inOut"
 | 
									ease: 'power2.inOut',
 | 
				
			||||||
			}
 | 
								},
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -397,15 +439,16 @@ function toggleAutoScroll() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 按钮点击动画
 | 
						// 按钮点击动画
 | 
				
			||||||
	if (controlPanel.value) {
 | 
						if (controlPanel.value) {
 | 
				
			||||||
		gsap.fromTo(controlPanel.value.children[0],
 | 
							gsap.fromTo(
 | 
				
			||||||
 | 
								controlPanel.value.children[0],
 | 
				
			||||||
			{ scale: 1 },
 | 
								{ scale: 1 },
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				scale: 0.95,
 | 
									scale: 0.95,
 | 
				
			||||||
				duration: 0.1,
 | 
									duration: 0.1,
 | 
				
			||||||
				yoyo: true,
 | 
									yoyo: true,
 | 
				
			||||||
				repeat: 1,
 | 
									repeat: 1,
 | 
				
			||||||
				ease: "power2.inOut"
 | 
									ease: 'power2.inOut',
 | 
				
			||||||
			}
 | 
								},
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -418,17 +461,23 @@ function toggleAutoScroll() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 重置滚动
 | 
					// 重置滚动
 | 
				
			||||||
function resetScroll() {
 | 
					function resetScroll() {
 | 
				
			||||||
	if (!lyricsWrapper.value) return
 | 
						if (!lyricsWrapper.value) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 停止所有动画
 | 
						// 停止所有动画
 | 
				
			||||||
	if (scrollTween) scrollTween.kill()
 | 
						if (scrollTween) {
 | 
				
			||||||
	if (highlightTween) highlightTween.kill()
 | 
							scrollTween.kill()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (highlightTween) {
 | 
				
			||||||
 | 
							highlightTween.kill()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 重置位置
 | 
						// 重置位置
 | 
				
			||||||
	gsap.to(lyricsWrapper.value, {
 | 
						gsap.to(lyricsWrapper.value, {
 | 
				
			||||||
		y: 0,
 | 
							y: 0,
 | 
				
			||||||
		duration: 0.3,
 | 
							duration: 0.3,
 | 
				
			||||||
		ease: "power2.out"
 | 
							ease: 'power2.out',
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	autoScroll.value = true
 | 
						autoScroll.value = true
 | 
				
			||||||
| 
						 | 
					@ -436,15 +485,16 @@ function resetScroll() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 按钮点击动画
 | 
						// 按钮点击动画
 | 
				
			||||||
	if (controlPanel.value) {
 | 
						if (controlPanel.value) {
 | 
				
			||||||
		gsap.fromTo(controlPanel.value.children[1],
 | 
							gsap.fromTo(
 | 
				
			||||||
 | 
								controlPanel.value.children[1],
 | 
				
			||||||
			{ scale: 1 },
 | 
								{ scale: 1 },
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				scale: 0.95,
 | 
									scale: 0.95,
 | 
				
			||||||
				duration: 0.1,
 | 
									duration: 0.1,
 | 
				
			||||||
				yoyo: true,
 | 
									yoyo: true,
 | 
				
			||||||
				repeat: 1,
 | 
									repeat: 1,
 | 
				
			||||||
				ease: "power2.inOut"
 | 
									ease: 'power2.inOut',
 | 
				
			||||||
			}
 | 
								},
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -456,12 +506,15 @@ function resetScroll() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// gap 圆点透明度计算
 | 
					// gap 圆点透明度计算
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function getGapDotOpacities(line: GapLine) {
 | 
					function getGapDotOpacities(line: GapLine) {
 | 
				
			||||||
	// 获取 gap 的持续时间
 | 
						// 获取 gap 的持续时间
 | 
				
			||||||
	const duration = line.duration ?? 0
 | 
						const duration = line.duration ?? 0
 | 
				
			||||||
	if (duration <= 0) return [0.3, 0.3, 0.3]
 | 
						if (duration <= 0) {
 | 
				
			||||||
 | 
							return [0.3, 0.3, 0.3]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// 当前播放时间
 | 
						// 当前播放时间
 | 
				
			||||||
	const now = playQueueStore.currentTime
 | 
						const now = playStore.progress.currentTime
 | 
				
			||||||
	// gap 起止时间
 | 
						// gap 起止时间
 | 
				
			||||||
	const start = line.time
 | 
						const start = line.time
 | 
				
			||||||
	// 计算进度
 | 
						// 计算进度
 | 
				
			||||||
| 
						 | 
					@ -470,11 +523,15 @@ function getGapDotOpacities(line: GapLine) {
 | 
				
			||||||
	// 每个圆点的阈值
 | 
						// 每个圆点的阈值
 | 
				
			||||||
	const thresholds = [1 / 4, 2 / 4, 3 / 4]
 | 
						const thresholds = [1 / 4, 2 / 4, 3 / 4]
 | 
				
			||||||
	// 透明度从 0.3 到 1
 | 
						// 透明度从 0.3 到 1
 | 
				
			||||||
	return thresholds.map(t => progress >= t ? 1 : progress >= t - 1 / 3 ? 0.6 : 0.3)
 | 
						return thresholds.map((t) =>
 | 
				
			||||||
 | 
							progress >= t ? 1 : progress >= t - 1 / 3 ? 0.6 : 0.3,
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 监听播放时间变化
 | 
					// 监听播放时间变化
 | 
				
			||||||
watch(() => playQueueStore.currentTime, (time) => {
 | 
					watch(
 | 
				
			||||||
 | 
						() => playStore.progress.currentTime,
 | 
				
			||||||
 | 
						(time) => {
 | 
				
			||||||
		const newIndex = findCurrentLineIndex(time)
 | 
							const newIndex = findCurrentLineIndex(time)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (newIndex !== currentLineIndex.value && newIndex >= 0) {
 | 
							if (newIndex !== currentLineIndex.value && newIndex >= 0) {
 | 
				
			||||||
| 
						 | 
					@ -490,27 +547,35 @@ watch(() => playQueueStore.currentTime, (time) => {
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
})
 | 
						},
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 监听歌词源变化
 | 
					// 监听歌词源变化
 | 
				
			||||||
watch(() => props.lrcSrc, async (newSrc) => {
 | 
					watch(
 | 
				
			||||||
 | 
						() => props.lrcSrc,
 | 
				
			||||||
 | 
						async (newSrc) => {
 | 
				
			||||||
		console.log('Loading new lyrics from:', newSrc)
 | 
							console.log('Loading new lyrics from:', newSrc)
 | 
				
			||||||
		// 重置状态
 | 
							// 重置状态
 | 
				
			||||||
		currentLineIndex.value = -1
 | 
							currentLineIndex.value = -1
 | 
				
			||||||
		lineRefs.value = []
 | 
							lineRefs.value = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 停止所有动画
 | 
							// 停止所有动画
 | 
				
			||||||
	if (scrollTween) scrollTween.kill()
 | 
							if (scrollTween) {
 | 
				
			||||||
	if (highlightTween) highlightTween.kill()
 | 
								scrollTween.kill()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (highlightTween) {
 | 
				
			||||||
 | 
								highlightTween.kill()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (newSrc) {
 | 
							if (newSrc) {
 | 
				
			||||||
			loading.value = true
 | 
								loading.value = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 加载动画
 | 
								// 加载动画
 | 
				
			||||||
			if (loadingIndicator.value) {
 | 
								if (loadingIndicator.value) {
 | 
				
			||||||
			gsap.fromTo(loadingIndicator.value,
 | 
									gsap.fromTo(
 | 
				
			||||||
 | 
										loadingIndicator.value,
 | 
				
			||||||
					{ opacity: 0, scale: 0.8 },
 | 
										{ opacity: 0, scale: 0.8 },
 | 
				
			||||||
				{ opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" }
 | 
										{ opacity: 1, scale: 1, duration: 0.3, ease: 'back.out(1.7)' },
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -526,7 +591,6 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
				
			||||||
				if (lyricsWrapper.value) {
 | 
									if (lyricsWrapper.value) {
 | 
				
			||||||
					gsap.set(lyricsWrapper.value, { y: 0 })
 | 
										gsap.set(lyricsWrapper.value, { y: 0 })
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					 | 
				
			||||||
			} catch (error) {
 | 
								} catch (error) {
 | 
				
			||||||
				console.error('Failed to load lyrics:', error)
 | 
									console.error('Failed to load lyrics:', error)
 | 
				
			||||||
				parsedLyrics.value = []
 | 
									parsedLyrics.value = []
 | 
				
			||||||
| 
						 | 
					@ -541,8 +605,9 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
				
			||||||
				gsap.set(lyricsWrapper.value, { y: 0 })
 | 
									gsap.set(lyricsWrapper.value, { y: 0 })
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
}, { immediate: true })
 | 
						},
 | 
				
			||||||
 | 
						{ immediate: true },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 页面焦点处理函数变量声明
 | 
					// 页面焦点处理函数变量声明
 | 
				
			||||||
let handleVisibilityChange: (() => void) | null = null
 | 
					let handleVisibilityChange: (() => void) | null = null
 | 
				
			||||||
| 
						 | 
					@ -552,16 +617,28 @@ function setupPageFocusHandlers() {
 | 
				
			||||||
	handleVisibilityChange = () => {
 | 
						handleVisibilityChange = () => {
 | 
				
			||||||
		if (document.hidden) {
 | 
							if (document.hidden) {
 | 
				
			||||||
			// 页面失去焦点时暂停动画
 | 
								// 页面失去焦点时暂停动画
 | 
				
			||||||
			if (scrollTween) scrollTween.pause()
 | 
								if (scrollTween) {
 | 
				
			||||||
			if (highlightTween) highlightTween.pause()
 | 
									scrollTween.pause()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (highlightTween) {
 | 
				
			||||||
 | 
									highlightTween.pause()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// 页面重新获得焦点时恢复并重新同步
 | 
								// 页面重新获得焦点时恢复并重新同步
 | 
				
			||||||
			if (scrollTween && scrollTween.paused()) scrollTween.resume()
 | 
								if (scrollTween?.paused()) {
 | 
				
			||||||
			if (highlightTween && highlightTween.paused()) highlightTween.resume()
 | 
									scrollTween.resume()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (highlightTween?.paused()) {
 | 
				
			||||||
 | 
									highlightTween.resume()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 重新同步歌词位置
 | 
								// 重新同步歌词位置
 | 
				
			||||||
			nextTick(() => {
 | 
								nextTick(() => {
 | 
				
			||||||
				if (currentLineIndex.value >= 0 && autoScroll.value && !userScrolling.value) {
 | 
									if (
 | 
				
			||||||
 | 
										currentLineIndex.value >= 0 &&
 | 
				
			||||||
 | 
										autoScroll.value &&
 | 
				
			||||||
 | 
										!userScrolling.value
 | 
				
			||||||
 | 
									) {
 | 
				
			||||||
					scrollToLine(currentLineIndex.value, false) // 不使用动画,直接定位
 | 
										scrollToLine(currentLineIndex.value, false) // 不使用动画,直接定位
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
| 
						 | 
					@ -578,9 +655,10 @@ onMounted(() => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 控制面板入场动画
 | 
						// 控制面板入场动画
 | 
				
			||||||
	if (controlPanel.value) {
 | 
						if (controlPanel.value) {
 | 
				
			||||||
		gsap.fromTo(controlPanel.value,
 | 
							gsap.fromTo(
 | 
				
			||||||
 | 
								controlPanel.value,
 | 
				
			||||||
			{ opacity: 0, x: 20 },
 | 
								{ opacity: 0, x: 20 },
 | 
				
			||||||
			{ opacity: 0, x: 0, duration: 0.2, ease: "power2.out", delay: 0.2 }
 | 
								{ opacity: 0, x: 0, duration: 0.2, ease: 'power2.out', delay: 0.2 },
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -588,15 +666,16 @@ onMounted(() => {
 | 
				
			||||||
	nextTick(() => {
 | 
						nextTick(() => {
 | 
				
			||||||
		lineRefs.value.forEach((el, index) => {
 | 
							lineRefs.value.forEach((el, index) => {
 | 
				
			||||||
			if (el) {
 | 
								if (el) {
 | 
				
			||||||
				gsap.fromTo(el,
 | 
									gsap.fromTo(
 | 
				
			||||||
 | 
										el,
 | 
				
			||||||
					{ opacity: 0, y: 30 },
 | 
										{ opacity: 0, y: 30 },
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						opacity: 1,
 | 
											opacity: 1,
 | 
				
			||||||
						y: 0,
 | 
											y: 0,
 | 
				
			||||||
						duration: 0.2,
 | 
											duration: 0.2,
 | 
				
			||||||
						ease: "power2.out",
 | 
											ease: 'power2.out',
 | 
				
			||||||
						delay: index * 0.1
 | 
											delay: index * 0.1,
 | 
				
			||||||
					}
 | 
										},
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
| 
						 | 
					@ -605,9 +684,15 @@ onMounted(() => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 组件卸载时清理
 | 
					// 组件卸载时清理
 | 
				
			||||||
onUnmounted(() => {
 | 
					onUnmounted(() => {
 | 
				
			||||||
	if (scrollTween) scrollTween.kill()
 | 
						if (scrollTween) {
 | 
				
			||||||
	if (highlightTween) highlightTween.kill()
 | 
							scrollTween.kill()
 | 
				
			||||||
	if (userScrollTimeout) clearTimeout(userScrollTimeout)
 | 
						}
 | 
				
			||||||
 | 
						if (highlightTween) {
 | 
				
			||||||
 | 
							highlightTween.kill()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (userScrollTimeout) {
 | 
				
			||||||
 | 
							clearTimeout(userScrollTimeout)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 清理页面焦点事件监听器
 | 
						// 清理页面焦点事件监听器
 | 
				
			||||||
	if (handleVisibilityChange) {
 | 
						if (handleVisibilityChange) {
 | 
				
			||||||
| 
						 | 
					@ -620,7 +705,10 @@ defineExpose({
 | 
				
			||||||
	scrollToLine,
 | 
						scrollToLine,
 | 
				
			||||||
	toggleAutoScroll,
 | 
						toggleAutoScroll,
 | 
				
			||||||
	resetScroll,
 | 
						resetScroll,
 | 
				
			||||||
	getCurrentLine: () => currentLineIndex.value >= 0 ? parsedLyrics.value[currentLineIndex.value] : null
 | 
						getCurrentLine: () =>
 | 
				
			||||||
 | 
							currentLineIndex.value >= 0
 | 
				
			||||||
 | 
								? parsedLyrics.value[currentLineIndex.value]
 | 
				
			||||||
 | 
								: null,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
	import { RouterLink } from 'vue-router'
 | 
					import { RouterLink } from 'vue-router'
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,7 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { artistsOrganize } from '../utils'
 | 
					import { artistsOrganize } from '../utils'
 | 
				
			||||||
import { ref } from 'vue'
 | 
					import { ref } from 'vue'
 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
import { useToast } from 'vue-toast-notification'
 | 
					 | 
				
			||||||
import { useFavourites } from '../stores/useFavourites'
 | 
					import { useFavourites } from '../stores/useFavourites'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import QueueAddIcon from '../assets/icons/queueadd.vue'
 | 
					import QueueAddIcon from '../assets/icons/queueadd.vue'
 | 
				
			||||||
| 
						 | 
					@ -10,30 +9,24 @@ import StarEmptyIcon from '../assets/icons/starempty.vue'
 | 
				
			||||||
import StarFilledIcon from '../assets/icons/starfilled.vue'
 | 
					import StarFilledIcon from '../assets/icons/starfilled.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = defineProps<{
 | 
					const props = defineProps<{
 | 
				
			||||||
	album?: Album,
 | 
						album?: Album
 | 
				
			||||||
	track: Song,
 | 
						track: Song
 | 
				
			||||||
	index: number,
 | 
						index: number
 | 
				
			||||||
	playfrom: (index: number) => void,
 | 
						playfrom: (index: number) => void
 | 
				
			||||||
}>()
 | 
					}>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const hover = ref(false)
 | 
					const hover = ref(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const playQueueStore = usePlayQueueStore()
 | 
					const playStore = usePlayStore()
 | 
				
			||||||
const toast = useToast()
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const favourites = useFavourites()
 | 
					const favourites = useFavourites()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function appendToQueue() {
 | 
					function appendToQueue() {
 | 
				
			||||||
	console.log('aaa')
 | 
						playStore.appendItem({
 | 
				
			||||||
	let queue = playQueueStore.list
 | 
					 | 
				
			||||||
	queue.push({
 | 
					 | 
				
			||||||
		song: props.track,
 | 
							song: props.track,
 | 
				
			||||||
		album: props.album,
 | 
							album: props.album,
 | 
				
			||||||
	} as QueueItem)
 | 
					 | 
				
			||||||
	playQueueStore.list = queue
 | 
					 | 
				
			||||||
	playQueueStore.queueReplaceLock = true
 | 
					 | 
				
			||||||
	toast.success('已添加到播放队列末尾', {
 | 
					 | 
				
			||||||
		position: 'top-right',
 | 
					 | 
				
			||||||
		duration: 1000,
 | 
					 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,23 +7,20 @@ import 'vue-toast-notification/dist/theme-default.css'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import App from './App.vue'
 | 
					import App from './App.vue'
 | 
				
			||||||
import HomePage from './pages/Home.vue'
 | 
					import HomePage from './pages/Home.vue'
 | 
				
			||||||
import AlbumDetailView from './pages/AlbumDetail.vue'
 | 
					 | 
				
			||||||
import Playroom from './pages/Playroom.vue'
 | 
					import Playroom from './pages/Playroom.vue'
 | 
				
			||||||
import Library from './pages/Library.vue'
 | 
					import Library from './pages/Library.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const routes = [
 | 
					const routes = [
 | 
				
			||||||
	{ path: '/', component: HomePage },
 | 
						{ path: '/', component: HomePage },
 | 
				
			||||||
  { path: '/albums/:albumId', component: AlbumDetailView },
 | 
					 | 
				
			||||||
	{ path: '/playroom', component: Playroom },
 | 
						{ path: '/playroom', component: Playroom },
 | 
				
			||||||
  { path: '/library', component: Library }
 | 
						{ path: '/library', component: Library },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = createRouter({
 | 
					const router = createRouter({
 | 
				
			||||||
	history: createWebHashHistory(),
 | 
						history: createWebHashHistory(),
 | 
				
			||||||
  routes
 | 
						routes,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const pinia = createPinia()
 | 
					const pinia = createPinia()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
createApp(App).use(router).use(pinia).use(ToastPlugin).mount('#app')
 | 
					createApp(App).use(router).use(pinia).use(ToastPlugin).mount('#app')
 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,107 +0,0 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					 | 
				
			||||||
import { ref, onMounted } from 'vue'
 | 
					 | 
				
			||||||
import apis from '../apis'
 | 
					 | 
				
			||||||
import { useRoute } from 'vue-router'
 | 
					 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					 | 
				
			||||||
import { artistsOrganize } from '../utils'
 | 
					 | 
				
			||||||
import TrackItem from '../components/TrackItem.vue'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import PlayIcon from '../assets/icons/play.vue'
 | 
					 | 
				
			||||||
import StarEmptyIcon from '../assets/icons/starempty.vue'
 | 
					 | 
				
			||||||
import ShuffleIcon from '../assets/icons/shuffle.vue'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const album = ref<Album>()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const route = useRoute()
 | 
					 | 
				
			||||||
const albumId = route.params.albumId
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const playQueue = usePlayQueueStore()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(async () => {
 | 
					 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		let res = await apis.getAlbum(albumId as string)
 | 
					 | 
				
			||||||
		for (const track in res.songs) {
 | 
					 | 
				
			||||||
			res.songs[parseInt(track)] = await apis.getSong(res.songs[parseInt(track)].cid)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		album.value = res
 | 
					 | 
				
			||||||
		console.log(res)
 | 
					 | 
				
			||||||
	} catch (error) {
 | 
					 | 
				
			||||||
		console.log(error)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function playTheAlbum(from: number = 0) {
 | 
					 | 
				
			||||||
	if (playQueue.queueReplaceLock) {
 | 
					 | 
				
			||||||
		if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
 | 
					 | 
				
			||||||
		playQueue.queueReplaceLock = false
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	let newPlayQueue = []
 | 
					 | 
				
			||||||
	for (const track of album.value?.songs ?? []) {
 | 
					 | 
				
			||||||
		console.log(track)
 | 
					 | 
				
			||||||
		newPlayQueue.push({
 | 
					 | 
				
			||||||
			song: track,
 | 
					 | 
				
			||||||
			album: album.value
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	playQueue.playMode.shuffle = false
 | 
					 | 
				
			||||||
	playQueue.list = newPlayQueue
 | 
					 | 
				
			||||||
	playQueue.currentIndex = from
 | 
					 | 
				
			||||||
	playQueue.isPlaying = true
 | 
					 | 
				
			||||||
	playQueue.isBuffering = true
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
	<div class="px-4 md:px-8 flex gap-8 flex-col md:flex-row select-none mt-[6.625rem]">
 | 
					 | 
				
			||||||
		<div class="mx-auto md:mx-0 md:w-72">
 | 
					 | 
				
			||||||
			<div class="md:sticky md:top-[6.625rem] flex flex-col gap-8">
 | 
					 | 
				
			||||||
				<div
 | 
					 | 
				
			||||||
					class="border border-[#5b5b5b] rounded-md overflow-hidden shadow-2xl bg-neutral-800 sticky w-48 mx-auto md:w-72">
 | 
					 | 
				
			||||||
					<img :src="album?.coverUrl" class="md:w-72 md:h-72 w-48 h-48 object-contain" />
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
				<div class="flex flex-col gap-2 text-center md:text-left">
 | 
					 | 
				
			||||||
					<div class="text-white text-2xl font-semibold">{{ album?.name }}</div>
 | 
					 | 
				
			||||||
					<div class="text-sky-200 text-xl">{{ artistsOrganize(album?.artistes ?? []) }}</div>
 | 
					 | 
				
			||||||
					<div class="text-white/50 text-sm">{{ album?.intro }}</div>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		<div class="flex-1 flex flex-col gap-8 mb-2">
 | 
					 | 
				
			||||||
			<div class="flex justify-between items-center">
 | 
					 | 
				
			||||||
				<div class="flex gap-2">
 | 
					 | 
				
			||||||
					<button
 | 
					 | 
				
			||||||
						class="bg-sky-500/20 hover:bg-sky-500/30 active:bg-sky-600/30 active:shadow-inner border border-[#ffffff39] rounded-full w-56 h-10 text-base text-white flex justify-center items-center gap-2"
 | 
					 | 
				
			||||||
						@click="playTheAlbum()">
 | 
					 | 
				
			||||||
						<PlayIcon :size="4" />
 | 
					 | 
				
			||||||
						<div>播放专辑</div>
 | 
					 | 
				
			||||||
					</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					<button
 | 
					 | 
				
			||||||
						class="text-white w-10 h-10 bg-white/5 border border-[#ffffff39] rounded-full flex justify-center items-center"
 | 
					 | 
				
			||||||
						@click="() => {
 | 
					 | 
				
			||||||
							playTheAlbum()
 | 
					 | 
				
			||||||
							playQueue.shuffleCurrent = true
 | 
					 | 
				
			||||||
							playQueue.playMode.shuffle = true
 | 
					 | 
				
			||||||
						}">
 | 
					 | 
				
			||||||
						<ShuffleIcon :size="4" />
 | 
					 | 
				
			||||||
					</button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					<button
 | 
					 | 
				
			||||||
						class="text-white w-10 h-10 bg-white/5 border border-[#ffffff39] rounded-full flex justify-center items-center">
 | 
					 | 
				
			||||||
						<StarEmptyIcon :size="4" />
 | 
					 | 
				
			||||||
					</button>
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				<div class="text-sm text-gray-500 font-medium">
 | 
					 | 
				
			||||||
					共 {{ album?.songs?.length ?? '?' }} 首曲目
 | 
					 | 
				
			||||||
				</div>
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
			<div class="flex flex-col gap-2">
 | 
					 | 
				
			||||||
				<TrackItem v-for="(track, index) in album?.songs" :key="track.cid" :album="album" :track="track" :index="index"
 | 
					 | 
				
			||||||
					:playfrom="playTheAlbum" />
 | 
					 | 
				
			||||||
			</div>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ import AlbumDetailDialog from '../components/AlbumDetailDialog.vue'
 | 
				
			||||||
const albums = ref([] as AlbumList)
 | 
					const albums = ref([] as AlbumList)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const presentAlbumDetailDialog = ref(false)
 | 
					const presentAlbumDetailDialog = ref(false)
 | 
				
			||||||
const presentedAlbum = ref("")
 | 
					const presentedAlbum = ref('')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onMounted(async () => {
 | 
					onMounted(async () => {
 | 
				
			||||||
	const res = await apis.getAlbums()
 | 
						const res = await apis.getAlbums()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,50 +6,57 @@ import ShuffleIcon from '../assets/icons/shuffle.vue'
 | 
				
			||||||
import { useFavourites } from '../stores/useFavourites'
 | 
					import { useFavourites } from '../stores/useFavourites'
 | 
				
			||||||
import { ref } from 'vue'
 | 
					import { ref } from 'vue'
 | 
				
			||||||
import PlayListItem from '../components/PlayListItem.vue'
 | 
					import PlayListItem from '../components/PlayListItem.vue'
 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const favourites = useFavourites()
 | 
					const favourites = useFavourites()
 | 
				
			||||||
const playQueueStore = usePlayQueueStore()
 | 
					const playQueueStore = usePlayStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const currentList = ref<'favourites' | number>('favourites')
 | 
					const currentList = ref<'favourites' | number>('favourites')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function playTheList(list: 'favourites' | number, playFrom: number = 0) {
 | 
					function playTheList(list: 'favourites' | number, playFrom = 0) {
 | 
				
			||||||
	if (playFrom < 0 || playFrom >= favourites.favouritesCount) { playFrom = 0 }
 | 
						let actualPlayFrom = playFrom
 | 
				
			||||||
 | 
						if (playFrom < 0 || playFrom >= favourites.favouritesCount) {
 | 
				
			||||||
	if (usePlayQueueStore().queueReplaceLock) {
 | 
							actualPlayFrom = 0
 | 
				
			||||||
		if (!confirm("当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?")) { return }
 | 
						}
 | 
				
			||||||
		usePlayQueueStore().queueReplaceLock = false
 | 
					
 | 
				
			||||||
 | 
						if (playQueueStore.queueReplaceLock) {
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								!confirm(
 | 
				
			||||||
 | 
									'当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?',
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
							) {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							playQueueStore.queueReplaceLock = false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	playQueueStore.list = []
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (list === 'favourites') {
 | 
						if (list === 'favourites') {
 | 
				
			||||||
		if (favourites.favouritesCount === 0) return
 | 
							if (favourites.favouritesCount === 0) return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let newPlayQueue = favourites.favourites.map(item => ({
 | 
							let newPlayQueue = favourites.favourites.map((item) => ({
 | 
				
			||||||
			song: item.song,
 | 
								song: item.song,
 | 
				
			||||||
			album: item.album
 | 
								album: item.album,
 | 
				
			||||||
		}))
 | 
							}))
 | 
				
			||||||
		playQueueStore.list = newPlayQueue.slice().reverse()
 | 
							// playQueueStore.list = newPlayQueue.slice().reverse()
 | 
				
			||||||
		playQueueStore.currentIndex = playFrom
 | 
							// playQueueStore.currentIndex = playFrom
 | 
				
			||||||
		playQueueStore.playMode.shuffle = false
 | 
							// playQueueStore.playMode.shuffle = false
 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
							// playQueueStore.isPlaying = true
 | 
				
			||||||
		playQueueStore.isBuffering = true
 | 
							// playQueueStore.isBuffering = true
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// Handle other lists if needed
 | 
							// Handle other lists if needed
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function shuffle(list: 'favourites' | number) {
 | 
					function shuffle(list: 'favourites' | number) {
 | 
				
			||||||
	playTheList(list)
 | 
						// playTheList(list)
 | 
				
			||||||
	playQueueStore.shuffleCurrent = true
 | 
						// playQueueStore.shuffleCurrent = true
 | 
				
			||||||
	playQueueStore.playMode.shuffle = false
 | 
						// playQueueStore.playMode.shuffle = false
 | 
				
			||||||
	setTimeout(() => {
 | 
						// setTimeout(() => {
 | 
				
			||||||
		playQueueStore.playMode.shuffle = true
 | 
						// 	playQueueStore.playMode.shuffle = true
 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
						// 	playQueueStore.isPlaying = true
 | 
				
			||||||
		playQueueStore.isBuffering = true
 | 
						// 	playQueueStore.isBuffering = true
 | 
				
			||||||
	}, 100)
 | 
						// }, 100)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,7 @@
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
					import { usePlayStore } from '../stores/usePlayStore'
 | 
				
			||||||
import { artistsOrganize } from '../utils'
 | 
					 | 
				
			||||||
import gsap from 'gsap'
 | 
					import gsap from 'gsap'
 | 
				
			||||||
import { Draggable } from "gsap/Draggable"
 | 
					import { Draggable } from 'gsap/Draggable'
 | 
				
			||||||
import { onMounted, onUnmounted, nextTick } from 'vue'
 | 
					import { onMounted, onUnmounted, nextTick } from 'vue'
 | 
				
			||||||
import { useTemplateRef } from 'vue'
 | 
					import { useTemplateRef } from 'vue'
 | 
				
			||||||
import { ref, watch } from 'vue'
 | 
					import { ref, watch } from 'vue'
 | 
				
			||||||
| 
						 | 
					@ -30,8 +29,9 @@ import SpeakerIcon from '../assets/icons/speaker.vue'
 | 
				
			||||||
import MuscialNoteSparklingIcon from '../assets/icons/musicalnotesparkling.vue'
 | 
					import MuscialNoteSparklingIcon from '../assets/icons/musicalnotesparkling.vue'
 | 
				
			||||||
import CastEmptyIcon from '../assets/icons/castempty.vue'
 | 
					import CastEmptyIcon from '../assets/icons/castempty.vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const playQueueStore = usePlayQueueStore()
 | 
					const playStore = usePlayStore()
 | 
				
			||||||
const preferences = usePreferences()
 | 
					const preferences = usePreferences()
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const favourites = useFavourites()
 | 
					const favourites = useFavourites()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gsap.registerPlugin(Draggable)
 | 
					gsap.registerPlugin(Draggable)
 | 
				
			||||||
| 
						 | 
					@ -52,6 +52,7 @@ const volumeSliderContainer = useTemplateRef('volumeSliderContainer')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const presentQueueListDialog = ref(false)
 | 
					const presentQueueListDialog = ref(false)
 | 
				
			||||||
const presentLyrics = ref(false)
 | 
					const presentLyrics = ref(false)
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
const showLyricsTooltip = ref(false)
 | 
					const showLyricsTooltip = ref(false)
 | 
				
			||||||
const showMoreOptions = ref(false)
 | 
					const showMoreOptions = ref(false)
 | 
				
			||||||
const presentVolumeControl = ref(false)
 | 
					const presentVolumeControl = ref(false)
 | 
				
			||||||
| 
						 | 
					@ -66,9 +67,10 @@ onMounted(async () => {
 | 
				
			||||||
		onDrag: function () {
 | 
							onDrag: function () {
 | 
				
			||||||
			const thumbPosition = this.x
 | 
								const thumbPosition = this.x
 | 
				
			||||||
			const containerWidth = progressBarContainer.value?.clientWidth || 0
 | 
								const containerWidth = progressBarContainer.value?.clientWidth || 0
 | 
				
			||||||
			const newTime = (thumbPosition / containerWidth) * playQueueStore.duration
 | 
								const newTime =
 | 
				
			||||||
			playQueueStore.updatedCurrentTime = newTime
 | 
									(thumbPosition / containerWidth) * playStore.progress.duration
 | 
				
			||||||
		}
 | 
								playStore.updateCurrentTime(newTime)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 等待DOM完全渲染后再初始化拖拽
 | 
						// 等待DOM完全渲染后再初始化拖拽
 | 
				
			||||||
| 
						 | 
					@ -77,7 +79,7 @@ onMounted(async () => {
 | 
				
			||||||
	// 初始化音量从localStorage或默认值
 | 
						// 初始化音量从localStorage或默认值
 | 
				
			||||||
	const savedVolume = localStorage.getItem('audioVolume')
 | 
						const savedVolume = localStorage.getItem('audioVolume')
 | 
				
			||||||
	if (savedVolume) {
 | 
						if (savedVolume) {
 | 
				
			||||||
		volume.value = parseFloat(savedVolume)
 | 
							volume.value = Number.parseFloat(savedVolume)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	thumbUpdate()
 | 
						thumbUpdate()
 | 
				
			||||||
| 
						 | 
					@ -88,25 +90,33 @@ onMounted(async () => {
 | 
				
			||||||
	setupPageFocusHandlers()
 | 
						setupPageFocusHandlers()
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function timeFormatter(time: number) {
 | 
					function timeFormatter(time: number) {
 | 
				
			||||||
	const timeInSeconds = Math.floor(time)
 | 
						const timeInSeconds = Math.floor(time)
 | 
				
			||||||
	if (timeInSeconds < 0) { return '-:--' }
 | 
						if (timeInSeconds < 0) {
 | 
				
			||||||
 | 
							return '-:--'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	const minutes = Math.floor(timeInSeconds / 60)
 | 
						const minutes = Math.floor(timeInSeconds / 60)
 | 
				
			||||||
	const seconds = Math.floor(timeInSeconds % 60)
 | 
						const seconds = Math.floor(timeInSeconds % 60)
 | 
				
			||||||
	if (Number.isNaN(minutes) || Number.isNaN(seconds)) { return '-:--' }
 | 
						if (Number.isNaN(minutes) || Number.isNaN(seconds)) {
 | 
				
			||||||
 | 
							return '-:--'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
 | 
						return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 监听播放进度,更新进度条
 | 
					// 监听播放进度,更新进度条
 | 
				
			||||||
watch(() => playQueueStore.currentTime, () => {
 | 
					watch(
 | 
				
			||||||
 | 
						() => playStore.progress.currentTime,
 | 
				
			||||||
 | 
						() => {
 | 
				
			||||||
		thumbUpdate()
 | 
							thumbUpdate()
 | 
				
			||||||
})
 | 
						},
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function thumbUpdate() {
 | 
					function thumbUpdate() {
 | 
				
			||||||
	const progress = playQueueStore.currentTime / playQueueStore.duration
 | 
						const progress = playStore.progress.percentage
 | 
				
			||||||
	const containerWidth = progressBarContainer.value?.clientWidth || 0
 | 
						const containerWidth = progressBarContainer.value?.clientWidth || 0
 | 
				
			||||||
	const thumbWidth = progressBarThumb.value?.clientWidth || 0
 | 
						const thumbWidth = progressBarThumb.value?.clientWidth || 0
 | 
				
			||||||
	const newPosition = (containerWidth - thumbWidth) * progress
 | 
						const newPosition = ((containerWidth - thumbWidth) * progress) / 100
 | 
				
			||||||
	gsap.to(progressBarThumb.value, { x: newPosition, duration: 0.1 })
 | 
						gsap.to(progressBarThumb.value, { x: newPosition, duration: 0.1 })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -117,6 +127,7 @@ function volumeThumbUpdate() {
 | 
				
			||||||
	gsap.to(volumeSliderThumb.value, { x: newPosition, duration: 0.1 })
 | 
						gsap.to(volumeSliderThumb.value, { x: newPosition, duration: 0.1 })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function toggleVolumeControl() {
 | 
					function toggleVolumeControl() {
 | 
				
			||||||
	if (!presentVolumeControl.value) {
 | 
						if (!presentVolumeControl.value) {
 | 
				
			||||||
		presentVolumeControl.value = true
 | 
							presentVolumeControl.value = true
 | 
				
			||||||
| 
						 | 
					@ -152,7 +163,10 @@ function createVolumeDraggable() {
 | 
				
			||||||
			const containerWidth = volumeSliderContainer.value?.clientWidth || 0
 | 
								const containerWidth = volumeSliderContainer.value?.clientWidth || 0
 | 
				
			||||||
			const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
 | 
								const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
 | 
				
			||||||
			// 确保音量值在0-1之间
 | 
								// 确保音量值在0-1之间
 | 
				
			||||||
			const newVolume = Math.max(0, Math.min(1, thumbPosition / (containerWidth - thumbWidth)))
 | 
								const newVolume = Math.max(
 | 
				
			||||||
 | 
									0,
 | 
				
			||||||
 | 
									Math.min(1, thumbPosition / (containerWidth - thumbWidth)),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			volume.value = newVolume
 | 
								volume.value = newVolume
 | 
				
			||||||
			updateAudioVolume()
 | 
								updateAudioVolume()
 | 
				
			||||||
			// 保存音量到localStorage
 | 
								// 保存音量到localStorage
 | 
				
			||||||
| 
						 | 
					@ -161,7 +175,7 @@ function createVolumeDraggable() {
 | 
				
			||||||
		onDragEnd: () => {
 | 
							onDragEnd: () => {
 | 
				
			||||||
			// 拖拽结束时也保存一次
 | 
								// 拖拽结束时也保存一次
 | 
				
			||||||
			localStorage.setItem('audioVolume', volume.value.toString())
 | 
								localStorage.setItem('audioVolume', volume.value.toString())
 | 
				
			||||||
		}
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	console.log('Volume draggable created successfully')
 | 
						console.log('Volume draggable created successfully')
 | 
				
			||||||
| 
						 | 
					@ -175,80 +189,67 @@ function updateAudioVolume() {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
				
			||||||
function formatDetector() {
 | 
					function formatDetector() {
 | 
				
			||||||
	const format = playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl?.split('.').pop()
 | 
						const format = playStore.currentTrack?.url?.split('.').pop()
 | 
				
			||||||
	if (format === 'mp3') { return 'MP3' }
 | 
						if (format === 'mp3') {
 | 
				
			||||||
	if (format === 'flac') { return 'FLAC' }
 | 
							return 'MP3'
 | 
				
			||||||
	if (format === 'm4a') { return 'M4A' }
 | 
						}
 | 
				
			||||||
	if (format === 'ape') { return 'APE' }
 | 
						if (format === 'flac') {
 | 
				
			||||||
	if (format === 'wav') { return 'WAV' }
 | 
							return 'FLAC'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (format === 'm4a') {
 | 
				
			||||||
 | 
							return 'M4A'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (format === 'ape') {
 | 
				
			||||||
 | 
							return 'APE'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (format === 'wav') {
 | 
				
			||||||
 | 
							return 'WAV'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return '未知格式'
 | 
						return '未知格式'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function playNext() {
 | 
					 | 
				
			||||||
	if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
 | 
					 | 
				
			||||||
		console.log("at the bottom, pause")
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex = 0
 | 
					 | 
				
			||||||
		playQueueStore.isPlaying = false
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex++
 | 
					 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function playPrevious() {
 | 
					 | 
				
			||||||
	if (playQueueStore.currentTime < 5 && playQueueStore.currentIndex > 0) {
 | 
					 | 
				
			||||||
		playQueueStore.currentIndex--
 | 
					 | 
				
			||||||
		playQueueStore.isPlaying = true
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		playQueueStore.updatedCurrentTime = 0
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function setupEntranceAnimations() {
 | 
					function setupEntranceAnimations() {
 | 
				
			||||||
	if (controllerRef.value) {
 | 
						if (controllerRef.value) {
 | 
				
			||||||
		gsap.fromTo(controllerRef.value.children,
 | 
							gsap.fromTo(
 | 
				
			||||||
 | 
								controllerRef.value.children,
 | 
				
			||||||
			{ opacity: 0, y: 30, scale: 0.95 },
 | 
								{ opacity: 0, y: 30, scale: 0.95 },
 | 
				
			||||||
			{
 | 
								{
 | 
				
			||||||
				opacity: 1, y: 0, scale: 1,
 | 
									opacity: 1,
 | 
				
			||||||
				duration: 0.6, ease: "power2.out", stagger: 0.1
 | 
									y: 0,
 | 
				
			||||||
			}
 | 
									scale: 1,
 | 
				
			||||||
 | 
									duration: 0.6,
 | 
				
			||||||
 | 
									ease: 'power2.out',
 | 
				
			||||||
 | 
									stagger: 0.1,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (lyricsSection.value) {
 | 
						if (lyricsSection.value) {
 | 
				
			||||||
		gsap.fromTo(lyricsSection.value,
 | 
							gsap.fromTo(
 | 
				
			||||||
 | 
								lyricsSection.value,
 | 
				
			||||||
			{ opacity: 0, x: 50 },
 | 
								{ opacity: 0, x: 50 },
 | 
				
			||||||
			{ opacity: 1, x: 0, duration: 0.8, ease: "power2.out", delay: 0.3 }
 | 
								{ opacity: 1, x: 0, duration: 0.8, ease: 'power2.out', delay: 0.3 },
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handlePlayPause() {
 | 
					function handlePlayPause() {
 | 
				
			||||||
	if (playButton.value) {
 | 
						playStore.togglePlay()
 | 
				
			||||||
		gsap.to(playButton.value, {
 | 
					 | 
				
			||||||
			scale: 0.9, duration: 0.1, yoyo: true, repeat: 1,
 | 
					 | 
				
			||||||
			ease: "power2.inOut",
 | 
					 | 
				
			||||||
			onComplete: () => {
 | 
					 | 
				
			||||||
				playQueueStore.isPlaying = !playQueueStore.isPlaying
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		playQueueStore.isPlaying = !playQueueStore.isPlaying
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toggleShuffle() {
 | 
					function toggleShuffle() {
 | 
				
			||||||
	playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
 | 
						// playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
 | 
				
			||||||
	playQueueStore.shuffleCurrent = false
 | 
						// playQueueStore.shuffleCurrent = false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toggleRepeat() {
 | 
					function toggleRepeat() {
 | 
				
			||||||
	switch (playQueueStore.playMode.repeat) {
 | 
						// switch (playQueueStore.playMode.repeat) {
 | 
				
			||||||
		case 'off': playQueueStore.playMode.repeat = 'all'; break
 | 
						// 	case 'off': playQueueStore.playMode.repeat = 'all'; break
 | 
				
			||||||
		case 'all': playQueueStore.playMode.repeat = 'single'; break
 | 
						// 	case 'all': playQueueStore.playMode.repeat = 'single'; break
 | 
				
			||||||
		case 'single': playQueueStore.playMode.repeat = 'off'; break
 | 
						// 	case 'single': playQueueStore.playMode.repeat = 'off'; break
 | 
				
			||||||
	}
 | 
						// }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function makePlayQueueListPresent() {
 | 
					function makePlayQueueListPresent() {
 | 
				
			||||||
| 
						 | 
					@ -259,15 +260,26 @@ function makePlayQueueListPresent() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const tl = gsap.timeline()
 | 
							const tl = gsap.timeline()
 | 
				
			||||||
		tl.to(playQueueDialogContainer.value, {
 | 
							tl.to(playQueueDialogContainer.value, {
 | 
				
			||||||
			backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
 | 
								backgroundColor: '#17171780',
 | 
				
			||||||
		}).to(playQueueDialog.value, {
 | 
								duration: 0.3,
 | 
				
			||||||
			x: 0, duration: 0.4, ease: 'power3.out'
 | 
								ease: 'power2.out',
 | 
				
			||||||
		}, '<0.1')
 | 
							}).to(
 | 
				
			||||||
 | 
								playQueueDialog.value,
 | 
				
			||||||
 | 
								{
 | 
				
			||||||
 | 
									x: 0,
 | 
				
			||||||
 | 
									duration: 0.4,
 | 
				
			||||||
 | 
									ease: 'power3.out',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								'<0.1',
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (playQueueDialog.value.children.length > 0) {
 | 
							if (playQueueDialog.value.children.length > 0) {
 | 
				
			||||||
			tl.fromTo(playQueueDialog.value.children,
 | 
								tl.fromTo(
 | 
				
			||||||
 | 
									playQueueDialog.value.children,
 | 
				
			||||||
				{ opacity: 0, x: -20 },
 | 
									{ opacity: 0, x: -20 },
 | 
				
			||||||
				{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 }, '<0.2')
 | 
									{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 },
 | 
				
			||||||
 | 
									'<0.2',
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -285,34 +297,44 @@ function makePlayQueueListDismiss() {
 | 
				
			||||||
				gsap.set(playQueueDialog.value, { x: -384 })
 | 
									gsap.set(playQueueDialog.value, { x: -384 })
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			if (playQueueDialogContainer.value) {
 | 
								if (playQueueDialogContainer.value) {
 | 
				
			||||||
				gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
 | 
									gsap.set(playQueueDialogContainer.value, {
 | 
				
			||||||
			}
 | 
										backgroundColor: 'transparent',
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (playQueueDialog.value.children.length > 0) {
 | 
						if (playQueueDialog.value.children.length > 0) {
 | 
				
			||||||
		tl.to(playQueueDialog.value.children, {
 | 
							tl.to(playQueueDialog.value.children, {
 | 
				
			||||||
			opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03
 | 
								opacity: 0,
 | 
				
			||||||
 | 
								x: -20,
 | 
				
			||||||
 | 
								duration: 0.2,
 | 
				
			||||||
 | 
								ease: 'power2.in',
 | 
				
			||||||
 | 
								stagger: 0.03,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	tl.to(playQueueDialog.value, {
 | 
						tl.to(
 | 
				
			||||||
		x: -384, duration: 0.3, ease: 'power2.in'
 | 
							playQueueDialog.value,
 | 
				
			||||||
	}, playQueueDialog.value.children.length > 0 ? '<0.1' : '0')
 | 
							{
 | 
				
			||||||
		.to(playQueueDialogContainer.value, {
 | 
								x: -384,
 | 
				
			||||||
			backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in'
 | 
								duration: 0.3,
 | 
				
			||||||
		}, '<')
 | 
								ease: 'power2.in',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							playQueueDialog.value.children.length > 0 ? '<0.1' : '0',
 | 
				
			||||||
 | 
						).to(
 | 
				
			||||||
 | 
							playQueueDialogContainer.value,
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								backgroundColor: 'transparent',
 | 
				
			||||||
 | 
								duration: 0.2,
 | 
				
			||||||
 | 
								ease: 'power2.in',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							'<',
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getCurrentTrack() {
 | 
					function getCurrentTrack() {
 | 
				
			||||||
	if (playQueueStore.list.length === 0) {
 | 
						return playStore.currentTrack
 | 
				
			||||||
		return null
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (playQueueStore.playMode.shuffle) {
 | 
					 | 
				
			||||||
		return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return playQueueStore.list[playQueueStore.currentIndex]
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toggleMoreOptions() {
 | 
					function toggleMoreOptions() {
 | 
				
			||||||
| 
						 | 
					@ -321,15 +343,23 @@ function toggleMoreOptions() {
 | 
				
			||||||
		nextTick(() => {
 | 
							nextTick(() => {
 | 
				
			||||||
			if (moreOptionsDialog.value) {
 | 
								if (moreOptionsDialog.value) {
 | 
				
			||||||
				const tl = gsap.timeline()
 | 
									const tl = gsap.timeline()
 | 
				
			||||||
				tl.fromTo(moreOptionsDialog.value,
 | 
									tl.fromTo(
 | 
				
			||||||
 | 
										moreOptionsDialog.value,
 | 
				
			||||||
					{ opacity: 0, scale: 0.9, y: 10 },
 | 
										{ opacity: 0, scale: 0.9, y: 10 },
 | 
				
			||||||
					{ opacity: 1, scale: 1, y: 0, duration: 0.2, ease: "power2.out" }
 | 
										{ opacity: 1, scale: 1, y: 0, duration: 0.2, ease: 'power2.out' },
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
				if (moreOptionsDialog.value.children[0]?.children) {
 | 
									if (moreOptionsDialog.value.children[0]?.children) {
 | 
				
			||||||
					tl.fromTo(moreOptionsDialog.value.children[0].children,
 | 
										tl.fromTo(
 | 
				
			||||||
 | 
											moreOptionsDialog.value.children[0].children,
 | 
				
			||||||
						{ opacity: 0, x: -10 },
 | 
											{ opacity: 0, x: -10 },
 | 
				
			||||||
						{ opacity: 1, x: 0, duration: 0.15, ease: "power2.out", stagger: 0.05 },
 | 
											{
 | 
				
			||||||
						"<0.1"
 | 
												opacity: 1,
 | 
				
			||||||
 | 
												x: 0,
 | 
				
			||||||
 | 
												duration: 0.15,
 | 
				
			||||||
 | 
												ease: 'power2.out',
 | 
				
			||||||
 | 
												stagger: 0.05,
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											'<0.1',
 | 
				
			||||||
					)
 | 
										)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -339,16 +369,21 @@ function toggleMoreOptions() {
 | 
				
			||||||
			const tl = gsap.timeline({
 | 
								const tl = gsap.timeline({
 | 
				
			||||||
				onComplete: () => {
 | 
									onComplete: () => {
 | 
				
			||||||
					showMoreOptions.value = false
 | 
										showMoreOptions.value = false
 | 
				
			||||||
				}
 | 
									},
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
			if (moreOptionsDialog.value.children[0]?.children) {
 | 
								if (moreOptionsDialog.value.children[0]?.children) {
 | 
				
			||||||
				tl.to(moreOptionsDialog.value.children[0].children,
 | 
									tl.to(moreOptionsDialog.value.children[0].children, {
 | 
				
			||||||
					{ opacity: 0, x: -10, duration: 0.1, ease: "power2.in", stagger: 0.02 }
 | 
										opacity: 0,
 | 
				
			||||||
				)
 | 
										x: -10,
 | 
				
			||||||
 | 
										duration: 0.1,
 | 
				
			||||||
 | 
										ease: 'power2.in',
 | 
				
			||||||
 | 
										stagger: 0.02,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			tl.to(moreOptionsDialog.value,
 | 
								tl.to(
 | 
				
			||||||
				{ opacity: 0, scale: 0.9, y: 10, duration: 0.15, ease: "power2.in" },
 | 
									moreOptionsDialog.value,
 | 
				
			||||||
				moreOptionsDialog.value.children[0]?.children ? "<0.05" : "0"
 | 
									{ opacity: 0, scale: 0.9, y: 10, duration: 0.15, ease: 'power2.in' },
 | 
				
			||||||
 | 
									moreOptionsDialog.value.children[0]?.children ? '<0.05' : '0',
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			showMoreOptions.value = false
 | 
								showMoreOptions.value = false
 | 
				
			||||||
| 
						 | 
					@ -356,8 +391,13 @@ function toggleMoreOptions() {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newValue, oldValue) => {
 | 
					// TODO: lyrics
 | 
				
			||||||
	if (!getCurrentTrack()) { return }
 | 
					watch(
 | 
				
			||||||
 | 
						() => [preferences.presentLyrics, getCurrentTrack()?.extra?.lyric],
 | 
				
			||||||
 | 
						(newValue, oldValue) => {
 | 
				
			||||||
 | 
							if (!getCurrentTrack()) {
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const [showLyrics, hasLyricUrl] = newValue
 | 
							const [showLyrics, hasLyricUrl] = newValue
 | 
				
			||||||
		const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
 | 
							const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
 | 
				
			||||||
| 
						 | 
					@ -369,58 +409,79 @@ watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newV
 | 
				
			||||||
				const tl = gsap.timeline()
 | 
									const tl = gsap.timeline()
 | 
				
			||||||
				tl.from(controllerRef.value, {
 | 
									tl.from(controllerRef.value, {
 | 
				
			||||||
					marginRight: '-40rem',
 | 
										marginRight: '-40rem',
 | 
				
			||||||
			}).fromTo(lyricsSection.value,
 | 
									}).fromTo(
 | 
				
			||||||
 | 
										lyricsSection.value,
 | 
				
			||||||
					{ opacity: 0, x: 50, scale: 0.95 },
 | 
										{ opacity: 0, x: 50, scale: 0.95 },
 | 
				
			||||||
				{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
 | 
										{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: 'power2.out' },
 | 
				
			||||||
				"-=0.3"
 | 
										'-=0.3',
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// Hide lyrics with different animations based on reason
 | 
							// Hide lyrics with different animations based on reason
 | 
				
			||||||
		else if (presentLyrics.value) {
 | 
							else if (presentLyrics.value) {
 | 
				
			||||||
		let animationConfig
 | 
								let animationConfig: {
 | 
				
			||||||
 | 
									opacity: number
 | 
				
			||||||
 | 
									x: number
 | 
				
			||||||
 | 
									scale?: number
 | 
				
			||||||
 | 
									duration: number
 | 
				
			||||||
 | 
									ease: string
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// If lyrics were toggled off
 | 
								// If lyrics were toggled off
 | 
				
			||||||
			if (prevShowLyrics && !showLyrics) {
 | 
								if (prevShowLyrics && !showLyrics) {
 | 
				
			||||||
				animationConfig = {
 | 
									animationConfig = {
 | 
				
			||||||
				opacity: 0, x: -50, scale: 0.95,
 | 
										opacity: 0,
 | 
				
			||||||
				duration: 0.3, ease: "power2.in"
 | 
										x: -50,
 | 
				
			||||||
 | 
										scale: 0.95,
 | 
				
			||||||
 | 
										duration: 0.3,
 | 
				
			||||||
 | 
										ease: 'power2.in',
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			// If no lyrics available (song changed)
 | 
								// If no lyrics available (song changed)
 | 
				
			||||||
			else if (!hasLyricUrl) {
 | 
								else if (!hasLyricUrl) {
 | 
				
			||||||
				animationConfig = {
 | 
									animationConfig = {
 | 
				
			||||||
				opacity: 0, y: -20, scale: 0.98,
 | 
										opacity: 0,
 | 
				
			||||||
				duration: 0.3, ease: "power1.in"
 | 
										y: -20,
 | 
				
			||||||
 | 
										scale: 0.98,
 | 
				
			||||||
 | 
										duration: 0.3,
 | 
				
			||||||
 | 
										ease: 'power1.in',
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			// Default animation
 | 
								// Default animation
 | 
				
			||||||
			else {
 | 
								else {
 | 
				
			||||||
				animationConfig = {
 | 
									animationConfig = {
 | 
				
			||||||
				opacity: 0, x: -50,
 | 
										opacity: 0,
 | 
				
			||||||
				duration: 0.3, ease: "power2.in"
 | 
										x: -50,
 | 
				
			||||||
 | 
										duration: 0.3,
 | 
				
			||||||
 | 
										ease: 'power2.in',
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const tl = gsap.timeline({
 | 
								const tl = gsap.timeline({
 | 
				
			||||||
				onComplete: () => {
 | 
									onComplete: () => {
 | 
				
			||||||
					presentLyrics.value = false
 | 
										presentLyrics.value = false
 | 
				
			||||||
			}
 | 
									},
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			tl.to(controllerRef.value, {
 | 
								tl.to(controllerRef.value, {
 | 
				
			||||||
				marginLeft: '44rem',
 | 
									marginLeft: '44rem',
 | 
				
			||||||
			duration: 0.3, ease: "power2.out"
 | 
									duration: 0.3,
 | 
				
			||||||
 | 
									ease: 'power2.out',
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
				.to(lyricsSection.value, animationConfig, '<')
 | 
									.to(lyricsSection.value, animationConfig, '<')
 | 
				
			||||||
				.set(lyricsSection.value, {
 | 
									.set(lyricsSection.value, {
 | 
				
			||||||
				opacity: 1, x: 0, y: 0, scale: 1 // Reset for next time
 | 
										opacity: 1,
 | 
				
			||||||
 | 
										x: 0,
 | 
				
			||||||
 | 
										y: 0,
 | 
				
			||||||
 | 
										scale: 1, // Reset for next time
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				.set(controllerRef.value, {
 | 
									.set(controllerRef.value, {
 | 
				
			||||||
				marginLeft: '0rem' // Reset for next time
 | 
										marginLeft: '0rem', // Reset for next time
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
}, { immediate: true })
 | 
						},
 | 
				
			||||||
 | 
						{ immediate: true },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 页面焦点处理函数变量声明
 | 
					// 页面焦点处理函数变量声明
 | 
				
			||||||
let handleVisibilityChange: (() => void) | null = null
 | 
					let handleVisibilityChange: (() => void) | null = null
 | 
				
			||||||
| 
						 | 
					@ -466,7 +527,9 @@ function setupPageFocusHandlers() {
 | 
				
			||||||
// 重新同步歌词状态
 | 
					// 重新同步歌词状态
 | 
				
			||||||
function resyncLyricsState() {
 | 
					function resyncLyricsState() {
 | 
				
			||||||
	const currentTrack = getCurrentTrack()
 | 
						const currentTrack = getCurrentTrack()
 | 
				
			||||||
	if (!currentTrack) { return }
 | 
						if (!currentTrack) {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	console.log('[Playroom] 重新同步歌词状态')
 | 
						console.log('[Playroom] 重新同步歌词状态')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -474,7 +537,7 @@ function resyncLyricsState() {
 | 
				
			||||||
	if (controllerRef.value) {
 | 
						if (controllerRef.value) {
 | 
				
			||||||
		gsap.set(controllerRef.value, {
 | 
							gsap.set(controllerRef.value, {
 | 
				
			||||||
			marginLeft: '0rem',
 | 
								marginLeft: '0rem',
 | 
				
			||||||
			marginRight: '0rem' 
 | 
								marginRight: '0rem',
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -483,15 +546,19 @@ function resyncLyricsState() {
 | 
				
			||||||
			opacity: 1,
 | 
								opacity: 1,
 | 
				
			||||||
			x: 0,
 | 
								x: 0,
 | 
				
			||||||
			y: 0,
 | 
								y: 0,
 | 
				
			||||||
			scale: 1 
 | 
								scale: 1,
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检查当前歌词显示状态应该是什么
 | 
						// 检查当前歌词显示状态应该是什么
 | 
				
			||||||
	const shouldShowLyrics = preferences.presentLyrics && currentTrack.song.lyricUrl ? true : false
 | 
						const shouldShowLyrics = !!(
 | 
				
			||||||
 | 
							preferences.presentLyrics && currentTrack.extra?.lyric
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (shouldShowLyrics !== presentLyrics.value) {
 | 
						if (shouldShowLyrics !== presentLyrics.value) {
 | 
				
			||||||
		console.log(`[Playroom] 歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`)
 | 
							console.log(
 | 
				
			||||||
 | 
								`[Playroom] 歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 直接设置状态,不触发动画
 | 
							// 直接设置状态,不触发动画
 | 
				
			||||||
		presentLyrics.value = shouldShowLyrics
 | 
							presentLyrics.value = shouldShowLyrics
 | 
				
			||||||
| 
						 | 
					@ -503,11 +570,12 @@ function resyncLyricsState() {
 | 
				
			||||||
				tl.from(controllerRef.value, {
 | 
									tl.from(controllerRef.value, {
 | 
				
			||||||
					marginRight: '-40rem',
 | 
										marginRight: '-40rem',
 | 
				
			||||||
					duration: 0.4,
 | 
										duration: 0.4,
 | 
				
			||||||
					ease: "power2.out"
 | 
										ease: 'power2.out',
 | 
				
			||||||
				}).fromTo(lyricsSection.value,
 | 
									}).fromTo(
 | 
				
			||||||
 | 
										lyricsSection.value,
 | 
				
			||||||
					{ opacity: 0, x: 50, scale: 0.95 },
 | 
										{ opacity: 0, x: 50, scale: 0.95 },
 | 
				
			||||||
					{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
 | 
										{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: 'power2.out' },
 | 
				
			||||||
					"-=0.2"
 | 
										'-=0.2',
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -515,29 +583,37 @@ function resyncLyricsState() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// New: Watch for track changes and animate
 | 
					// New: Watch for track changes and animate
 | 
				
			||||||
watch(() => playQueueStore.currentIndex, () => {
 | 
					watch(
 | 
				
			||||||
 | 
						() => playStore.currentTrack,
 | 
				
			||||||
 | 
						() => {
 | 
				
			||||||
		if (albumCover.value) {
 | 
							if (albumCover.value) {
 | 
				
			||||||
			gsap.to(albumCover.value, {
 | 
								gsap.to(albumCover.value, {
 | 
				
			||||||
			scale: 0.95, opacity: 0.7, duration: 0.2,
 | 
									scale: 0.95,
 | 
				
			||||||
			ease: "power2.inOut", yoyo: true, repeat: 1
 | 
									opacity: 0.7,
 | 
				
			||||||
 | 
									duration: 0.2,
 | 
				
			||||||
 | 
									ease: 'power2.inOut',
 | 
				
			||||||
 | 
									yoyo: true,
 | 
				
			||||||
 | 
									repeat: 1,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (songInfo.value) {
 | 
							if (songInfo.value) {
 | 
				
			||||||
		gsap.fromTo(songInfo.value,
 | 
								gsap.fromTo(
 | 
				
			||||||
 | 
									songInfo.value,
 | 
				
			||||||
				{ opacity: 0, y: 10 },
 | 
									{ opacity: 0, y: 10 },
 | 
				
			||||||
			{ opacity: 1, y: 0, duration: 0.4, ease: "power2.out", delay: 0.3 }
 | 
									{ opacity: 1, y: 0, duration: 0.4, ease: 'power2.out', delay: 0.3 },
 | 
				
			||||||
			)
 | 
								)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
})
 | 
						},
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
	<div v-if="getCurrentTrack() !== null">
 | 
						<div v-if="getCurrentTrack() !== null">
 | 
				
			||||||
		<!-- Background remains unchanged -->
 | 
							<!-- Background remains unchanged -->
 | 
				
			||||||
		<div class="z-0 absolute top-0 left-0 w-screen h-screen overflow-hidden"
 | 
							<div class="z-0 absolute top-0 left-0 w-screen h-screen overflow-hidden"
 | 
				
			||||||
			v-if="getCurrentTrack()?.album?.coverDeUrl">
 | 
								v-if="getCurrentTrack()?.extra?.background">
 | 
				
			||||||
			<img class="w-full h-full blur-2xl object-cover scale-110" :src="getCurrentTrack()?.album?.coverDeUrl" />
 | 
								<img class="w-full h-full blur-2xl object-cover scale-110" :src="getCurrentTrack()?.extra?.background as string | undefined ?? ''" />
 | 
				
			||||||
			<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/5" />
 | 
								<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/5" />
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -549,9 +625,9 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
				<div class="flex flex-col w-96 gap-4" ref="controllerRef">
 | 
									<div class="flex flex-col w-96 gap-4" ref="controllerRef">
 | 
				
			||||||
					<!-- Album cover with enhanced hover effect -->
 | 
										<!-- Album cover with enhanced hover effect -->
 | 
				
			||||||
					<div ref="albumCover" class="relative">
 | 
										<div ref="albumCover" class="relative">
 | 
				
			||||||
						<img :src="getCurrentTrack()?.album?.coverUrl" class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96
 | 
											<img :src="getCurrentTrack()?.metadata?.artwork?.[0].src" class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96
 | 
				
			||||||
							transition-transform duration-300
 | 
												transition-transform duration-300
 | 
				
			||||||
							" :class="playQueueStore.isPlaying ? 'scale-100' : 'scale-85'" />
 | 
												" :class="playStore.isPlaying ? 'scale-100' : 'scale-85'" />
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<!-- Song info with enhanced styling -->
 | 
										<!-- Song info with enhanced styling -->
 | 
				
			||||||
| 
						 | 
					@ -560,26 +636,26 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
							<!-- ...existing song info code... -->
 | 
												<!-- ...existing song info code... -->
 | 
				
			||||||
							<div class="">
 | 
												<div class="">
 | 
				
			||||||
								<div class="text-black/90 blur-lg text-lg font-medium truncate w-80">
 | 
													<div class="text-black/90 blur-lg text-lg font-medium truncate w-80">
 | 
				
			||||||
									{{ getCurrentTrack()?.song.name }}
 | 
														{{ getCurrentTrack()?.metadata?.title }}
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
								<div class="text-black/90 blur-lg text-base truncate w-80">
 | 
													<div class="text-black/90 blur-lg text-base truncate w-80">
 | 
				
			||||||
									{{ getCurrentTrack()?.song.artists ?? [] }} —
 | 
														{{ getCurrentTrack()?.metadata?.artist}} —
 | 
				
			||||||
									{{ getCurrentTrack()?.album?.name ?? '未知专辑' }}
 | 
														{{ getCurrentTrack()?.metadata?.album ?? '未知专辑' }}
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							<div class="absolute top-0">
 | 
												<div class="absolute top-0">
 | 
				
			||||||
								<div class="text-white text-lg font-medium truncate w-80">
 | 
													<div class="text-white text-lg font-medium truncate w-80">
 | 
				
			||||||
									{{ getCurrentTrack()?.song.name }}
 | 
														{{ getCurrentTrack()?.metadata?.title }}
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
								<div class="text-white/75 text-base truncate w-80">
 | 
													<div class="text-white/75 text-base truncate w-80">
 | 
				
			||||||
									{{ artistsOrganize(getCurrentTrack()?.song.artists ?? []) }} —
 | 
														{{ getCurrentTrack()?.metadata?.artist }} —
 | 
				
			||||||
									{{ getCurrentTrack()?.album?.name ?? '未知专辑' }}
 | 
														{{ getCurrentTrack()?.metadata?.album ?? '未知专辑' }}
 | 
				
			||||||
								</div>
 | 
													</div>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						<button
 | 
											<!-- <button
 | 
				
			||||||
							class="h-10 w-10 flex justify-center items-center rounded-full backdrop-blur-3xl transition-all duration-200 hover:scale-110"
 | 
												class="h-10 w-10 flex justify-center items-center rounded-full backdrop-blur-3xl transition-all duration-200 hover:scale-110"
 | 
				
			||||||
							ref="favoriteButton"
 | 
												ref="favoriteButton"
 | 
				
			||||||
							@click="() => { const track = getCurrentTrack(); if (track !== null) favourites.toggleFavourite(track) }"
 | 
												@click="() => { const track = getCurrentTrack(); if (track !== null) favourites.toggleFavourite(track) }"
 | 
				
			||||||
| 
						 | 
					@ -590,7 +666,7 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
									:size="6" />
 | 
														:size="6" />
 | 
				
			||||||
								<StarEmptyIcon v-else :size="6" />
 | 
													<StarEmptyIcon v-else :size="6" />
 | 
				
			||||||
							</span>
 | 
												</span>
 | 
				
			||||||
						</button>
 | 
											</button> -->
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<!-- Progress section -->
 | 
										<!-- Progress section -->
 | 
				
			||||||
| 
						 | 
					@ -607,9 +683,9 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
							<!-- ...existing time display code... -->
 | 
												<!-- ...existing time display code... -->
 | 
				
			||||||
							<div class="font-medium flex-1 text-left text-xs relative">
 | 
												<div class="font-medium flex-1 text-left text-xs relative">
 | 
				
			||||||
								<span
 | 
													<span
 | 
				
			||||||
									class="text-black blur-lg absolute top-0 text-xs">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
														class="text-black blur-lg absolute top-0 text-xs">{{ timeFormatter(Math.floor(playStore.progress.currentTime)) }}</span>
 | 
				
			||||||
								<span
 | 
													<span
 | 
				
			||||||
									class="text-white/90 absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
														class="text-white/90 absolute top-0">{{ timeFormatter(Math.floor(playStore.progress.currentTime)) }}</span>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
							<div class="text-xs text-center relative flex-1">
 | 
												<div class="text-xs text-center relative flex-1">
 | 
				
			||||||
								<span class="text-black blur-lg absolute top-0">{{ formatDetector() }}</span>
 | 
													<span class="text-black blur-lg absolute top-0">{{ formatDetector() }}</span>
 | 
				
			||||||
| 
						 | 
					@ -621,8 +697,8 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
									class="text-white/90 text-xs font-medium text-right relative transition-colors duration-200 hover:text-white"
 | 
														class="text-white/90 text-xs font-medium text-right relative transition-colors duration-200 hover:text-white"
 | 
				
			||||||
									@click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
 | 
														@click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
 | 
				
			||||||
									<span
 | 
														<span
 | 
				
			||||||
										class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
															class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playStore.progress.duration) - Math.floor(playStore.progress.currentTime) : playStore.progress.duration)}` }}</span>
 | 
				
			||||||
									<span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
														<span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playStore.progress.duration) - Math.floor(playStore.progress.currentTime) : playStore.progress.duration)}` }}</span>
 | 
				
			||||||
								</button>
 | 
													</button>
 | 
				
			||||||
							</div>
 | 
												</div>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
| 
						 | 
					@ -684,16 +760,16 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
								class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200"
 | 
													class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200"
 | 
				
			||||||
								@click="handlePlayPause" ref="playButton">
 | 
													@click="handlePlayPause" ref="playButton">
 | 
				
			||||||
								<!-- ...existing play/pause icon code... -->
 | 
													<!-- ...existing play/pause icon code... -->
 | 
				
			||||||
								<div v-if="playQueueStore.isPlaying">
 | 
													<div v-if="playStore.isPlaying">
 | 
				
			||||||
									<div v-if="playQueueStore.isBuffering" class="w-6 h-6 relative">
 | 
														<!-- <div v-if="playQueueStore.isBuffering" class="w-6 h-6 relative">
 | 
				
			||||||
										<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
															<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
				
			||||||
											<LoadingIndicator :size="6" />
 | 
																<LoadingIndicator :size="6" />
 | 
				
			||||||
										</span>
 | 
															</span>
 | 
				
			||||||
										<span class="text-white absolute top-0 left-0">
 | 
															<span class="text-white absolute top-0 left-0">
 | 
				
			||||||
											<LoadingIndicator :size="6" />
 | 
																<LoadingIndicator :size="6" />
 | 
				
			||||||
										</span>
 | 
															</span>
 | 
				
			||||||
									</div>
 | 
														</div> -->
 | 
				
			||||||
									<div v-else class="w-8 h-8 relative">
 | 
														<div class="w-8 h-8 relative">
 | 
				
			||||||
										<span class="text-black blur-md absolute top-0 left-0">
 | 
															<span class="text-black blur-md absolute top-0 left-0">
 | 
				
			||||||
											<PauseIcon :size="8" />
 | 
																<PauseIcon :size="8" />
 | 
				
			||||||
										</span>
 | 
															</span>
 | 
				
			||||||
| 
						 | 
					@ -716,7 +792,7 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							<button
 | 
												<button
 | 
				
			||||||
								class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200 hover:scale-105"
 | 
													class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200 hover:scale-105"
 | 
				
			||||||
								@click="playNext" ref="nextButton">
 | 
													@click="playStore.skipToPrevious" ref="nextButton">
 | 
				
			||||||
								<div class="w-8 h-8 relative">
 | 
													<div class="w-8 h-8 relative">
 | 
				
			||||||
									<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
														<span class="text-black/80 blur-lg absolute top-0 left-0">
 | 
				
			||||||
										<ForwardIcon :size="8" />
 | 
															<ForwardIcon :size="8" />
 | 
				
			||||||
| 
						 | 
					@ -748,7 +824,7 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
								</button>
 | 
													</button>
 | 
				
			||||||
								<!-- Show tooltip only on hover, with transition -->
 | 
													<!-- Show tooltip only on hover, with transition -->
 | 
				
			||||||
								<transition name="lyrics-tooltip-fade">
 | 
													<transition name="lyrics-tooltip-fade">
 | 
				
			||||||
									<div v-if="showLyricsTooltip && !getCurrentTrack()?.song.lyricUrl"
 | 
														<div v-if="showLyricsTooltip && !getCurrentTrack()?.extra?.lyric"
 | 
				
			||||||
										class="absolute bottom-10 w-60 left-[-7rem] bg-black/60 backdrop-blur-3xl rounded-md p-2 text-xs flex flex-col text-left shadow-2xl border border-[#ffffff39]">
 | 
															class="absolute bottom-10 w-60 left-[-7rem] bg-black/60 backdrop-blur-3xl rounded-md p-2 text-xs flex flex-col text-left shadow-2xl border border-[#ffffff39]">
 | 
				
			||||||
										<div class="font-semibold text-white">这首曲目不提供歌词文本</div>
 | 
															<div class="font-semibold text-white">这首曲目不提供歌词文本</div>
 | 
				
			||||||
										<div class="text-white/60">启用歌词时,将会在下一首有歌词的曲目中显示歌词文本。</div>
 | 
															<div class="text-white/60">启用歌词时,将会在下一首有歌词的曲目中显示歌词文本。</div>
 | 
				
			||||||
| 
						 | 
					@ -802,7 +878,7 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<!-- Lyrics section - full screen height -->
 | 
									<!-- Lyrics section - full screen height -->
 | 
				
			||||||
				<div class="w-[40rem] h-screen" ref="lyricsSection" v-if="presentLyrics">
 | 
									<div class="w-[40rem] h-screen" ref="lyricsSection" v-if="presentLyrics">
 | 
				
			||||||
					<ScrollingLyrics :lrcSrc="getCurrentTrack()?.song.lyricUrl ?? undefined" class="h-full"
 | 
										<ScrollingLyrics :lrcSrc="getCurrentTrack()?.extra?.lyric as string ?? undefined" class="h-full"
 | 
				
			||||||
						ref="scrollingLyrics" />
 | 
											ref="scrollingLyrics" />
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					@ -826,29 +902,29 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
				
			||||||
				<div class="flex gap-2 mx-8 mb-4">
 | 
									<div class="flex gap-2 mx-8 mb-4">
 | 
				
			||||||
					<button
 | 
										<button
 | 
				
			||||||
						class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
 | 
											class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
 | 
				
			||||||
						:class="playQueueStore.playMode.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
 | 
											:class="playStore.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
 | 
				
			||||||
						@click="toggleShuffle">
 | 
											@click="toggleShuffle">
 | 
				
			||||||
						<ShuffleIcon :size="4" />
 | 
											<ShuffleIcon :size="4" />
 | 
				
			||||||
					</button>
 | 
										</button>
 | 
				
			||||||
					<button
 | 
										<button
 | 
				
			||||||
						class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
 | 
											class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
 | 
				
			||||||
						:class="playQueueStore.playMode.repeat === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'"
 | 
											:class="playStore.loop === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'"
 | 
				
			||||||
						@click="toggleRepeat">
 | 
											@click="toggleRepeat">
 | 
				
			||||||
						<CycleTwoArrowsIcon :size="4" v-if="playQueueStore.playMode.repeat !== 'single'" />
 | 
											<CycleTwoArrowsIcon :size="4" v-if="playStore.loop !== 'single_track'" />
 | 
				
			||||||
						<CycleTwoArrowsWithNumOneIcon :size="4" v-else />
 | 
											<CycleTwoArrowsWithNumOneIcon :size="4" v-else />
 | 
				
			||||||
					</button>
 | 
										</button>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<hr class="border-[#ffffff39]" />
 | 
									<hr class="border-[#ffffff39]" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playQueueStore.playMode.shuffle">
 | 
									<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playStore.shuffle">
 | 
				
			||||||
					<PlayQueueItem v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList"
 | 
										<!-- <PlayQueueItem v-for="(oriIndex, shuffledIndex) in playStore.shuffleList"
 | 
				
			||||||
						:queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex"
 | 
											:queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex"
 | 
				
			||||||
						:key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" />
 | 
											:key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" /> -->
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-else>
 | 
									<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-else>
 | 
				
			||||||
					<PlayQueueItem :queueItem="track" :isCurrent="playQueueStore.currentIndex === index"
 | 
										<!-- <PlayQueueItem :queueItem="track" :isCurrent="playQueueStore.currentIndex === index"
 | 
				
			||||||
						v-for="(track, index) in playQueueStore.list" :index="index" :key="track.song.cid" />
 | 
											v-for="(track, index) in playQueueStore.list" :index="index" :key="track.song.cid" /> -->
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</dialog>
 | 
							</dialog>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { defineStore } from "pinia"
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
import { ref, watch, computed } from "vue"
 | 
					import { ref, watch, computed } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 声明全局类型
 | 
					// 声明全局类型
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
| 
						 | 
					@ -9,19 +9,23 @@ declare global {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useFavourites = defineStore('favourites', () => {
 | 
					export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
	const favourites = ref<QueueItem[]>([])
 | 
						const favourites = ref<InternalQueueItem[]>([])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const isLoaded = ref(false)
 | 
						const isLoaded = ref(false)
 | 
				
			||||||
	const storageType = ref<'chrome' | 'localStorage' | 'memory'>('chrome')
 | 
						const storageType = ref<'chrome' | 'localStorage' | 'memory'>('chrome')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 默认收藏列表
 | 
						// 默认收藏列表
 | 
				
			||||||
	const defaultFavourites: QueueItem[] = []
 | 
						const defaultFavourites: InternalQueueItem[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检测可用的 API
 | 
						// 检测可用的 API
 | 
				
			||||||
	const detectAvailableAPIs = () => {
 | 
						const detectAvailableAPIs = () => {
 | 
				
			||||||
		// 检查原生 chrome API
 | 
							// 检查原生 chrome API
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
 | 
								if (
 | 
				
			||||||
 | 
									typeof chrome !== 'undefined' &&
 | 
				
			||||||
 | 
									chrome.storage &&
 | 
				
			||||||
 | 
									chrome.storage.local
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
				storageType.value = 'chrome'
 | 
									storageType.value = 'chrome'
 | 
				
			||||||
				return 'chrome'
 | 
									return 'chrome'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -31,7 +35,11 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 检查 window.chrome
 | 
							// 检查 window.chrome
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.local) {
 | 
								if (
 | 
				
			||||||
 | 
									window.chrome &&
 | 
				
			||||||
 | 
									window.chrome.storage &&
 | 
				
			||||||
 | 
									window.chrome.storage.local
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
				storageType.value = 'chrome'
 | 
									storageType.value = 'chrome'
 | 
				
			||||||
				return 'chrome'
 | 
									return 'chrome'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -128,10 +136,11 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 数据验证和规范化函数
 | 
						// 数据验证和规范化函数
 | 
				
			||||||
	const normalizeFavourites = (data: any[]): QueueItem[] => {
 | 
						const normalizeFavourites = (data: any[]): InternalQueueItem[] => {
 | 
				
			||||||
		if (!Array.isArray(data)) return []
 | 
							if (!Array.isArray(data)) return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return data.map(item => {
 | 
							return data
 | 
				
			||||||
 | 
								.map((item) => {
 | 
				
			||||||
				if (!item || !item.song) return null
 | 
									if (!item || !item.song) return null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// 规范化 Song 对象
 | 
									// 规范化 Song 对象
 | 
				
			||||||
| 
						 | 
					@ -144,37 +153,48 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
					mvUrl: item.song.mvUrl,
 | 
										mvUrl: item.song.mvUrl,
 | 
				
			||||||
					mvCoverUrl: item.song.mvCoverUrl,
 | 
										mvCoverUrl: item.song.mvCoverUrl,
 | 
				
			||||||
					// 确保 artistes 和 artists 是数组
 | 
										// 确保 artistes 和 artists 是数组
 | 
				
			||||||
				artistes: Array.isArray(item.song.artistes) ? item.song.artistes :
 | 
										artistes: Array.isArray(item.song.artistes)
 | 
				
			||||||
					typeof item.song.artistes === 'object' ? Object.values(item.song.artistes) :
 | 
											? item.song.artistes
 | 
				
			||||||
						[],
 | 
											: typeof item.song.artistes === 'object'
 | 
				
			||||||
				artists: Array.isArray(item.song.artists) ? item.song.artists :
 | 
												? Object.values(item.song.artistes)
 | 
				
			||||||
					typeof item.song.artists === 'object' ? Object.values(item.song.artists) :
 | 
												: [],
 | 
				
			||||||
						[]
 | 
										artists: Array.isArray(item.song.artists)
 | 
				
			||||||
 | 
											? item.song.artists
 | 
				
			||||||
 | 
											: typeof item.song.artists === 'object'
 | 
				
			||||||
 | 
												? Object.values(item.song.artists)
 | 
				
			||||||
 | 
												: [],
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				// 规范化 Album 对象(如果存在)
 | 
									// 规范化 Album 对象(如果存在)
 | 
				
			||||||
			const album = item.album ? {
 | 
									const album = item.album
 | 
				
			||||||
 | 
										? {
 | 
				
			||||||
							cid: item.album.cid || '',
 | 
												cid: item.album.cid || '',
 | 
				
			||||||
							name: item.album.name || '',
 | 
												name: item.album.name || '',
 | 
				
			||||||
							intro: item.album.intro,
 | 
												intro: item.album.intro,
 | 
				
			||||||
							belong: item.album.belong,
 | 
												belong: item.album.belong,
 | 
				
			||||||
							coverUrl: item.album.coverUrl || '',
 | 
												coverUrl: item.album.coverUrl || '',
 | 
				
			||||||
							coverDeUrl: item.album.coverDeUrl,
 | 
												coverDeUrl: item.album.coverDeUrl,
 | 
				
			||||||
				artistes: Array.isArray(item.album.artistes) ? item.album.artistes :
 | 
												artistes: Array.isArray(item.album.artistes)
 | 
				
			||||||
					typeof item.album.artistes === 'object' ? Object.values(item.album.artistes) :
 | 
													? item.album.artistes
 | 
				
			||||||
						[],
 | 
													: typeof item.album.artistes === 'object'
 | 
				
			||||||
				songs: item.album.songs
 | 
														? Object.values(item.album.artistes)
 | 
				
			||||||
			} : undefined
 | 
														: [],
 | 
				
			||||||
 | 
												songs: item.album.songs,
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										: undefined
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				return { song, album }
 | 
									return { song, album }
 | 
				
			||||||
		}).filter(Boolean) as QueueItem[]
 | 
								})
 | 
				
			||||||
 | 
								.filter(Boolean) as InternalQueueItem[]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 获取收藏列表
 | 
						// 获取收藏列表
 | 
				
			||||||
	const getFavourites = async () => {
 | 
						const getFavourites = async () => {
 | 
				
			||||||
		const result = await getStoredValue('favourites', defaultFavourites)
 | 
							const result = await getStoredValue('favourites', defaultFavourites)
 | 
				
			||||||
		// 确保返回的是数组并进行数据规范化
 | 
							// 确保返回的是数组并进行数据规范化
 | 
				
			||||||
		const normalizedResult = Array.isArray(result) ? normalizeFavourites(result) : defaultFavourites
 | 
							const normalizedResult = Array.isArray(result)
 | 
				
			||||||
 | 
								? normalizeFavourites(result)
 | 
				
			||||||
 | 
								: defaultFavourites
 | 
				
			||||||
		return normalizedResult
 | 
							return normalizedResult
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -187,11 +207,11 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检查歌曲是否已收藏
 | 
						// 检查歌曲是否已收藏
 | 
				
			||||||
	const isFavourite = (songCid: string): boolean => {
 | 
						const isFavourite = (songCid: string): boolean => {
 | 
				
			||||||
		return favourites.value.some(item => item.song.cid === songCid)
 | 
							return favourites.value.some((item) => item.song.cid === songCid)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 添加到收藏
 | 
						// 添加到收藏
 | 
				
			||||||
	const addToFavourites = async (queueItem: QueueItem) => {
 | 
						const addToFavourites = async (queueItem: InternalQueueItem) => {
 | 
				
			||||||
		if (!isFavourite(queueItem.song.cid)) {
 | 
							if (!isFavourite(queueItem.song.cid)) {
 | 
				
			||||||
			favourites.value.push(queueItem)
 | 
								favourites.value.push(queueItem)
 | 
				
			||||||
			if (isLoaded.value) {
 | 
								if (isLoaded.value) {
 | 
				
			||||||
| 
						 | 
					@ -208,7 +228,9 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 从收藏中移除
 | 
						// 从收藏中移除
 | 
				
			||||||
	const removeFromFavourites = async (songCid: string) => {
 | 
						const removeFromFavourites = async (songCid: string) => {
 | 
				
			||||||
		const index = favourites.value.findIndex(item => item.song.cid === songCid)
 | 
							const index = favourites.value.findIndex(
 | 
				
			||||||
 | 
								(item) => item.song.cid === songCid,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		if (index !== -1) {
 | 
							if (index !== -1) {
 | 
				
			||||||
			const removedItem = favourites.value.splice(index, 1)[0]
 | 
								const removedItem = favourites.value.splice(index, 1)[0]
 | 
				
			||||||
			if (isLoaded.value) {
 | 
								if (isLoaded.value) {
 | 
				
			||||||
| 
						 | 
					@ -224,7 +246,7 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 切换收藏状态
 | 
						// 切换收藏状态
 | 
				
			||||||
	const toggleFavourite = async (queueItem: QueueItem) => {
 | 
						const toggleFavourite = async (queueItem: InternalQueueItem) => {
 | 
				
			||||||
		if (isFavourite(queueItem.song.cid)) {
 | 
							if (isFavourite(queueItem.song.cid)) {
 | 
				
			||||||
			await removeFromFavourites(queueItem.song.cid)
 | 
								await removeFromFavourites(queueItem.song.cid)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
| 
						 | 
					@ -265,7 +287,9 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 监听变化并保存(防抖处理)
 | 
						// 监听变化并保存(防抖处理)
 | 
				
			||||||
	let saveTimeout: NodeJS.Timeout | null = null
 | 
						let saveTimeout: NodeJS.Timeout | null = null
 | 
				
			||||||
	watch(favourites, async () => {
 | 
						watch(
 | 
				
			||||||
 | 
							favourites,
 | 
				
			||||||
 | 
							async () => {
 | 
				
			||||||
			if (isLoaded.value) {
 | 
								if (isLoaded.value) {
 | 
				
			||||||
				// 清除之前的定时器
 | 
									// 清除之前的定时器
 | 
				
			||||||
				if (saveTimeout) {
 | 
									if (saveTimeout) {
 | 
				
			||||||
| 
						 | 
					@ -280,14 +304,21 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}, 300)
 | 
									}, 300)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	}, { deep: true })
 | 
							},
 | 
				
			||||||
 | 
							{ deep: true },
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 更新收藏列表中的歌曲信息
 | 
						// 更新收藏列表中的歌曲信息
 | 
				
			||||||
	const updateSongInFavourites = async (songCid: string, updatedSong: Song) => {
 | 
						const updateSongInFavourites = async (songCid: string, updatedSong: Song) => {
 | 
				
			||||||
		const index = favourites.value.findIndex(item => item.song.cid === songCid)
 | 
							const index = favourites.value.findIndex(
 | 
				
			||||||
 | 
								(item) => item.song.cid === songCid,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		if (index !== -1) {
 | 
							if (index !== -1) {
 | 
				
			||||||
			// 更新歌曲信息,保持其他属性不变
 | 
								// 更新歌曲信息,保持其他属性不变
 | 
				
			||||||
			favourites.value[index].song = { ...favourites.value[index].song, ...updatedSong }
 | 
								favourites.value[index].song = {
 | 
				
			||||||
 | 
									...favourites.value[index].song,
 | 
				
			||||||
 | 
									...updatedSong,
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			if (isLoaded.value) {
 | 
								if (isLoaded.value) {
 | 
				
			||||||
				try {
 | 
									try {
 | 
				
			||||||
					await saveFavourites()
 | 
										await saveFavourites()
 | 
				
			||||||
| 
						 | 
					@ -317,7 +348,6 @@ export const useFavourites = defineStore('favourites', () => {
 | 
				
			||||||
		clearFavourites,
 | 
							clearFavourites,
 | 
				
			||||||
		getStoredValue,
 | 
							getStoredValue,
 | 
				
			||||||
		setStoredValue,
 | 
							setStoredValue,
 | 
				
			||||||
		updateSongInFavourites
 | 
							updateSongInFavourites,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,217 +0,0 @@
 | 
				
			||||||
import { defineStore } from 'pinia'
 | 
					 | 
				
			||||||
import { computed, ref } from 'vue'
 | 
					 | 
				
			||||||
import { checkAndRefreshSongResource } from '../utils'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const usePlayQueueStore = defineStore('queue', () => {
 | 
					 | 
				
			||||||
	const list = ref<QueueItem[]>([])
 | 
					 | 
				
			||||||
	const currentIndex = ref<number>(0)
 | 
					 | 
				
			||||||
	const isPlaying = ref<boolean>(false)
 | 
					 | 
				
			||||||
	const queueReplaceLock = ref<boolean>(false)
 | 
					 | 
				
			||||||
	const isBuffering = ref<boolean>(false)
 | 
					 | 
				
			||||||
	const currentTime = ref<number>(0)
 | 
					 | 
				
			||||||
	const duration = ref<number>(0)
 | 
					 | 
				
			||||||
	const updatedCurrentTime = ref<number | null>(null)
 | 
					 | 
				
			||||||
	const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
 | 
					 | 
				
			||||||
	const shuffleList = ref<number[]>([])
 | 
					 | 
				
			||||||
	const playMode = ref<{
 | 
					 | 
				
			||||||
		shuffle: boolean
 | 
					 | 
				
			||||||
		repeat: 'off' | 'single' | 'all'
 | 
					 | 
				
			||||||
	}>({
 | 
					 | 
				
			||||||
		shuffle: false,
 | 
					 | 
				
			||||||
		repeat: 'off',
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
	const shuffleCurrent = ref<boolean | undefined>(undefined)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 预加载相关状态
 | 
					 | 
				
			||||||
	const preloadedAudio = ref<Map<string, HTMLAudioElement>>(new Map())
 | 
					 | 
				
			||||||
	const isPreloading = ref<boolean>(false)
 | 
					 | 
				
			||||||
	const preloadProgress = ref<number>(0)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 获取下一首歌的索引
 | 
					 | 
				
			||||||
	const getNextIndex = computed(() => {
 | 
					 | 
				
			||||||
		if (list.value.length === 0) return -1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (playMode.value.repeat === 'single') {
 | 
					 | 
				
			||||||
			return currentIndex.value
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (playMode.value.shuffle && shuffleList.value.length > 0) {
 | 
					 | 
				
			||||||
			// 当前在 shuffleList 中的位置
 | 
					 | 
				
			||||||
			const currentShuffleIndex = currentIndex.value
 | 
					 | 
				
			||||||
			if (currentShuffleIndex < shuffleList.value.length - 1) {
 | 
					 | 
				
			||||||
				// 返回下一个位置对应的原始 list 索引
 | 
					 | 
				
			||||||
				return shuffleList.value[currentShuffleIndex + 1]
 | 
					 | 
				
			||||||
			} else if (playMode.value.repeat === 'all') {
 | 
					 | 
				
			||||||
				// 返回第一个位置对应的原始 list 索引
 | 
					 | 
				
			||||||
				return shuffleList.value[0]
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			return -1
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (currentIndex.value < list.value.length - 1) {
 | 
					 | 
				
			||||||
			return currentIndex.value + 1
 | 
					 | 
				
			||||||
		} else if (playMode.value.repeat === 'all') {
 | 
					 | 
				
			||||||
			return 0
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		return -1
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 预加载下一首歌
 | 
					 | 
				
			||||||
	const preloadNext = async () => {
 | 
					 | 
				
			||||||
		const nextIndex = getNextIndex.value
 | 
					 | 
				
			||||||
		if (nextIndex === -1) {
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 获取下一首歌曲对象
 | 
					 | 
				
			||||||
		// nextIndex 已经是原始 list 中的索引
 | 
					 | 
				
			||||||
		const nextSong = list.value[nextIndex]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!nextSong || !nextSong.song) {
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const songId = nextSong.song.cid
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 如果已经预加载过,跳过
 | 
					 | 
				
			||||||
		if (preloadedAudio.value.has(songId)) {
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// 检查是否有有效的音频源
 | 
					 | 
				
			||||||
		if (!nextSong.song.sourceUrl) {
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		try {
 | 
					 | 
				
			||||||
			isPreloading.value = true
 | 
					 | 
				
			||||||
			preloadProgress.value = 0
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 在预加载前检查和刷新资源
 | 
					 | 
				
			||||||
			console.log('[Store] 预加载前检查资源:', nextSong.song.name)
 | 
					 | 
				
			||||||
			const updatedSong = await checkAndRefreshSongResource(
 | 
					 | 
				
			||||||
				nextSong.song,
 | 
					 | 
				
			||||||
				(updated) => {
 | 
					 | 
				
			||||||
					// 更新播放队列中的歌曲信息
 | 
					 | 
				
			||||||
					// nextIndex 已经是原始 list 中的索引
 | 
					 | 
				
			||||||
					if (list.value[nextIndex]) {
 | 
					 | 
				
			||||||
						list.value[nextIndex].song = updated
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					// 如果歌曲在收藏夹中,也更新收藏夹
 | 
					 | 
				
			||||||
					// 注意:这里不直接导入 favourites store 以避免循环依赖
 | 
					 | 
				
			||||||
					// 改为触发一个事件或者在调用方处理
 | 
					 | 
				
			||||||
					console.log('[Store] 预加载时需要更新收藏夹:', updated.name)
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
			)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const audio = new Audio()
 | 
					 | 
				
			||||||
			audio.preload = 'auto'
 | 
					 | 
				
			||||||
			audio.crossOrigin = 'anonymous'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 监听加载进度
 | 
					 | 
				
			||||||
			audio.addEventListener('progress', () => {
 | 
					 | 
				
			||||||
				if (audio.buffered.length > 0) {
 | 
					 | 
				
			||||||
					const buffered = audio.buffered.end(0)
 | 
					 | 
				
			||||||
					const total = audio.duration || 1
 | 
					 | 
				
			||||||
					preloadProgress.value = (buffered / total) * 100
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 监听加载完成
 | 
					 | 
				
			||||||
			audio.addEventListener('canplaythrough', () => {
 | 
					 | 
				
			||||||
				preloadedAudio.value.set(songId, audio)
 | 
					 | 
				
			||||||
				isPreloading.value = false
 | 
					 | 
				
			||||||
				preloadProgress.value = 100
 | 
					 | 
				
			||||||
				console.log('[Store] 预加载完成:', updatedSong.name)
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 监听加载错误
 | 
					 | 
				
			||||||
			audio.addEventListener('error', (e) => {
 | 
					 | 
				
			||||||
				console.error(`[Store] 预加载音频失败: ${updatedSong.name}`, e)
 | 
					 | 
				
			||||||
				isPreloading.value = false
 | 
					 | 
				
			||||||
				preloadProgress.value = 0
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// 使用更新后的音频源
 | 
					 | 
				
			||||||
			audio.src = updatedSong.sourceUrl!
 | 
					 | 
				
			||||||
		} catch (error) {
 | 
					 | 
				
			||||||
			console.error('[Store] 预加载过程出错:', error)
 | 
					 | 
				
			||||||
			isPreloading.value = false
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 获取预加载的音频对象
 | 
					 | 
				
			||||||
	const getPreloadedAudio = (songId: string): HTMLAudioElement | null => {
 | 
					 | 
				
			||||||
		const audio = preloadedAudio.value.get(songId) || null
 | 
					 | 
				
			||||||
		return audio
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 清理预加载的音频
 | 
					 | 
				
			||||||
	const clearPreloadedAudio = (songId: string) => {
 | 
					 | 
				
			||||||
		const audio = preloadedAudio.value.get(songId)
 | 
					 | 
				
			||||||
		if (audio) {
 | 
					 | 
				
			||||||
			audio.pause()
 | 
					 | 
				
			||||||
			audio.src = ''
 | 
					 | 
				
			||||||
			preloadedAudio.value.delete(songId)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 清理所有预加载的音频
 | 
					 | 
				
			||||||
	const clearAllPreloadedAudio = () => {
 | 
					 | 
				
			||||||
		preloadedAudio.value.forEach((_audio, songId) => {
 | 
					 | 
				
			||||||
			clearPreloadedAudio(songId)
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
		preloadedAudio.value.clear()
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 限制预加载缓存大小(最多保留3首歌)
 | 
					 | 
				
			||||||
	const limitPreloadCache = () => {
 | 
					 | 
				
			||||||
		while (preloadedAudio.value.size > 3) {
 | 
					 | 
				
			||||||
			const oldestKey = preloadedAudio.value.keys().next().value
 | 
					 | 
				
			||||||
			if (oldestKey) {
 | 
					 | 
				
			||||||
				clearPreloadedAudio(oldestKey)
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// 调试函数:打印当前状态
 | 
					 | 
				
			||||||
	const debugPreloadState = () => {
 | 
					 | 
				
			||||||
		console.log('[Store] 预加载状态:', {
 | 
					 | 
				
			||||||
			isPreloading: isPreloading.value,
 | 
					 | 
				
			||||||
			progress: preloadProgress.value,
 | 
					 | 
				
			||||||
			cacheSize: preloadedAudio.value.size,
 | 
					 | 
				
			||||||
			cachedSongs: Array.from(preloadedAudio.value.keys()),
 | 
					 | 
				
			||||||
			nextIndex: getNextIndex.value,
 | 
					 | 
				
			||||||
		})
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		list,
 | 
					 | 
				
			||||||
		currentIndex,
 | 
					 | 
				
			||||||
		isPlaying,
 | 
					 | 
				
			||||||
		queueReplaceLock,
 | 
					 | 
				
			||||||
		isBuffering,
 | 
					 | 
				
			||||||
		currentTime,
 | 
					 | 
				
			||||||
		duration,
 | 
					 | 
				
			||||||
		updatedCurrentTime,
 | 
					 | 
				
			||||||
		visualizer,
 | 
					 | 
				
			||||||
		shuffleList,
 | 
					 | 
				
			||||||
		playMode,
 | 
					 | 
				
			||||||
		shuffleCurrent,
 | 
					 | 
				
			||||||
		// 预加载相关 - 确保所有函数都在返回对象中
 | 
					 | 
				
			||||||
		preloadedAudio,
 | 
					 | 
				
			||||||
		isPreloading,
 | 
					 | 
				
			||||||
		preloadProgress,
 | 
					 | 
				
			||||||
		getNextIndex,
 | 
					 | 
				
			||||||
		preloadNext,
 | 
					 | 
				
			||||||
		getPreloadedAudio,
 | 
					 | 
				
			||||||
		clearPreloadedAudio,
 | 
					 | 
				
			||||||
		clearAllPreloadedAudio,
 | 
					 | 
				
			||||||
		limitPreloadCache,
 | 
					 | 
				
			||||||
		debugPreloadState,
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
})
 | 
					 | 
				
			||||||
							
								
								
									
										136
									
								
								src/stores/usePlayStore.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/stores/usePlayStore.ts
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,136 @@
 | 
				
			||||||
 | 
					import { Player } from '@astrian/music-surge-revolution'
 | 
				
			||||||
 | 
					import type { QueueItem } from '@astrian/music-surge-revolution'
 | 
				
			||||||
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					import { artistsOrganize } from '../utils'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const usePlayStore = defineStore('player', () => {
 | 
				
			||||||
 | 
						const player = ref(new Player())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const queueReplaceLock = ref(false)
 | 
				
			||||||
 | 
						const visualizer = ref([0, 0, 0, 0, 0, 0])
 | 
				
			||||||
 | 
						const progress = ref({
 | 
				
			||||||
 | 
							currentTime: 0,
 | 
				
			||||||
 | 
							duration: 0,
 | 
				
			||||||
 | 
							percentage: 0,
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						const currentTrack = ref<QueueItem>()
 | 
				
			||||||
 | 
						const isPlaying = ref(false)
 | 
				
			||||||
 | 
						const queue = ref<QueueItem[]>([])
 | 
				
			||||||
 | 
						const shuffle = ref(false)
 | 
				
			||||||
 | 
						const loop = ref<'off' | 'entire_queue' | 'single_track'>('off')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const replaceQueue = (
 | 
				
			||||||
 | 
							queue: {
 | 
				
			||||||
 | 
								song: Song
 | 
				
			||||||
 | 
								album: Album | undefined
 | 
				
			||||||
 | 
							}[],
 | 
				
			||||||
 | 
						) => {
 | 
				
			||||||
 | 
							const newQueue = []
 | 
				
			||||||
 | 
							for (const item of queue) {
 | 
				
			||||||
 | 
								console.log(item.song.artists ?? item.song.artistes)
 | 
				
			||||||
 | 
								newQueue.push({
 | 
				
			||||||
 | 
									url: item.song.sourceUrl ?? '',
 | 
				
			||||||
 | 
									metadata: {
 | 
				
			||||||
 | 
										title: item.song.name,
 | 
				
			||||||
 | 
										artist: artistsOrganize(
 | 
				
			||||||
 | 
											item.song.artists ?? item.song.artistes ?? [],
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
										album: item.album?.name,
 | 
				
			||||||
 | 
										artwork: [
 | 
				
			||||||
 | 
											{
 | 
				
			||||||
 | 
												src: item.album?.coverUrl ?? '',
 | 
				
			||||||
 | 
												sizes: '500x500',
 | 
				
			||||||
 | 
												type: ((item.album?.coverUrl ?? '').split('.').at(-1) === 'jpg'
 | 
				
			||||||
 | 
													? 'image/jpeg'
 | 
				
			||||||
 | 
													: 'image/png') as 'image/jpeg' | 'image/png',
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
										],
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									extra: {
 | 
				
			||||||
 | 
										lyric: item.song.lyricUrl,
 | 
				
			||||||
 | 
										background: item.album?.coverDeUrl,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							player.value.replaceQueue(newQueue)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const togglePlay = (play?: boolean) => {
 | 
				
			||||||
 | 
							player.value.togglePlaying(play)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const appendItem = (item: {
 | 
				
			||||||
 | 
							song: Song
 | 
				
			||||||
 | 
							album: Album | undefined
 | 
				
			||||||
 | 
						}) => {
 | 
				
			||||||
 | 
							player.value.appendTrack({
 | 
				
			||||||
 | 
								url: item.song.sourceUrl ?? '',
 | 
				
			||||||
 | 
								metadata: {
 | 
				
			||||||
 | 
									title: item.song.name,
 | 
				
			||||||
 | 
									artist: artistsOrganize(item.song.artistes ?? item.song.artists ?? []),
 | 
				
			||||||
 | 
									artwork: [
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											src: item.album?.coverUrl ?? '',
 | 
				
			||||||
 | 
											sizes: '500x500',
 | 
				
			||||||
 | 
											type: ((item.album?.coverUrl ?? '').split('.').at(-1) === 'jpg'
 | 
				
			||||||
 | 
												? 'image/jpeg'
 | 
				
			||||||
 | 
												: 'image/png') as 'image/jpeg' | 'image/png',
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									],
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								extra: {
 | 
				
			||||||
 | 
									lyric: item.song.lyricUrl,
 | 
				
			||||||
 | 
									background: item.album?.coverDeUrl,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						player.value.onProgressChange((params) => {
 | 
				
			||||||
 | 
							progress.value = params
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						player.value.onCurrentPlayingChange((params) => {
 | 
				
			||||||
 | 
							currentTrack.value = params
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						player.value.onPlayStateChange((params) => {
 | 
				
			||||||
 | 
							isPlaying.value = params
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						player.value.onQueueChange((params) => {
 | 
				
			||||||
 | 
							queue.value = params
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						player.value.onShuffleChange((params) => {
 | 
				
			||||||
 | 
							shuffle.value = params
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						player.value.onLoopChange((params) => {
 | 
				
			||||||
 | 
							loop.value = params
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const updateCurrentTime = (time: number) => {
 | 
				
			||||||
 | 
							player.value.seekTo(time)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const skipToPrevious = () => {
 | 
				
			||||||
 | 
							player.value.skipToPrevious()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return {
 | 
				
			||||||
 | 
							queueReplaceLock,
 | 
				
			||||||
 | 
							togglePlay,
 | 
				
			||||||
 | 
							visualizer,
 | 
				
			||||||
 | 
							appendItem,
 | 
				
			||||||
 | 
							progress,
 | 
				
			||||||
 | 
							currentTrack,
 | 
				
			||||||
 | 
							updateCurrentTime,
 | 
				
			||||||
 | 
							skipToPrevious,
 | 
				
			||||||
 | 
							isPlaying,
 | 
				
			||||||
 | 
							queue,
 | 
				
			||||||
 | 
							shuffle,
 | 
				
			||||||
 | 
							loop,
 | 
				
			||||||
 | 
							replaceQueue,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { defineStore } from "pinia"
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
import { ref, watch } from "vue"
 | 
					import { ref, watch } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 声明全局类型
 | 
					// 声明全局类型
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
| 
						 | 
					@ -20,14 +20,18 @@ export const usePreferences = defineStore('preferences', () => {
 | 
				
			||||||
	const defaultPreferences = {
 | 
						const defaultPreferences = {
 | 
				
			||||||
		displayTimeLeft: false,
 | 
							displayTimeLeft: false,
 | 
				
			||||||
		presentLyrics: false,
 | 
							presentLyrics: false,
 | 
				
			||||||
		autoRedirect: true
 | 
							autoRedirect: true,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检测可用的 API
 | 
						// 检测可用的 API
 | 
				
			||||||
	const detectAvailableAPIs = () => {
 | 
						const detectAvailableAPIs = () => {
 | 
				
			||||||
		// 检查原生 chrome API
 | 
							// 检查原生 chrome API
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
 | 
								if (
 | 
				
			||||||
 | 
									typeof chrome !== 'undefined' &&
 | 
				
			||||||
 | 
									chrome.storage &&
 | 
				
			||||||
 | 
									chrome.storage.sync
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
				storageType.value = 'chrome'
 | 
									storageType.value = 'chrome'
 | 
				
			||||||
				return 'chrome'
 | 
									return 'chrome'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -37,7 +41,11 @@ export const usePreferences = defineStore('preferences', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 检查 window.chrome
 | 
							// 检查 window.chrome
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
 | 
								if (
 | 
				
			||||||
 | 
									window.chrome &&
 | 
				
			||||||
 | 
									window.chrome.storage &&
 | 
				
			||||||
 | 
									window.chrome.storage.sync
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
				storageType.value = 'chrome'
 | 
									storageType.value = 'chrome'
 | 
				
			||||||
				return 'chrome'
 | 
									return 'chrome'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -143,7 +151,7 @@ export const usePreferences = defineStore('preferences', () => {
 | 
				
			||||||
		const preferences = {
 | 
							const preferences = {
 | 
				
			||||||
			displayTimeLeft: displayTimeLeft.value,
 | 
								displayTimeLeft: displayTimeLeft.value,
 | 
				
			||||||
			presentLyrics: presentLyrics.value,
 | 
								presentLyrics: presentLyrics.value,
 | 
				
			||||||
			autoRedirect: autoRedirect.value
 | 
								autoRedirect: autoRedirect.value,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		await setStoredValue('preferences', preferences)
 | 
							await setStoredValue('preferences', preferences)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -188,6 +196,6 @@ export const usePreferences = defineStore('preferences', () => {
 | 
				
			||||||
		getStoredValue,
 | 
							getStoredValue,
 | 
				
			||||||
		setStoredValue,
 | 
							setStoredValue,
 | 
				
			||||||
		getPreferences,
 | 
							getPreferences,
 | 
				
			||||||
		savePreferences
 | 
							savePreferences,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
import { defineStore } from "pinia"
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
import { ref } from "vue"
 | 
					import { ref } from 'vue'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 声明全局类型
 | 
					// 声明全局类型
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,11 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
				
			||||||
	const detectAvailableAPIs = () => {
 | 
						const detectAvailableAPIs = () => {
 | 
				
			||||||
		// 检查原生 chrome API
 | 
							// 检查原生 chrome API
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
 | 
								if (
 | 
				
			||||||
 | 
									typeof chrome !== 'undefined' &&
 | 
				
			||||||
 | 
									chrome.storage &&
 | 
				
			||||||
 | 
									chrome.storage.sync
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
				storageType.value = 'chrome'
 | 
									storageType.value = 'chrome'
 | 
				
			||||||
				return 'chrome'
 | 
									return 'chrome'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -36,7 +40,11 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 检查 window.chrome
 | 
							// 检查 window.chrome
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
 | 
								if (
 | 
				
			||||||
 | 
									window.chrome &&
 | 
				
			||||||
 | 
									window.chrome.storage &&
 | 
				
			||||||
 | 
									window.chrome.storage.sync
 | 
				
			||||||
 | 
								) {
 | 
				
			||||||
				storageType.value = 'chrome'
 | 
									storageType.value = 'chrome'
 | 
				
			||||||
				return 'chrome'
 | 
									return 'chrome'
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
| 
						 | 
					@ -143,7 +151,10 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 获取上次显示弹窗的版本号
 | 
								// 获取上次显示弹窗的版本号
 | 
				
			||||||
			const lastShownVersion = await getStoredValue('lastUpdatePopupVersion', '')
 | 
								const lastShownVersion = await getStoredValue(
 | 
				
			||||||
 | 
									'lastUpdatePopupVersion',
 | 
				
			||||||
 | 
									'',
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 如果版本号不同,需要显示弹窗并更新存储的版本号
 | 
								// 如果版本号不同,需要显示弹窗并更新存储的版本号
 | 
				
			||||||
			if (lastShownVersion !== currentVersion) {
 | 
								if (lastShownVersion !== currentVersion) {
 | 
				
			||||||
| 
						 | 
					@ -199,6 +210,6 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
				
			||||||
		getLastShownVersion,
 | 
							getLastShownVersion,
 | 
				
			||||||
		initializeUpdatePopup,
 | 
							initializeUpdatePopup,
 | 
				
			||||||
		getStoredValue,
 | 
							getStoredValue,
 | 
				
			||||||
		setStoredValue
 | 
							setStoredValue,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					@ -1,15 +1,15 @@
 | 
				
			||||||
@import "tailwindcss";
 | 
					@import "tailwindcss";
 | 
				
			||||||
/* 导入来自 /assets/MiSans_VF.ttf 的字体 */
 | 
					/* 导入来自 /assets/MiSans_VF.ttf 的字体 */
 | 
				
			||||||
@font-face {
 | 
					@font-face {
 | 
				
			||||||
	font-family: 'MiSans';
 | 
						font-family: "MiSans";
 | 
				
			||||||
	src: url('/assets/MiSans_VF.ttf') format('truetype-variations');
 | 
						src: url("/assets/MiSans_VF.ttf") format("truetype-variations");
 | 
				
			||||||
	font-weight: 1 999;
 | 
						font-weight: 1 999;
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@font-face {
 | 
					@font-face {
 | 
				
			||||||
	font-family: 'Alte DIN';
 | 
						font-family: "Alte DIN";
 | 
				
			||||||
	src: url('/assets/din1451alt.ttf') format('truetype-variations');
 | 
						src: url("/assets/din1451alt.ttf") format("truetype-variations");
 | 
				
			||||||
	font-weight: 1 999;
 | 
						font-weight: 1 999;
 | 
				
			||||||
	font-display: swap;
 | 
						font-display: swap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -27,5 +27,5 @@ input {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.track_num {
 | 
					.track_num {
 | 
				
			||||||
	font-family: 'DIN Alternate', 'Alte DIN' !important;
 | 
						font-family: "DIN Alternate", "Alte DIN" !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,10 @@
 | 
				
			||||||
export default (list: string[]) => {
 | 
					export default (list: string[]) => {
 | 
				
			||||||
	if (list.length === 0) { return '未知音乐人' }
 | 
						if (list.length === 0) {
 | 
				
			||||||
	return list.map((artist) => {
 | 
							return '未知音乐人'
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return list
 | 
				
			||||||
 | 
							.map((artist) => {
 | 
				
			||||||
			return artist
 | 
								return artist
 | 
				
			||||||
	}).join(' / ')
 | 
							})
 | 
				
			||||||
 | 
							.join(' / ')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
		midBoost = 1.2, // 提升中音
 | 
							midBoost = 1.2, // 提升中音
 | 
				
			||||||
		trebleBoost = 1.5, // 提升高音
 | 
							trebleBoost = 1.5, // 提升高音
 | 
				
			||||||
		threshold = 15, // 响度门槛,低于此值不产生波动
 | 
							threshold = 15, // 响度门槛,低于此值不产生波动
 | 
				
			||||||
    minHeight = 0         // 最小高度百分比
 | 
							minHeight = 0, // 最小高度百分比
 | 
				
			||||||
	} = options
 | 
						} = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	console.log('[AudioVisualizer] 初始化平衡频谱,选项:', options)
 | 
						console.log('[AudioVisualizer] 初始化平衡频谱,选项:', options)
 | 
				
			||||||
| 
						 | 
					@ -65,8 +65,15 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			log('开始初始化音频上下文...')
 | 
								log('开始初始化音频上下文...')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
 | 
								audioContext = new (
 | 
				
			||||||
      log('AudioContext 创建成功, 状态:', audioContext.state, '采样率:', audioContext.sampleRate)
 | 
									window.AudioContext || (window as any).webkitAudioContext
 | 
				
			||||||
 | 
								)()
 | 
				
			||||||
 | 
								log(
 | 
				
			||||||
 | 
									'AudioContext 创建成功, 状态:',
 | 
				
			||||||
 | 
									audioContext.state,
 | 
				
			||||||
 | 
									'采样率:',
 | 
				
			||||||
 | 
									audioContext.sampleRate,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 如果上下文被暂停,尝试恢复
 | 
								// 如果上下文被暂停,尝试恢复
 | 
				
			||||||
			if (audioContext.state === 'suspended') {
 | 
								if (audioContext.state === 'suspended') {
 | 
				
			||||||
| 
						 | 
					@ -98,7 +105,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
				frequencyBinCount: analyser.frequencyBinCount,
 | 
									frequencyBinCount: analyser.frequencyBinCount,
 | 
				
			||||||
				sampleRate: audioContext.sampleRate,
 | 
									sampleRate: audioContext.sampleRate,
 | 
				
			||||||
				frequencyResolution: audioContext.sampleRate / analyser.fftSize,
 | 
									frequencyResolution: audioContext.sampleRate / analyser.fftSize,
 | 
				
			||||||
        maxDecibels: analyser.maxDecibels
 | 
									maxDecibels: analyser.maxDecibels,
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 连接音频节点
 | 
								// 连接音频节点
 | 
				
			||||||
| 
						 | 
					@ -111,7 +118,6 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
			isInitialized.value = true
 | 
								isInitialized.value = true
 | 
				
			||||||
			error.value = null
 | 
								error.value = null
 | 
				
			||||||
			log('✅ 音频可视化器初始化成功')
 | 
								log('✅ 音频可视化器初始化成功')
 | 
				
			||||||
      
 | 
					 | 
				
			||||||
		} catch (err) {
 | 
							} catch (err) {
 | 
				
			||||||
			log('❌ 音频上下文初始化失败:', err)
 | 
								log('❌ 音频上下文初始化失败:', err)
 | 
				
			||||||
			error.value = `初始化失败: ${err instanceof Error ? err.message : String(err)}`
 | 
								error.value = `初始化失败: ${err instanceof Error ? err.message : String(err)}`
 | 
				
			||||||
| 
						 | 
					@ -150,21 +156,29 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
		analyser.getByteFrequencyData(dataArray)
 | 
							analyser.getByteFrequencyData(dataArray)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 使用平衡的频段分割
 | 
							// 使用平衡的频段分割
 | 
				
			||||||
    const frequencyBands = divideFrequencyBandsBalanced(dataArray, barCount, audioContext.sampleRate)
 | 
							const frequencyBands = divideFrequencyBandsBalanced(
 | 
				
			||||||
 | 
								dataArray,
 | 
				
			||||||
 | 
								barCount,
 | 
				
			||||||
 | 
								audioContext.sampleRate,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 应用频段特定的增强
 | 
							// 应用频段特定的增强
 | 
				
			||||||
		const enhancedBands = applyFrequencyEnhancement(frequencyBands)
 | 
							const enhancedBands = applyFrequencyEnhancement(frequencyBands)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 更新竖杠高度 (0-100)
 | 
							// 更新竖杠高度 (0-100)
 | 
				
			||||||
    barHeights.value = enhancedBands.map(value => 
 | 
							barHeights.value = enhancedBands.map((value) =>
 | 
				
			||||||
      Math.min(100, Math.max(0, (value / 255) * 100 * sensitivity))
 | 
								Math.min(100, Math.max(0, (value / 255) * 100 * sensitivity)),
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		animationId = requestAnimationFrame(animate)
 | 
							animationId = requestAnimationFrame(animate)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 平衡的频段分割 - 使用对数分布和人耳感知特性
 | 
						// 平衡的频段分割 - 使用对数分布和人耳感知特性
 | 
				
			||||||
  function divideFrequencyBandsBalanced(data: Uint8Array, bands: number, sampleRate: number): number[] {
 | 
						function divideFrequencyBandsBalanced(
 | 
				
			||||||
 | 
							data: Uint8Array,
 | 
				
			||||||
 | 
							bands: number,
 | 
				
			||||||
 | 
							sampleRate: number,
 | 
				
			||||||
 | 
						): number[] {
 | 
				
			||||||
		const nyquist = sampleRate / 2
 | 
							const nyquist = sampleRate / 2
 | 
				
			||||||
		const result: number[] = []
 | 
							const result: number[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -175,11 +189,12 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
			{ min: 250, max: 800, name: '中低音' }, // 索引 2
 | 
								{ min: 250, max: 800, name: '中低音' }, // 索引 2
 | 
				
			||||||
			{ min: 800, max: 2500, name: '中音' }, // 索引 3
 | 
								{ min: 800, max: 2500, name: '中音' }, // 索引 3
 | 
				
			||||||
			{ min: 2500, max: 6000, name: '中高音' }, // 索引 4
 | 
								{ min: 2500, max: 6000, name: '中高音' }, // 索引 4
 | 
				
			||||||
			{ min: 6000, max: 20000, name: '高音' }    // 索引 5
 | 
								{ min: 6000, max: 20000, name: '高音' }, // 索引 5
 | 
				
			||||||
		]
 | 
							]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (let i = 0; i < bands; i++) {
 | 
							for (let i = 0; i < bands; i++) {
 | 
				
			||||||
      const range = frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
 | 
								const range =
 | 
				
			||||||
 | 
									frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 将频率转换为 bin 索引
 | 
								// 将频率转换为 bin 索引
 | 
				
			||||||
			const startBin = Math.floor((range.min / nyquist) * data.length)
 | 
								const startBin = Math.floor((range.min / nyquist) * data.length)
 | 
				
			||||||
| 
						 | 
					@ -190,7 +205,9 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
			const actualEnd = Math.min(data.length - 1, endBin)
 | 
								const actualEnd = Math.min(data.length - 1, endBin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (debug && Math.random() < 0.01) {
 | 
								if (debug && Math.random() < 0.01) {
 | 
				
			||||||
        log(`频段 ${i} (${range.name}): ${range.min}-${range.max}Hz, bins ${actualStart}-${actualEnd}`)
 | 
									log(
 | 
				
			||||||
 | 
										`频段 ${i} (${range.name}): ${range.min}-${range.max}Hz, bins ${actualStart}-${actualEnd}`,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// 计算该频段的 RMS (均方根) 值,而不是简单平均
 | 
								// 计算该频段的 RMS (均方根) 值,而不是简单平均
 | 
				
			||||||
| 
						 | 
					@ -213,7 +230,14 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
	// 应用频段特定的增强和门槛
 | 
						// 应用频段特定的增强和门槛
 | 
				
			||||||
	function applyFrequencyEnhancement(bands: number[]): number[] {
 | 
						function applyFrequencyEnhancement(bands: number[]): number[] {
 | 
				
			||||||
		// 六个频段的增强倍数
 | 
							// 六个频段的增强倍数
 | 
				
			||||||
    const boosts = [bassBoost, bassBoost, midBoost, midBoost, trebleBoost, trebleBoost]
 | 
							const boosts = [
 | 
				
			||||||
 | 
								bassBoost,
 | 
				
			||||||
 | 
								bassBoost,
 | 
				
			||||||
 | 
								midBoost,
 | 
				
			||||||
 | 
								midBoost,
 | 
				
			||||||
 | 
								trebleBoost,
 | 
				
			||||||
 | 
								trebleBoost,
 | 
				
			||||||
 | 
							]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return bands.map((value, index) => {
 | 
							return bands.map((value, index) => {
 | 
				
			||||||
			// 应用响度门槛
 | 
								// 应用响度门槛
 | 
				
			||||||
| 
						 | 
					@ -221,7 +245,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
				if (debug && Math.random() < 0.01) {
 | 
									if (debug && Math.random() < 0.01) {
 | 
				
			||||||
					log(`频段 ${index} 低于门槛: ${Math.round(value)} < ${threshold}`)
 | 
										log(`频段 ${index} 低于门槛: ${Math.round(value)} < ${threshold}`)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
        return minHeight * 255 / 100  // 返回最小高度对应的值
 | 
									return (minHeight * 255) / 100 // 返回最小高度对应的值
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const boost = boosts[index] || 1
 | 
								const boost = boosts[index] || 1
 | 
				
			||||||
| 
						 | 
					@ -252,9 +276,13 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
		if (audioElement.readyState >= 2) {
 | 
							if (audioElement.readyState >= 2) {
 | 
				
			||||||
			initAudioContext(audioElement)
 | 
								initAudioContext(audioElement)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
      audioElement.addEventListener('loadeddata', () => {
 | 
								audioElement.addEventListener(
 | 
				
			||||||
 | 
									'loadeddata',
 | 
				
			||||||
 | 
									() => {
 | 
				
			||||||
					initAudioContext(audioElement)
 | 
										initAudioContext(audioElement)
 | 
				
			||||||
      }, { once: true })
 | 
									},
 | 
				
			||||||
 | 
									{ once: true },
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 监听播放状态
 | 
							// 监听播放状态
 | 
				
			||||||
| 
						 | 
					@ -309,7 +337,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
				Math.random() * 70 + 15, // 中低音:15-85
 | 
									Math.random() * 70 + 15, // 中低音:15-85
 | 
				
			||||||
				Math.random() * 80 + 10, // 中音:10-90
 | 
									Math.random() * 80 + 10, // 中音:10-90
 | 
				
			||||||
				Math.random() * 75 + 10, // 中高音:10-85
 | 
									Math.random() * 75 + 10, // 中高音:10-85
 | 
				
			||||||
        Math.random() * 65 + 15   // 高音:15-80
 | 
									Math.random() * 65 + 15, // 高音:15-80
 | 
				
			||||||
			]
 | 
								]
 | 
				
			||||||
			testCount++
 | 
								testCount++
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -323,7 +351,13 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 动态调整增强参数和门槛
 | 
						// 动态调整增强参数和门槛
 | 
				
			||||||
  function updateEnhancement(bass: number, mid: number, treble: number, newThreshold?: number, newMaxDecibels?: number) {
 | 
						function updateEnhancement(
 | 
				
			||||||
 | 
							bass: number,
 | 
				
			||||||
 | 
							mid: number,
 | 
				
			||||||
 | 
							treble: number,
 | 
				
			||||||
 | 
							newThreshold?: number,
 | 
				
			||||||
 | 
							newMaxDecibels?: number,
 | 
				
			||||||
 | 
						) {
 | 
				
			||||||
		options.bassBoost = bass
 | 
							options.bassBoost = bass
 | 
				
			||||||
		options.midBoost = mid
 | 
							options.midBoost = mid
 | 
				
			||||||
		options.trebleBoost = treble
 | 
							options.trebleBoost = treble
 | 
				
			||||||
| 
						 | 
					@ -338,7 +372,13 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
				log('实时更新 maxDecibels:', newMaxDecibels)
 | 
									log('实时更新 maxDecibels:', newMaxDecibels)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
    log('更新频段增强:', { bass, mid, treble, threshold: options.threshold, maxDecibels: options.maxDecibels })
 | 
							log('更新频段增强:', {
 | 
				
			||||||
 | 
								bass,
 | 
				
			||||||
 | 
								mid,
 | 
				
			||||||
 | 
								treble,
 | 
				
			||||||
 | 
								threshold: options.threshold,
 | 
				
			||||||
 | 
								maxDecibels: options.maxDecibels,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 设置响度门槛
 | 
						// 设置响度门槛
 | 
				
			||||||
| 
						 | 
					@ -373,6 +413,6 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
				
			||||||
		testWithFakeData,
 | 
							testWithFakeData,
 | 
				
			||||||
		updateEnhancement,
 | 
							updateEnhancement,
 | 
				
			||||||
		setThreshold,
 | 
							setThreshold,
 | 
				
			||||||
    setMaxDecibels
 | 
							setMaxDecibels,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -11,13 +11,15 @@ export function isSafari(): boolean {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检测 Safari 浏览器(包括 iOS 和 macOS)
 | 
						// 检测 Safari 浏览器(包括 iOS 和 macOS)
 | 
				
			||||||
	// Safari 的 User Agent 包含 'safari' 但不包含 'chrome' 或 'chromium'
 | 
						// Safari 的 User Agent 包含 'safari' 但不包含 'chrome' 或 'chromium'
 | 
				
			||||||
	const isSafariBrowser = ua.includes('safari') && 
 | 
						const isSafariBrowser =
 | 
				
			||||||
 | 
							ua.includes('safari') &&
 | 
				
			||||||
		!ua.includes('chrome') &&
 | 
							!ua.includes('chrome') &&
 | 
				
			||||||
		!ua.includes('chromium') &&
 | 
							!ua.includes('chromium') &&
 | 
				
			||||||
		!ua.includes('android')
 | 
							!ua.includes('android')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 额外检查:使用 Safari 特有的 API
 | 
						// 额外检查:使用 Safari 特有的 API
 | 
				
			||||||
	const isSafariByFeature = 'safari' in window || 
 | 
						const isSafariByFeature =
 | 
				
			||||||
 | 
							'safari' in window ||
 | 
				
			||||||
		/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
 | 
							/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return isSafariBrowser || isSafariByFeature
 | 
						return isSafariBrowser || isSafariByFeature
 | 
				
			||||||
| 
						 | 
					@ -28,7 +30,9 @@ export function isSafari(): boolean {
 | 
				
			||||||
 * @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false
 | 
					 * @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function isMobileSafari(): boolean {
 | 
					export function isMobileSafari(): boolean {
 | 
				
			||||||
	return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
 | 
						return (
 | 
				
			||||||
 | 
							/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -39,16 +43,20 @@ export function supportsWebAudioVisualization(): boolean {
 | 
				
			||||||
	// Safari 在某些情况下对 AudioContext 的支持有限制
 | 
						// Safari 在某些情况下对 AudioContext 的支持有限制
 | 
				
			||||||
	// 特别是在处理跨域音频资源时
 | 
						// 特别是在处理跨域音频资源时
 | 
				
			||||||
	if (isSafari()) {
 | 
						if (isSafari()) {
 | 
				
			||||||
		console.log('[BrowserDetection] Safari detected, audio visualization disabled')
 | 
							console.log(
 | 
				
			||||||
 | 
								'[BrowserDetection] Safari detected, audio visualization disabled',
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// 检查基本的 Web Audio API 支持
 | 
						// 检查基本的 Web Audio API 支持
 | 
				
			||||||
	const hasAudioContext = 'AudioContext' in window || 'webkitAudioContext' in window
 | 
						const hasAudioContext =
 | 
				
			||||||
	const hasAnalyserNode = hasAudioContext && (
 | 
							'AudioContext' in window || 'webkitAudioContext' in window
 | 
				
			||||||
		'AnalyserNode' in window || 
 | 
						const hasAnalyserNode =
 | 
				
			||||||
		((window as any).AudioContext && 'createAnalyser' in (window as any).AudioContext.prototype)
 | 
							hasAudioContext &&
 | 
				
			||||||
	)
 | 
							('AnalyserNode' in window ||
 | 
				
			||||||
 | 
								((window as any).AudioContext &&
 | 
				
			||||||
 | 
									'createAnalyser' in (window as any).AudioContext.prototype))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return hasAudioContext && hasAnalyserNode
 | 
						return hasAudioContext && hasAnalyserNode
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -93,6 +101,6 @@ export function getBrowserInfo() {
 | 
				
			||||||
		version: browserVersion,
 | 
							version: browserVersion,
 | 
				
			||||||
		isSafari: isSafari(),
 | 
							isSafari: isSafari(),
 | 
				
			||||||
		isMobileSafari: isMobileSafari(),
 | 
							isMobileSafari: isMobileSafari(),
 | 
				
			||||||
		supportsAudioVisualization: supportsWebAudioVisualization()
 | 
							supportsAudioVisualization: supportsWebAudioVisualization(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,16 @@
 | 
				
			||||||
import artistsOrganize from "./artistsOrganize"
 | 
					import artistsOrganize from './artistsOrganize'
 | 
				
			||||||
import { audioVisualizer } from "./audioVisualizer"
 | 
					import { audioVisualizer } from './audioVisualizer'
 | 
				
			||||||
import cicdInfo from "./cicdInfo"
 | 
					import cicdInfo from './cicdInfo'
 | 
				
			||||||
import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker"
 | 
					import {
 | 
				
			||||||
import { isSafari, isMobileSafari, supportsWebAudioVisualization, getBrowserInfo } from "./browserDetection"
 | 
						checkAndRefreshSongResource,
 | 
				
			||||||
 | 
						checkAndRefreshMultipleSongs,
 | 
				
			||||||
 | 
					} from './songResourceChecker'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						isSafari,
 | 
				
			||||||
 | 
						isMobileSafari,
 | 
				
			||||||
 | 
						supportsWebAudioVisualization,
 | 
				
			||||||
 | 
						getBrowserInfo,
 | 
				
			||||||
 | 
					} from './browserDetection'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
	artistsOrganize,
 | 
						artistsOrganize,
 | 
				
			||||||
| 
						 | 
					@ -13,5 +21,5 @@ export {
 | 
				
			||||||
	isSafari,
 | 
						isSafari,
 | 
				
			||||||
	isMobileSafari,
 | 
						isMobileSafari,
 | 
				
			||||||
	supportsWebAudioVisualization,
 | 
						supportsWebAudioVisualization,
 | 
				
			||||||
	getBrowserInfo
 | 
						getBrowserInfo,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ import apis from '../apis'
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const checkAndRefreshSongResource = async (
 | 
					export const checkAndRefreshSongResource = async (
 | 
				
			||||||
	song: Song,
 | 
						song: Song,
 | 
				
			||||||
  updateCallback?: (updatedSong: Song) => void
 | 
						updateCallback?: (updatedSong: Song) => void,
 | 
				
			||||||
): Promise<Song> => {
 | 
					): Promise<Song> => {
 | 
				
			||||||
	if (!song.sourceUrl) {
 | 
						if (!song.sourceUrl) {
 | 
				
			||||||
		console.warn('[ResourceChecker] 歌曲没有 sourceUrl:', song.name)
 | 
							console.warn('[ResourceChecker] 歌曲没有 sourceUrl:', song.name)
 | 
				
			||||||
| 
						 | 
					@ -21,13 +21,13 @@ export const checkAndRefreshSongResource = async (
 | 
				
			||||||
		await axios.head(song.sourceUrl, {
 | 
							await axios.head(song.sourceUrl, {
 | 
				
			||||||
			headers: {
 | 
								headers: {
 | 
				
			||||||
				'Cache-Control': 'no-cache, no-store, must-revalidate',
 | 
									'Cache-Control': 'no-cache, no-store, must-revalidate',
 | 
				
			||||||
        'Pragma': 'no-cache',
 | 
									Pragma: 'no-cache',
 | 
				
			||||||
        'Expires': '0'
 | 
									Expires: '0',
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			params: {
 | 
								params: {
 | 
				
			||||||
        _t: Date.now() // 添加时间戳参数避免缓存
 | 
									_t: Date.now(), // 添加时间戳参数避免缓存
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
      timeout: 5000 // 5秒超时
 | 
								timeout: 5000, // 5秒超时
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// 资源可用,返回原始歌曲
 | 
							// 资源可用,返回原始歌曲
 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ export const checkAndRefreshSongResource = async (
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export const checkAndRefreshMultipleSongs = async (
 | 
					export const checkAndRefreshMultipleSongs = async (
 | 
				
			||||||
	songs: Song[],
 | 
						songs: Song[],
 | 
				
			||||||
  updateCallback?: (updatedSong: Song, originalIndex: number) => void
 | 
						updateCallback?: (updatedSong: Song, originalIndex: number) => void,
 | 
				
			||||||
): Promise<Song[]> => {
 | 
					): Promise<Song[]> => {
 | 
				
			||||||
	const results: Song[] = []
 | 
						const results: Song[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,7 +71,7 @@ export const checkAndRefreshMultipleSongs = async (
 | 
				
			||||||
		const originalSong = songs[i]
 | 
							const originalSong = songs[i]
 | 
				
			||||||
		const updatedSong = await checkAndRefreshSongResource(
 | 
							const updatedSong = await checkAndRefreshSongResource(
 | 
				
			||||||
			originalSong,
 | 
								originalSong,
 | 
				
			||||||
      updateCallback ? (updated) => updateCallback(updated, i) : undefined
 | 
								updateCallback ? (updated) => updateCallback(updated, i) : undefined,
 | 
				
			||||||
		)
 | 
							)
 | 
				
			||||||
		results.push(updatedSong)
 | 
							results.push(updatedSong)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/vite-env.d.ts
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -35,7 +35,7 @@ interface ApiResponse {
 | 
				
			||||||
	data: unknown
 | 
						data: unknown
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface QueueItem {
 | 
					interface InternalQueueItem {
 | 
				
			||||||
	song: Song
 | 
						song: Song
 | 
				
			||||||
	album?: Album
 | 
						album?: Album
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import tailwindcss from '@tailwindcss/vite'
 | 
					import tailwindcss from '@tailwindcss/vite'
 | 
				
			||||||
import vue from '@vitejs/plugin-vue'
 | 
					import vue from '@vitejs/plugin-vue'
 | 
				
			||||||
import { defineConfig } from 'vite'
 | 
					import { defineConfig } from 'vite'
 | 
				
			||||||
import path from "node:path"
 | 
					import path from 'node:path'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// https://vite.dev/config/
 | 
					// https://vite.dev/config/
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@ export default defineConfig({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	resolve: {
 | 
						resolve: {
 | 
				
			||||||
		alias: {
 | 
							alias: {
 | 
				
			||||||
      "@": path.resolve(__dirname, "./src"),
 | 
								'@': path.resolve(__dirname, './src'),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user