fix: 修复播放间背景图像和歌词不显示的问题
This commit is contained in:
		
							parent
							
								
									f1fb5330a9
								
							
						
					
					
						commit
						4f0b897b4c
					
				
							
								
								
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -44,9 +44,9 @@
 | 
			
		|||
			}
 | 
			
		||||
		},
 | 
			
		||||
		"node_modules/@astrian/music-surge-revolution": {
 | 
			
		||||
			"version": "0.0.0-20250831055015",
 | 
			
		||||
			"resolved": "https://registry.npmjs.org/@astrian/music-surge-revolution/-/music-surge-revolution-0.0.0-20250831055015.tgz",
 | 
			
		||||
			"integrity": "sha512-joXpUDjez+5M90C4RoGsfHZifXdUBhqSHH+kW3v6TDQJQZwh/sdof1ro4qYXG3/8D8AkfWdhFV3O1C8nxG6syw=="
 | 
			
		||||
			"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": {
 | 
			
		||||
			"version": "7.27.1",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
console.log("aaaa")
 | 
			
		||||
console.log('aaaa')
 | 
			
		||||
 | 
			
		||||
// 兼容 Chrome 和 Firefox
 | 
			
		||||
const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
 | 
			
		||||
const browserAPI = typeof browser !== 'undefined' ? browser : chrome
 | 
			
		||||
 | 
			
		||||
browserAPI.webRequest.onBeforeRequest.addListener(
 | 
			
		||||
	async (details) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -16,12 +16,18 @@ browserAPI.webRequest.onBeforeRequest.addListener(
 | 
			
		|||
		console.log('recived request for fontset api, redirecting to index.html')
 | 
			
		||||
		const pref = await browserAPI.storage.sync.get('preferences')
 | 
			
		||||
 | 
			
		||||
		if (pref === undefined || pref.preferences === undefined || pref.preferences.autoRedirect === undefined || pref.preferences.autoRedirect === true) {
 | 
			
		||||
			const isChrome = typeof browserAPI.runtime.getBrowserInfo === 'undefined';
 | 
			
		||||
		if (
 | 
			
		||||
			pref === undefined ||
 | 
			
		||||
			pref.preferences === undefined ||
 | 
			
		||||
			pref.preferences.autoRedirect === undefined ||
 | 
			
		||||
			pref.preferences.autoRedirect === true
 | 
			
		||||
		) {
 | 
			
		||||
			const isChrome = typeof browserAPI.runtime.getBrowserInfo === 'undefined'
 | 
			
		||||
 | 
			
		||||
			if (isChrome) {
 | 
			
		||||
				if (
 | 
			
		||||
					details.url === 'https://monster-siren.hypergryph.com/manifest.json' &&
 | 
			
		||||
					details.url ===
 | 
			
		||||
						'https://monster-siren.hypergryph.com/manifest.json' &&
 | 
			
		||||
					details.type === 'other' &&
 | 
			
		||||
					details.frameId === 0
 | 
			
		||||
				) {
 | 
			
		||||
| 
						 | 
				
			
			@ -32,15 +38,22 @@ browserAPI.webRequest.onBeforeRequest.addListener(
 | 
			
		|||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				// 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
 | 
			
		||||
const actionAPI = browserAPI.action || browserAPI.browserAction;
 | 
			
		||||
const actionAPI = browserAPI.action || browserAPI.browserAction
 | 
			
		||||
if (actionAPI) {
 | 
			
		||||
	actionAPI.onClicked.addListener(() => {
 | 
			
		||||
		browserAPI.tabs.create({ url: browserAPI.runtime.getURL('index.html') })
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,12 +5,8 @@
 | 
			
		|||
	"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
 | 
			
		||||
	"content_scripts": [
 | 
			
		||||
		{
 | 
			
		||||
			"matches": [
 | 
			
		||||
				"https://monster-siren.hypergryph.com/"
 | 
			
		||||
			],
 | 
			
		||||
			"js": [
 | 
			
		||||
				"content.js"
 | 
			
		||||
			],
 | 
			
		||||
			"matches": ["https://monster-siren.hypergryph.com/"],
 | 
			
		||||
			"js": ["content.js"],
 | 
			
		||||
			"run_at": "document_end"
 | 
			
		||||
		}
 | 
			
		||||
	],
 | 
			
		||||
| 
						 | 
				
			
			@ -36,11 +32,7 @@
 | 
			
		|||
	"background": {
 | 
			
		||||
		"service_worker": "background.js"
 | 
			
		||||
	},
 | 
			
		||||
	"permissions": [
 | 
			
		||||
		"tabs",
 | 
			
		||||
		"webRequest",
 | 
			
		||||
		"storage"
 | 
			
		||||
	],
 | 
			
		||||
	"permissions": ["tabs", "webRequest", "storage"],
 | 
			
		||||
	"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;",
 | 
			
		||||
		"sandbox": "sandbox"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,61 +1,65 @@
 | 
			
		|||
import fs from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { fileURLToPath } from 'url';
 | 
			
		||||
import fs from 'fs'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import { fileURLToPath } from 'url'
 | 
			
		||||
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const __dirname = path.dirname(__filename);
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url)
 | 
			
		||||
const __dirname = path.dirname(__filename)
 | 
			
		||||
 | 
			
		||||
// 处理 manifest.json
 | 
			
		||||
function processManifest() {
 | 
			
		||||
  const manifestPath = path.join(__dirname, '../public/manifest.json');
 | 
			
		||||
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
 | 
			
		||||
	const manifestPath = path.join(__dirname, '../public/manifest.json')
 | 
			
		||||
	const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
 | 
			
		||||
 | 
			
		||||
	// 移除本地调试相关的配置
 | 
			
		||||
	if (manifest.host_permissions) {
 | 
			
		||||
		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 中的本地开发相关配置
 | 
			
		||||
    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(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
			
		||||
				.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
			
		||||
				.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
			
		||||
				.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
			
		||||
      .trim();
 | 
			
		||||
				.trim()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
			
		||||
  console.log('✅ Manifest.json processed');
 | 
			
		||||
	fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 | 
			
		||||
	console.log('✅ Manifest.json processed')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理 index.html
 | 
			
		||||
function processIndexHtml() {
 | 
			
		||||
  const indexPath = path.join(__dirname, '../index.html');
 | 
			
		||||
  let content = fs.readFileSync(indexPath, 'utf8');
 | 
			
		||||
	const indexPath = path.join(__dirname, '../index.html')
 | 
			
		||||
	let content = fs.readFileSync(indexPath, 'utf8')
 | 
			
		||||
 | 
			
		||||
	// 替换脚本地址
 | 
			
		||||
	content = content.replace(
 | 
			
		||||
		/src="[^"]*\/src\/main\.ts"/g,
 | 
			
		||||
    'src="./src/main.ts"'
 | 
			
		||||
  );
 | 
			
		||||
		'src="./src/main.ts"',
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 移除 crossorigin 属性
 | 
			
		||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
			
		||||
	content = content.replace(/\s+crossorigin/g, '')
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(indexPath, content);
 | 
			
		||||
  console.log('✅ Index.html processed');
 | 
			
		||||
	fs.writeFileSync(indexPath, content)
 | 
			
		||||
	console.log('✅ Index.html processed')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行处理
 | 
			
		||||
try {
 | 
			
		||||
  processManifest();
 | 
			
		||||
  processIndexHtml();
 | 
			
		||||
  console.log('🎉 Build preparation completed!');
 | 
			
		||||
	processManifest()
 | 
			
		||||
	processIndexHtml()
 | 
			
		||||
	console.log('🎉 Build preparation completed!')
 | 
			
		||||
} catch (error) {
 | 
			
		||||
  console.error('❌ Error during build preparation:', error);
 | 
			
		||||
  process.exit(1);
 | 
			
		||||
	console.error('❌ Error during build preparation:', error)
 | 
			
		||||
	process.exit(1)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,42 +1,49 @@
 | 
			
		|||
import fs from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { fileURLToPath } from 'url';
 | 
			
		||||
import fs from 'fs'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import { fileURLToPath } from 'url'
 | 
			
		||||
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const __dirname = path.dirname(__filename);
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url)
 | 
			
		||||
const __dirname = path.dirname(__filename)
 | 
			
		||||
 | 
			
		||||
// 处理 manifest.json
 | 
			
		||||
function processManifest() {
 | 
			
		||||
  const manifestPath = path.join(__dirname, '../public/manifest.json');
 | 
			
		||||
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
 | 
			
		||||
	const manifestPath = path.join(__dirname, '../public/manifest.json')
 | 
			
		||||
	const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
 | 
			
		||||
 | 
			
		||||
	// 移除本地调试相关的配置
 | 
			
		||||
	if (manifest.host_permissions) {
 | 
			
		||||
		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 中的本地开发相关配置
 | 
			
		||||
    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(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
			
		||||
				.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
			
		||||
				.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
			
		||||
				.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
			
		||||
      .trim();
 | 
			
		||||
				.trim()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 移除 CSP 中的 sandbox 配置(Firefox 不支持)
 | 
			
		||||
  if (manifest.content_security_policy && manifest.content_security_policy.sandbox) {
 | 
			
		||||
    delete manifest.content_security_policy.sandbox;
 | 
			
		||||
	if (
 | 
			
		||||
		manifest.content_security_policy &&
 | 
			
		||||
		manifest.content_security_policy.sandbox
 | 
			
		||||
	) {
 | 
			
		||||
		delete manifest.content_security_policy.sandbox
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 移除 background.service_worker,替换为 background.scripts
 | 
			
		||||
	if (manifest.background && manifest.background.service_worker) {
 | 
			
		||||
    manifest.background.scripts = [manifest.background.service_worker];
 | 
			
		||||
    delete manifest.background.service_worker;
 | 
			
		||||
		manifest.background.scripts = [manifest.background.service_worker]
 | 
			
		||||
		delete manifest.background.service_worker
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 添加 firefox 特有配置
 | 
			
		||||
| 
						 | 
				
			
			@ -44,37 +51,37 @@ function processManifest() {
 | 
			
		|||
		gecko: {
 | 
			
		||||
			id: 'msr-mod@firefox-addon.astrian.moe',
 | 
			
		||||
			strict_min_version: '115.0',
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
			
		||||
  console.log('✅ Manifest.json processed');
 | 
			
		||||
	fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 | 
			
		||||
	console.log('✅ Manifest.json processed')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理 index.html
 | 
			
		||||
function processIndexHtml() {
 | 
			
		||||
  const indexPath = path.join(__dirname, '../index.html');
 | 
			
		||||
  let content = fs.readFileSync(indexPath, 'utf8');
 | 
			
		||||
	const indexPath = path.join(__dirname, '../index.html')
 | 
			
		||||
	let content = fs.readFileSync(indexPath, 'utf8')
 | 
			
		||||
 | 
			
		||||
	// 替换脚本地址
 | 
			
		||||
	content = content.replace(
 | 
			
		||||
		/src="[^"]*\/src\/main\.ts"/g,
 | 
			
		||||
    'src="./src/main.ts"'
 | 
			
		||||
  );
 | 
			
		||||
		'src="./src/main.ts"',
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 移除 crossorigin 属性
 | 
			
		||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
			
		||||
	content = content.replace(/\s+crossorigin/g, '')
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(indexPath, content);
 | 
			
		||||
  console.log('✅ Index.html processed');
 | 
			
		||||
	fs.writeFileSync(indexPath, content)
 | 
			
		||||
	console.log('✅ Index.html processed')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行处理
 | 
			
		||||
try {
 | 
			
		||||
  processManifest();
 | 
			
		||||
  processIndexHtml();
 | 
			
		||||
  console.log('🎉 Build preparation completed!');
 | 
			
		||||
	processManifest()
 | 
			
		||||
	processIndexHtml()
 | 
			
		||||
	console.log('🎉 Build preparation completed!')
 | 
			
		||||
} catch (error) {
 | 
			
		||||
  console.error('❌ Error during build preparation:', error);
 | 
			
		||||
  process.exit(1);
 | 
			
		||||
	console.error('❌ Error during build preparation:', error)
 | 
			
		||||
	process.exit(1)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,39 +1,43 @@
 | 
			
		|||
import fs from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { fileURLToPath } from 'url';
 | 
			
		||||
import fs from 'fs'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import { fileURLToPath } from 'url'
 | 
			
		||||
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const __dirname = path.dirname(__filename);
 | 
			
		||||
const __filename = fileURLToPath(import.meta.url)
 | 
			
		||||
const __dirname = path.dirname(__filename)
 | 
			
		||||
 | 
			
		||||
// 处理 manifest.json for Safari
 | 
			
		||||
function processManifest() {
 | 
			
		||||
  const manifestPath = path.join(__dirname, '../public/manifest.json');
 | 
			
		||||
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
 | 
			
		||||
	const manifestPath = path.join(__dirname, '../public/manifest.json')
 | 
			
		||||
	const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
 | 
			
		||||
 | 
			
		||||
	// 移除本地调试相关的配置
 | 
			
		||||
	if (manifest.host_permissions) {
 | 
			
		||||
		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 中的本地开发相关配置
 | 
			
		||||
    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(/\s*http:\/\/localhost:5173\s*/g, ' ')
 | 
			
		||||
				.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
 | 
			
		||||
				.replace(/;\s+/g, '; ') // 标准化分号后的空格
 | 
			
		||||
				.replace(/\s+/g, ' ') // 合并多个空格为一个
 | 
			
		||||
      .trim();
 | 
			
		||||
				.trim()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Safari 特殊处理:添加 appShell.html 到 content scripts 匹配
 | 
			
		||||
	if (manifest.content_scripts && manifest.content_scripts[0]) {
 | 
			
		||||
		// 添加 appShell.html 的匹配规则
 | 
			
		||||
    const existingMatches = manifest.content_scripts[0].matches;
 | 
			
		||||
    if (!existingMatches.includes("https://monster-siren.hypergryph.com/")) {
 | 
			
		||||
      existingMatches.push("https://monster-siren.hypergryph.com/");
 | 
			
		||||
		const existingMatches = manifest.content_scripts[0].matches
 | 
			
		||||
		if (!existingMatches.includes('https://monster-siren.hypergryph.com/')) {
 | 
			
		||||
			existingMatches.push('https://monster-siren.hypergryph.com/')
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -42,13 +46,13 @@ function processManifest() {
 | 
			
		|||
		// Safari 扩展在 Manifest V3 中必须使用 persistent: false
 | 
			
		||||
		// 但为了调试,我们暂时设为 true 来确保页面加载
 | 
			
		||||
		manifest.background = {
 | 
			
		||||
      page: "background.html",
 | 
			
		||||
      persistent: true
 | 
			
		||||
    };
 | 
			
		||||
			page: 'background.html',
 | 
			
		||||
			persistent: true,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建 background.html 文件用于 Safari
 | 
			
		||||
  const backgroundHtmlPath = path.join(__dirname, '../public/background.html');
 | 
			
		||||
	const backgroundHtmlPath = path.join(__dirname, '../public/background.html')
 | 
			
		||||
	const backgroundHtmlContent = `<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
| 
						 | 
				
			
			@ -102,16 +106,16 @@ function processManifest() {
 | 
			
		|||
    log('=== After background.js script tag ===');
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>`;
 | 
			
		||||
  fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent);
 | 
			
		||||
</html>`
 | 
			
		||||
	fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent)
 | 
			
		||||
 | 
			
		||||
	// 创建 Safari 兼容的 background.js
 | 
			
		||||
  const backgroundJsPath = path.join(__dirname, '../public/background.js');
 | 
			
		||||
  let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8');
 | 
			
		||||
	const backgroundJsPath = path.join(__dirname, '../public/background.js')
 | 
			
		||||
	let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8')
 | 
			
		||||
 | 
			
		||||
	// 检查是否已经添加过 Safari 代码,避免重复
 | 
			
		||||
	if (backgroundJsContent.includes('=== Safari background.js starting ===')) {
 | 
			
		||||
    console.log('Safari background.js already processed, skipping...');
 | 
			
		||||
		console.log('Safari background.js already processed, skipping...')
 | 
			
		||||
	} else {
 | 
			
		||||
		// 在开头添加 Safari 调试信息(只添加一次)
 | 
			
		||||
		const safariDebugCode = `
 | 
			
		||||
| 
						 | 
				
			
			@ -168,38 +172,40 @@ if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage)
 | 
			
		|||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
`;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
		// 替换 Safari 的重定向 URL 监听
 | 
			
		||||
		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', '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 的重定向判断逻辑
 | 
			
		||||
		backgroundJsContent = backgroundJsContent.replace(
 | 
			
		||||
			/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(
 | 
			
		||||
			/\(\(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);
 | 
			
		||||
  console.log('✅ Safari-compatible background.js created');
 | 
			
		||||
	fs.writeFileSync(backgroundJsPath, backgroundJsContent)
 | 
			
		||||
	console.log('✅ Safari-compatible background.js created')
 | 
			
		||||
 | 
			
		||||
	// 创建 Safari 专用的 content.js
 | 
			
		||||
  const contentJsPath = path.join(__dirname, '../public/content.js');
 | 
			
		||||
	const contentJsPath = path.join(__dirname, '../public/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')) {
 | 
			
		||||
    console.log('Safari content.js already processed, skipping...');
 | 
			
		||||
		console.log('Safari content.js already processed, skipping...')
 | 
			
		||||
	} else {
 | 
			
		||||
		const contentJsContent = `
 | 
			
		||||
// Safari 扩展 content script for redirect
 | 
			
		||||
| 
						 | 
				
			
			@ -307,53 +313,53 @@ async function main() {
 | 
			
		|||
main().catch(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 可能需要额外的权限
 | 
			
		||||
	if (!manifest.permissions.includes('activeTab')) {
 | 
			
		||||
    manifest.permissions.push('activeTab');
 | 
			
		||||
		manifest.permissions.push('activeTab')
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 添加 Safari 特有配置
 | 
			
		||||
	manifest.browser_specific_settings = {
 | 
			
		||||
		safari: {
 | 
			
		||||
      minimum_version: "14.0"
 | 
			
		||||
			minimum_version: '14.0',
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
			
		||||
  console.log('✅ Safari Manifest.json processed');
 | 
			
		||||
  console.log('✅ Background.html created for Safari');
 | 
			
		||||
	fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
 | 
			
		||||
	console.log('✅ Safari Manifest.json processed')
 | 
			
		||||
	console.log('✅ Background.html created for Safari')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理 index.html
 | 
			
		||||
function processIndexHtml() {
 | 
			
		||||
  const indexPath = path.join(__dirname, '../index.html');
 | 
			
		||||
  let content = fs.readFileSync(indexPath, 'utf8');
 | 
			
		||||
	const indexPath = path.join(__dirname, '../index.html')
 | 
			
		||||
	let content = fs.readFileSync(indexPath, 'utf8')
 | 
			
		||||
 | 
			
		||||
	// 替换脚本地址
 | 
			
		||||
	content = content.replace(
 | 
			
		||||
		/src="[^"]*\/src\/main\.ts"/g,
 | 
			
		||||
    'src="./src/main.ts"'
 | 
			
		||||
  );
 | 
			
		||||
		'src="./src/main.ts"',
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 移除 crossorigin 属性
 | 
			
		||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
			
		||||
	content = content.replace(/\s+crossorigin/g, '')
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(indexPath, content);
 | 
			
		||||
  console.log('✅ Index.html processed for Safari');
 | 
			
		||||
	fs.writeFileSync(indexPath, content)
 | 
			
		||||
	console.log('✅ Index.html processed for Safari')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行处理
 | 
			
		||||
try {
 | 
			
		||||
  processManifest();
 | 
			
		||||
  processIndexHtml();
 | 
			
		||||
  console.log('🎉 Safari build preparation completed!');
 | 
			
		||||
	processManifest()
 | 
			
		||||
	processIndexHtml()
 | 
			
		||||
	console.log('🎉 Safari build preparation completed!')
 | 
			
		||||
} catch (error) {
 | 
			
		||||
  console.error('❌ Error during Safari build preparation:', error);
 | 
			
		||||
  process.exit(1);
 | 
			
		||||
	console.error('❌ Error during Safari build preparation:', error)
 | 
			
		||||
	process.exit(1)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,10 +16,12 @@ const presentPreferencePanel = ref(false)
 | 
			
		|||
const route = useRoute()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
 | 
			
		||||
watch(() => presentPreferencePanel, (value) => {
 | 
			
		||||
watch(
 | 
			
		||||
	() => presentPreferencePanel,
 | 
			
		||||
	(value) => {
 | 
			
		||||
		console.log(value)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,33 +9,41 @@ export default {
 | 
			
		|||
		const songs: {
 | 
			
		||||
			data: ApiResponse
 | 
			
		||||
		} = 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 } }
 | 
			
		||||
	},
 | 
			
		||||
	async getSong(cid: string) {
 | 
			
		||||
		const song: {
 | 
			
		||||
			data: ApiResponse
 | 
			
		||||
		} = 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
 | 
			
		||||
	},
 | 
			
		||||
	async getAlbums() {
 | 
			
		||||
		const albums: {
 | 
			
		||||
			data: ApiResponse
 | 
			
		||||
		} = 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
 | 
			
		||||
	},
 | 
			
		||||
	async getAlbum(cid: string) {
 | 
			
		||||
		const album: {
 | 
			
		||||
			data: ApiResponse
 | 
			
		||||
		} = 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: {
 | 
			
		||||
			data: ApiResponse
 | 
			
		||||
		} = await msrInstance.get(`album/${cid}/data`)
 | 
			
		||||
		let data = album.data.data as Album
 | 
			
		||||
		data.artistes = (albumMeta.data.data as Album).artistes
 | 
			
		||||
		return data
 | 
			
		||||
	}
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,9 @@ const closeButton = ref<HTMLElement>()
 | 
			
		|||
 | 
			
		||||
// Animation functions
 | 
			
		||||
const animateIn = async () => {
 | 
			
		||||
	if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {return}
 | 
			
		||||
	if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Set initial states
 | 
			
		||||
	gsap.set(dialogBackdrop.value, { opacity: 0 })
 | 
			
		||||
| 
						 | 
				
			
			@ -39,48 +41,66 @@ const animateIn = async () => {
 | 
			
		|||
	tl.to(dialogBackdrop.value, {
 | 
			
		||||
		opacity: 1,
 | 
			
		||||
		duration: 0.3,
 | 
			
		||||
		ease: "power2.out"
 | 
			
		||||
		ease: 'power2.out',
 | 
			
		||||
	})
 | 
			
		||||
		.to(dialogContent.value, {
 | 
			
		||||
		.to(
 | 
			
		||||
			dialogContent.value,
 | 
			
		||||
			{
 | 
			
		||||
				y: 0,
 | 
			
		||||
				opacity: 1,
 | 
			
		||||
				scale: 1,
 | 
			
		||||
				duration: 0.4,
 | 
			
		||||
			ease: "power3.out"
 | 
			
		||||
		}, "-=0.1")
 | 
			
		||||
		.to(closeButton.value, {
 | 
			
		||||
				ease: 'power3.out',
 | 
			
		||||
			},
 | 
			
		||||
			'-=0.1',
 | 
			
		||||
		)
 | 
			
		||||
		.to(
 | 
			
		||||
			closeButton.value,
 | 
			
		||||
			{
 | 
			
		||||
				scale: 1,
 | 
			
		||||
				rotation: 0,
 | 
			
		||||
				duration: 0.3,
 | 
			
		||||
			ease: "back.out(1.7)"
 | 
			
		||||
		}, "-=0.2")
 | 
			
		||||
				ease: 'back.out(1.7)',
 | 
			
		||||
			},
 | 
			
		||||
			'-=0.2',
 | 
			
		||||
		)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const animateOut = () => {
 | 
			
		||||
	if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {return}
 | 
			
		||||
	if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const tl = gsap.timeline({
 | 
			
		||||
		onComplete: () => emit('dismiss')
 | 
			
		||||
		onComplete: () => emit('dismiss'),
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	tl.to(closeButton.value, {
 | 
			
		||||
		scale: 0,
 | 
			
		||||
		rotation: 180,
 | 
			
		||||
		duration: 0.2,
 | 
			
		||||
		ease: "power2.in"
 | 
			
		||||
		ease: 'power2.in',
 | 
			
		||||
	})
 | 
			
		||||
		.to(dialogContent.value, {
 | 
			
		||||
		.to(
 | 
			
		||||
			dialogContent.value,
 | 
			
		||||
			{
 | 
			
		||||
				y: 30,
 | 
			
		||||
				opacity: 0,
 | 
			
		||||
				scale: 0.95,
 | 
			
		||||
				duration: 0.3,
 | 
			
		||||
			ease: "power2.in"
 | 
			
		||||
		}, "-=0.1")
 | 
			
		||||
		.to(dialogBackdrop.value, {
 | 
			
		||||
				ease: 'power2.in',
 | 
			
		||||
			},
 | 
			
		||||
			'-=0.1',
 | 
			
		||||
		)
 | 
			
		||||
		.to(
 | 
			
		||||
			dialogBackdrop.value,
 | 
			
		||||
			{
 | 
			
		||||
				opacity: 0,
 | 
			
		||||
				duration: 0.2,
 | 
			
		||||
			ease: "power2.in"
 | 
			
		||||
		}, "-=0.1")
 | 
			
		||||
				ease: 'power2.in',
 | 
			
		||||
			},
 | 
			
		||||
			'-=0.1',
 | 
			
		||||
		)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// biome-ignore lint/correctness/noUnusedVariables: used inside <template>
 | 
			
		||||
| 
						 | 
				
			
			@ -88,33 +108,47 @@ const handleClose = () => {
 | 
			
		|||
	animateOut()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
watch(() => props.present, async (newVal) => {
 | 
			
		||||
watch(
 | 
			
		||||
	() => props.present,
 | 
			
		||||
	async (newVal) => {
 | 
			
		||||
		if (newVal) {
 | 
			
		||||
			await nextTick()
 | 
			
		||||
			animateIn()
 | 
			
		||||
		}
 | 
			
		||||
})
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
watch(() => props.albumCid, async () => {
 | 
			
		||||
	console.log("AlbumDetailDialog mounted with albumCid:", props.albumCid)
 | 
			
		||||
watch(
 | 
			
		||||
	() => props.albumCid,
 | 
			
		||||
	async () => {
 | 
			
		||||
		console.log('AlbumDetailDialog mounted with albumCid:', props.albumCid)
 | 
			
		||||
		album.value = undefined // Reset album when cid changes
 | 
			
		||||
		try {
 | 
			
		||||
			const res = await apis.getAlbum(props.albumCid)
 | 
			
		||||
			for (const track in res.songs) {
 | 
			
		||||
			res.songs[Number.parseInt(track, 10)] = await apis.getSong(res.songs[Number.parseInt(track, 10)].cid)
 | 
			
		||||
				res.songs[Number.parseInt(track, 10)] = await apis.getSong(
 | 
			
		||||
					res.songs[Number.parseInt(track, 10)].cid,
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
			album.value = res
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error(error)
 | 
			
		||||
		}
 | 
			
		||||
})
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const playQueue = usePlayStore()
 | 
			
		||||
 | 
			
		||||
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
			
		||||
function playTheAlbum(from = 0) {
 | 
			
		||||
	if (playQueue.queueReplaceLock) {
 | 
			
		||||
		if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
 | 
			
		||||
		if (
 | 
			
		||||
			!confirm(
 | 
			
		||||
				'当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?',
 | 
			
		||||
			)
 | 
			
		||||
		) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		playQueue.queueReplaceLock = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -123,7 +157,7 @@ function playTheAlbum(from = 0) {
 | 
			
		|||
		console.log(track)
 | 
			
		||||
		newPlayQueue.push({
 | 
			
		||||
			song: track,
 | 
			
		||||
			album: album.value
 | 
			
		||||
			album: album.value,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	playQueue.replaceQueue(newPlayQueue)
 | 
			
		||||
| 
						 | 
				
			
			@ -142,7 +176,6 @@ function shuffle() {
 | 
			
		|||
	// 	playQueue.isBuffering = true
 | 
			
		||||
	// }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -63,7 +63,9 @@ function moveUp() {
 | 
			
		|||
 | 
			
		||||
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
	playQueueStore.queueReplaceLock = true
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +116,10 @@ function removeItem() {
 | 
			
		|||
			playQueueStore.currentIndex--
 | 
			
		||||
		} else if (props.index === playQueueStore.currentIndex) {
 | 
			
		||||
			if (queue.length > 0) {
 | 
			
		||||
				playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
 | 
			
		||||
				playQueueStore.currentIndex = Math.min(
 | 
			
		||||
					playQueueStore.currentIndex,
 | 
			
		||||
					queue.length - 1,
 | 
			
		||||
				)
 | 
			
		||||
			} else {
 | 
			
		||||
				playQueueStore.currentIndex = 0
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -145,7 +150,10 @@ function removeItem() {
 | 
			
		|||
			playQueueStore.currentIndex--
 | 
			
		||||
		} else if (props.index === playQueueStore.currentIndex) {
 | 
			
		||||
			if (queue.length > 0) {
 | 
			
		||||
				playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
 | 
			
		||||
				playQueueStore.currentIndex = Math.min(
 | 
			
		||||
					playQueueStore.currentIndex,
 | 
			
		||||
					queue.length - 1,
 | 
			
		||||
				)
 | 
			
		||||
			} else {
 | 
			
		||||
				playQueueStore.currentIndex = 0
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
 | 
			
		||||
import XIcon from '../assets/icons/x.vue'
 | 
			
		||||
import { usePreferences } from '../stores/usePreferences'
 | 
			
		||||
import { computed } from 'vue'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -135,17 +135,25 @@ const props = defineProps<{
 | 
			
		|||
 | 
			
		||||
// 滚动指示器相关计算
 | 
			
		||||
const scrollIndicatorHeight = computed(() => {
 | 
			
		||||
	if (parsedLyrics.value.length === 0) {return 0}
 | 
			
		||||
	return Math.max(10, 100 / parsedLyrics.value.length * 5) // 显示大约5行的比例
 | 
			
		||||
	if (parsedLyrics.value.length === 0) {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	return Math.max(10, (100 / parsedLyrics.value.length) * 5) // 显示大约5行的比例
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
			
		||||
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 containerHeight = lyricsContainer.value?.clientHeight || 400
 | 
			
		||||
	const indicatorTrackHeight = containerHeight / 2 // 指示器轨道高度
 | 
			
		||||
	return progress * (indicatorTrackHeight - (scrollIndicatorHeight.value / 100 * indicatorTrackHeight))
 | 
			
		||||
	return (
 | 
			
		||||
		progress *
 | 
			
		||||
		(indicatorTrackHeight -
 | 
			
		||||
			(scrollIndicatorHeight.value / 100) * indicatorTrackHeight)
 | 
			
		||||
	)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 设置行引用
 | 
			
		||||
| 
						 | 
				
			
			@ -157,15 +165,18 @@ function setLineRef(el: HTMLElement | null, index: number) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// 歌词解析函数
 | 
			
		||||
function parseLyrics(lrcText: string, minGapDuration = 5): (LyricsLine | GapLine)[] {
 | 
			
		||||
function parseLyrics(
 | 
			
		||||
	lrcText: string,
 | 
			
		||||
	minGapDuration = 5,
 | 
			
		||||
): (LyricsLine | GapLine)[] {
 | 
			
		||||
	if (!lrcText) {
 | 
			
		||||
		return [
 | 
			
		||||
			{
 | 
			
		||||
				type: 'lyric',
 | 
			
		||||
				time: 0,
 | 
			
		||||
				text: '',
 | 
			
		||||
				originalTime: '[00:00]'
 | 
			
		||||
			}
 | 
			
		||||
				originalTime: '[00:00]',
 | 
			
		||||
			},
 | 
			
		||||
		]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -176,14 +187,18 @@ function parseLyrics(lrcText: string, minGapDuration = 5): (LyricsLine | GapLine
 | 
			
		|||
 | 
			
		||||
	for (const line of lines) {
 | 
			
		||||
		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()
 | 
			
		||||
 | 
			
		||||
		for (const match of matches) {
 | 
			
		||||
			const minutes = Number.parseInt(match[1])
 | 
			
		||||
			const seconds = Number.parseInt(match[2])
 | 
			
		||||
			const milliseconds = match[3] ? Number.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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -192,13 +207,13 @@ function parseLyrics(lrcText: string, minGapDuration = 5): (LyricsLine | GapLine
 | 
			
		|||
					type: 'lyric',
 | 
			
		||||
					time: totalSeconds,
 | 
			
		||||
					text: text,
 | 
			
		||||
					originalTime: match[0]
 | 
			
		||||
					originalTime: match[0],
 | 
			
		||||
				})
 | 
			
		||||
			} else {
 | 
			
		||||
				tempParsedLines.push({
 | 
			
		||||
					type: 'gap',
 | 
			
		||||
					time: totalSeconds,
 | 
			
		||||
					originalTime: match[0]
 | 
			
		||||
					originalTime: match[0],
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -207,14 +222,20 @@ function parseLyrics(lrcText: string, minGapDuration = 5): (LyricsLine | GapLine
 | 
			
		|||
	tempParsedLines.sort((a, b) => a.time - b.time)
 | 
			
		||||
 | 
			
		||||
	const finalLines: (LyricsLine | GapLine)[] = []
 | 
			
		||||
	const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
 | 
			
		||||
	const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
 | 
			
		||||
	const lyricLines = tempParsedLines.filter(
 | 
			
		||||
		(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++) {
 | 
			
		||||
		const gapLine = gapLines[i]
 | 
			
		||||
		const nextLyricLine = lyricLines.find(lyric => lyric.time > gapLine.time)
 | 
			
		||||
		const nextLyricLine = lyricLines.find((lyric) => lyric.time > gapLine.time)
 | 
			
		||||
 | 
			
		||||
		if (nextLyricLine) {
 | 
			
		||||
			const duration = nextLyricLine.time - gapLine.time
 | 
			
		||||
| 
						 | 
				
			
			@ -233,16 +254,20 @@ function parseLyrics(lrcText: string, minGapDuration = 5): (LyricsLine | GapLine
 | 
			
		|||
		type: 'lyric',
 | 
			
		||||
		time: 0,
 | 
			
		||||
		text: '',
 | 
			
		||||
		originalTime: '[00:00]'
 | 
			
		||||
		originalTime: '[00:00]',
 | 
			
		||||
	})
 | 
			
		||||
	return sortedLines
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查找当前行索引
 | 
			
		||||
function findCurrentLineIndex(time: number): number {
 | 
			
		||||
	if (parsedLyrics.value.length === 0) {return -1}
 | 
			
		||||
	if (parsedLyrics.value.length === 0) {
 | 
			
		||||
		return -1
 | 
			
		||||
	}
 | 
			
		||||
	// 如果时间小于第一句歌词,则返回0(空行)
 | 
			
		||||
	if (time < parsedLyrics.value[1]?.time) {return 0}
 | 
			
		||||
	if (time < parsedLyrics.value[1]?.time) {
 | 
			
		||||
		return 0
 | 
			
		||||
	}
 | 
			
		||||
	let index = 0
 | 
			
		||||
	for (let i = 1; i < parsedLyrics.value.length; i++) {
 | 
			
		||||
		if (time >= parsedLyrics.value[i].time) {
 | 
			
		||||
| 
						 | 
				
			
			@ -256,7 +281,13 @@ function findCurrentLineIndex(time: number): number {
 | 
			
		|||
 | 
			
		||||
// 使用 GSAP 滚动到指定行
 | 
			
		||||
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 wrapper = lyricsWrapper.value
 | 
			
		||||
| 
						 | 
				
			
			@ -280,10 +311,10 @@ function scrollToLine(lineIndex: number, smooth = true) {
 | 
			
		|||
		scrollTween = gsap.to(wrapper, {
 | 
			
		||||
			y: targetY,
 | 
			
		||||
			duration: 0.8,
 | 
			
		||||
			ease: "power2.out",
 | 
			
		||||
			ease: 'power2.out',
 | 
			
		||||
			onComplete: () => {
 | 
			
		||||
				scrollTween = null
 | 
			
		||||
			}
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
	} else {
 | 
			
		||||
		gsap.set(wrapper, { y: targetY })
 | 
			
		||||
| 
						 | 
				
			
			@ -292,7 +323,9 @@ function scrollToLine(lineIndex: number, smooth = true) {
 | 
			
		|||
 | 
			
		||||
// 高亮当前行动画
 | 
			
		||||
function highlightCurrentLine(lineIndex: number) {
 | 
			
		||||
	if (!lineRefs.value[lineIndex]) {return}
 | 
			
		||||
	if (!lineRefs.value[lineIndex]) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const lineElement = lineRefs.value[lineIndex]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -308,7 +341,7 @@ function highlightCurrentLine(lineIndex: number) {
 | 
			
		|||
				scale: 1,
 | 
			
		||||
				opacity: index < lineIndex ? 0.6 : 0.4,
 | 
			
		||||
				duration: 0.3,
 | 
			
		||||
				ease: "power2.out"
 | 
			
		||||
				ease: 'power2.out',
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
| 
						 | 
				
			
			@ -318,10 +351,10 @@ function highlightCurrentLine(lineIndex: number) {
 | 
			
		|||
		scale: 1.05,
 | 
			
		||||
		opacity: 1,
 | 
			
		||||
		duration: 0.2,
 | 
			
		||||
		ease: "back.out(1.7)",
 | 
			
		||||
		ease: 'back.out(1.7)',
 | 
			
		||||
		onComplete: () => {
 | 
			
		||||
			highlightTween = null
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -330,7 +363,9 @@ function highlightCurrentLine(lineIndex: number) {
 | 
			
		|||
function handleWheel(event: WheelEvent) {
 | 
			
		||||
	event.preventDefault()
 | 
			
		||||
 | 
			
		||||
	if (!lyricsWrapper.value || !lyricsContainer.value) {return}
 | 
			
		||||
	if (!lyricsWrapper.value || !lyricsContainer.value) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userScrolling.value = true
 | 
			
		||||
	autoScroll.value = false
 | 
			
		||||
| 
						 | 
				
			
			@ -339,7 +374,7 @@ function handleWheel(event: WheelEvent) {
 | 
			
		|||
		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
 | 
			
		||||
 | 
			
		||||
	// 修正滚动范围计算
 | 
			
		||||
| 
						 | 
				
			
			@ -352,7 +387,7 @@ function handleWheel(event: WheelEvent) {
 | 
			
		|||
	gsap.to(lyricsWrapper.value, {
 | 
			
		||||
		y: limitedY,
 | 
			
		||||
		duration: 0.1,
 | 
			
		||||
		ease: "power2.out"
 | 
			
		||||
		ease: 'power2.out',
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if (userScrollTimeout) {
 | 
			
		||||
| 
						 | 
				
			
			@ -383,15 +418,16 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
 | 
			
		|||
 | 
			
		||||
	// 添加点击反馈动画
 | 
			
		||||
	if (lineRefs.value[index]) {
 | 
			
		||||
		gsap.fromTo(lineRefs.value[index],
 | 
			
		||||
		gsap.fromTo(
 | 
			
		||||
			lineRefs.value[index],
 | 
			
		||||
			{ scale: 1 },
 | 
			
		||||
			{
 | 
			
		||||
				scale: 1.1,
 | 
			
		||||
				duration: 0.1,
 | 
			
		||||
				yoyo: true,
 | 
			
		||||
				repeat: 1,
 | 
			
		||||
				ease: "power2.inOut"
 | 
			
		||||
			}
 | 
			
		||||
				ease: 'power2.inOut',
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -403,15 +439,16 @@ function toggleAutoScroll() {
 | 
			
		|||
 | 
			
		||||
	// 按钮点击动画
 | 
			
		||||
	if (controlPanel.value) {
 | 
			
		||||
		gsap.fromTo(controlPanel.value.children[0],
 | 
			
		||||
		gsap.fromTo(
 | 
			
		||||
			controlPanel.value.children[0],
 | 
			
		||||
			{ scale: 1 },
 | 
			
		||||
			{
 | 
			
		||||
				scale: 0.95,
 | 
			
		||||
				duration: 0.1,
 | 
			
		||||
				yoyo: true,
 | 
			
		||||
				repeat: 1,
 | 
			
		||||
				ease: "power2.inOut"
 | 
			
		||||
			}
 | 
			
		||||
				ease: 'power2.inOut',
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -424,17 +461,23 @@ function toggleAutoScroll() {
 | 
			
		|||
 | 
			
		||||
// 重置滚动
 | 
			
		||||
function resetScroll() {
 | 
			
		||||
	if (!lyricsWrapper.value) {return}
 | 
			
		||||
	if (!lyricsWrapper.value) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 停止所有动画
 | 
			
		||||
	if (scrollTween) {scrollTween.kill()}
 | 
			
		||||
	if (highlightTween) {highlightTween.kill()}
 | 
			
		||||
	if (scrollTween) {
 | 
			
		||||
		scrollTween.kill()
 | 
			
		||||
	}
 | 
			
		||||
	if (highlightTween) {
 | 
			
		||||
		highlightTween.kill()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 重置位置
 | 
			
		||||
	gsap.to(lyricsWrapper.value, {
 | 
			
		||||
		y: 0,
 | 
			
		||||
		duration: 0.3,
 | 
			
		||||
		ease: "power2.out"
 | 
			
		||||
		ease: 'power2.out',
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	autoScroll.value = true
 | 
			
		||||
| 
						 | 
				
			
			@ -442,15 +485,16 @@ function resetScroll() {
 | 
			
		|||
 | 
			
		||||
	// 按钮点击动画
 | 
			
		||||
	if (controlPanel.value) {
 | 
			
		||||
		gsap.fromTo(controlPanel.value.children[1],
 | 
			
		||||
		gsap.fromTo(
 | 
			
		||||
			controlPanel.value.children[1],
 | 
			
		||||
			{ scale: 1 },
 | 
			
		||||
			{
 | 
			
		||||
				scale: 0.95,
 | 
			
		||||
				duration: 0.1,
 | 
			
		||||
				yoyo: true,
 | 
			
		||||
				repeat: 1,
 | 
			
		||||
				ease: "power2.inOut"
 | 
			
		||||
			}
 | 
			
		||||
				ease: 'power2.inOut',
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -466,7 +510,9 @@ function resetScroll() {
 | 
			
		|||
function getGapDotOpacities(line: GapLine) {
 | 
			
		||||
	// 获取 gap 的持续时间
 | 
			
		||||
	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 = playStore.progress.currentTime
 | 
			
		||||
	// gap 起止时间
 | 
			
		||||
| 
						 | 
				
			
			@ -477,11 +523,15 @@ function getGapDotOpacities(line: GapLine) {
 | 
			
		|||
	// 每个圆点的阈值
 | 
			
		||||
	const thresholds = [1 / 4, 2 / 4, 3 / 4]
 | 
			
		||||
	// 透明度从 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(() => playStore.progress.currentTime, (time) => {
 | 
			
		||||
watch(
 | 
			
		||||
	() => playStore.progress.currentTime,
 | 
			
		||||
	(time) => {
 | 
			
		||||
		const newIndex = findCurrentLineIndex(time)
 | 
			
		||||
 | 
			
		||||
		if (newIndex !== currentLineIndex.value && newIndex >= 0) {
 | 
			
		||||
| 
						 | 
				
			
			@ -497,27 +547,35 @@ watch(() => playStore.progress.currentTime, (time) => {
 | 
			
		|||
				})
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
})
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 监听歌词源变化
 | 
			
		||||
watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		||||
watch(
 | 
			
		||||
	() => props.lrcSrc,
 | 
			
		||||
	async (newSrc) => {
 | 
			
		||||
		console.log('Loading new lyrics from:', newSrc)
 | 
			
		||||
		// 重置状态
 | 
			
		||||
		currentLineIndex.value = -1
 | 
			
		||||
		lineRefs.value = []
 | 
			
		||||
 | 
			
		||||
		// 停止所有动画
 | 
			
		||||
	if (scrollTween) {scrollTween.kill()}
 | 
			
		||||
	if (highlightTween) {highlightTween.kill()}
 | 
			
		||||
		if (scrollTween) {
 | 
			
		||||
			scrollTween.kill()
 | 
			
		||||
		}
 | 
			
		||||
		if (highlightTween) {
 | 
			
		||||
			highlightTween.kill()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (newSrc) {
 | 
			
		||||
			loading.value = true
 | 
			
		||||
 | 
			
		||||
			// 加载动画
 | 
			
		||||
			if (loadingIndicator.value) {
 | 
			
		||||
			gsap.fromTo(loadingIndicator.value,
 | 
			
		||||
				gsap.fromTo(
 | 
			
		||||
					loadingIndicator.value,
 | 
			
		||||
					{ 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)' },
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -533,7 +591,6 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		|||
				if (lyricsWrapper.value) {
 | 
			
		||||
					gsap.set(lyricsWrapper.value, { y: 0 })
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error('Failed to load lyrics:', error)
 | 
			
		||||
				parsedLyrics.value = []
 | 
			
		||||
| 
						 | 
				
			
			@ -548,8 +605,9 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		|||
				gsap.set(lyricsWrapper.value, { y: 0 })
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
}, { immediate: true })
 | 
			
		||||
 | 
			
		||||
	},
 | 
			
		||||
	{ immediate: true },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 页面焦点处理函数变量声明
 | 
			
		||||
let handleVisibilityChange: (() => void) | null = null
 | 
			
		||||
| 
						 | 
				
			
			@ -559,16 +617,28 @@ function setupPageFocusHandlers() {
 | 
			
		|||
	handleVisibilityChange = () => {
 | 
			
		||||
		if (document.hidden) {
 | 
			
		||||
			// 页面失去焦点时暂停动画
 | 
			
		||||
			if (scrollTween) {scrollTween.pause()}
 | 
			
		||||
			if (highlightTween) {highlightTween.pause()}
 | 
			
		||||
			if (scrollTween) {
 | 
			
		||||
				scrollTween.pause()
 | 
			
		||||
			}
 | 
			
		||||
			if (highlightTween) {
 | 
			
		||||
				highlightTween.pause()
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// 页面重新获得焦点时恢复并重新同步
 | 
			
		||||
			if (scrollTween?.paused()) {scrollTween.resume()}
 | 
			
		||||
			if (highlightTween?.paused()) {highlightTween.resume()}
 | 
			
		||||
			if (scrollTween?.paused()) {
 | 
			
		||||
				scrollTween.resume()
 | 
			
		||||
			}
 | 
			
		||||
			if (highlightTween?.paused()) {
 | 
			
		||||
				highlightTween.resume()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 重新同步歌词位置
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				if (currentLineIndex.value >= 0 && autoScroll.value && !userScrolling.value) {
 | 
			
		||||
				if (
 | 
			
		||||
					currentLineIndex.value >= 0 &&
 | 
			
		||||
					autoScroll.value &&
 | 
			
		||||
					!userScrolling.value
 | 
			
		||||
				) {
 | 
			
		||||
					scrollToLine(currentLineIndex.value, false) // 不使用动画,直接定位
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
| 
						 | 
				
			
			@ -585,9 +655,10 @@ onMounted(() => {
 | 
			
		|||
 | 
			
		||||
	// 控制面板入场动画
 | 
			
		||||
	if (controlPanel.value) {
 | 
			
		||||
		gsap.fromTo(controlPanel.value,
 | 
			
		||||
		gsap.fromTo(
 | 
			
		||||
			controlPanel.value,
 | 
			
		||||
			{ 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 },
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -595,15 +666,16 @@ onMounted(() => {
 | 
			
		|||
	nextTick(() => {
 | 
			
		||||
		lineRefs.value.forEach((el, index) => {
 | 
			
		||||
			if (el) {
 | 
			
		||||
				gsap.fromTo(el,
 | 
			
		||||
				gsap.fromTo(
 | 
			
		||||
					el,
 | 
			
		||||
					{ opacity: 0, y: 30 },
 | 
			
		||||
					{
 | 
			
		||||
						opacity: 1,
 | 
			
		||||
						y: 0,
 | 
			
		||||
						duration: 0.2,
 | 
			
		||||
						ease: "power2.out",
 | 
			
		||||
						delay: index * 0.1
 | 
			
		||||
					}
 | 
			
		||||
						ease: 'power2.out',
 | 
			
		||||
						delay: index * 0.1,
 | 
			
		||||
					},
 | 
			
		||||
				)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
| 
						 | 
				
			
			@ -612,9 +684,15 @@ onMounted(() => {
 | 
			
		|||
 | 
			
		||||
// 组件卸载时清理
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	if (scrollTween) {scrollTween.kill()}
 | 
			
		||||
	if (highlightTween) {highlightTween.kill()}
 | 
			
		||||
	if (userScrollTimeout) {clearTimeout(userScrollTimeout)}
 | 
			
		||||
	if (scrollTween) {
 | 
			
		||||
		scrollTween.kill()
 | 
			
		||||
	}
 | 
			
		||||
	if (highlightTween) {
 | 
			
		||||
		highlightTween.kill()
 | 
			
		||||
	}
 | 
			
		||||
	if (userScrollTimeout) {
 | 
			
		||||
		clearTimeout(userScrollTimeout)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 清理页面焦点事件监听器
 | 
			
		||||
	if (handleVisibilityChange) {
 | 
			
		||||
| 
						 | 
				
			
			@ -627,7 +705,10 @@ defineExpose({
 | 
			
		|||
	scrollToLine,
 | 
			
		||||
	toggleAutoScroll,
 | 
			
		||||
	resetScroll,
 | 
			
		||||
	getCurrentLine: () => currentLineIndex.value >= 0 ? parsedLyrics.value[currentLineIndex.value] : null
 | 
			
		||||
	getCurrentLine: () =>
 | 
			
		||||
		currentLineIndex.value >= 0
 | 
			
		||||
			? parsedLyrics.value[currentLineIndex.value]
 | 
			
		||||
			: null,
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,10 +9,10 @@ import StarEmptyIcon from '../assets/icons/starempty.vue'
 | 
			
		|||
import StarFilledIcon from '../assets/icons/starfilled.vue'
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	album?: Album,
 | 
			
		||||
	track: Song,
 | 
			
		||||
	index: number,
 | 
			
		||||
	playfrom: (index: number) => void,
 | 
			
		||||
	album?: Album
 | 
			
		||||
	track: Song
 | 
			
		||||
	index: number
 | 
			
		||||
	playfrom: (index: number) => void
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,15 +13,14 @@ import Library from './pages/Library.vue'
 | 
			
		|||
const routes = [
 | 
			
		||||
	{ path: '/', component: HomePage },
 | 
			
		||||
	{ path: '/playroom', component: Playroom },
 | 
			
		||||
  { path: '/library', component: Library }
 | 
			
		||||
	{ path: '/library', component: Library },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
	history: createWebHashHistory(),
 | 
			
		||||
  routes
 | 
			
		||||
	routes,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const pinia = createPinia()
 | 
			
		||||
 | 
			
		||||
createApp(App).use(router).use(pinia).use(ToastPlugin).mount('#app')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ import AlbumDetailDialog from '../components/AlbumDetailDialog.vue'
 | 
			
		|||
const albums = ref([] as AlbumList)
 | 
			
		||||
 | 
			
		||||
const presentAlbumDetailDialog = ref(false)
 | 
			
		||||
const presentedAlbum = ref("")
 | 
			
		||||
const presentedAlbum = ref('')
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	const res = await apis.getAlbums()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,19 +15,27 @@ const currentList = ref<'favourites' | number>('favourites')
 | 
			
		|||
 | 
			
		||||
function playTheList(list: 'favourites' | number, playFrom = 0) {
 | 
			
		||||
	let actualPlayFrom = playFrom
 | 
			
		||||
	if (playFrom < 0 || playFrom >= favourites.favouritesCount) { actualPlayFrom = 0 }
 | 
			
		||||
	if (playFrom < 0 || playFrom >= favourites.favouritesCount) {
 | 
			
		||||
		actualPlayFrom = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (playQueueStore.queueReplaceLock) {
 | 
			
		||||
		if (!confirm("当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?")) { return }
 | 
			
		||||
		if (
 | 
			
		||||
			!confirm(
 | 
			
		||||
				'当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?',
 | 
			
		||||
			)
 | 
			
		||||
		) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		playQueueStore.queueReplaceLock = false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (list === 'favourites') {
 | 
			
		||||
		if (favourites.favouritesCount === 0) return
 | 
			
		||||
 | 
			
		||||
		let newPlayQueue = favourites.favourites.map(item => ({
 | 
			
		||||
		let newPlayQueue = favourites.favourites.map((item) => ({
 | 
			
		||||
			song: item.song,
 | 
			
		||||
			album: item.album
 | 
			
		||||
			album: item.album,
 | 
			
		||||
		}))
 | 
			
		||||
		// playQueueStore.list = newPlayQueue.slice().reverse()
 | 
			
		||||
		// playQueueStore.currentIndex = playFrom
 | 
			
		||||
| 
						 | 
				
			
			@ -49,7 +57,6 @@ function shuffle(list: 'favourites' | number) {
 | 
			
		|||
	// 	playQueueStore.isBuffering = true
 | 
			
		||||
	// }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,7 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { usePlayStore } from '../stores/usePlayStore'
 | 
			
		||||
import { artistsOrganize } from '../utils'
 | 
			
		||||
import gsap from 'gsap'
 | 
			
		||||
import { Draggable } from "gsap/Draggable"
 | 
			
		||||
import { Draggable } from 'gsap/Draggable'
 | 
			
		||||
import { onMounted, onUnmounted, nextTick } from 'vue'
 | 
			
		||||
import { useTemplateRef } from 'vue'
 | 
			
		||||
import { ref, watch } from 'vue'
 | 
			
		||||
| 
						 | 
				
			
			@ -68,9 +67,10 @@ onMounted(async () => {
 | 
			
		|||
		onDrag: function () {
 | 
			
		||||
			const thumbPosition = this.x
 | 
			
		||||
			const containerWidth = progressBarContainer.value?.clientWidth || 0
 | 
			
		||||
			const newTime = (thumbPosition / containerWidth) * playStore.progress.duration
 | 
			
		||||
			const newTime =
 | 
			
		||||
				(thumbPosition / containerWidth) * playStore.progress.duration
 | 
			
		||||
			playStore.updateCurrentTime(newTime)
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 等待DOM完全渲染后再初始化拖拽
 | 
			
		||||
| 
						 | 
				
			
			@ -93,23 +93,30 @@ onMounted(async () => {
 | 
			
		|||
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
			
		||||
function timeFormatter(time: number) {
 | 
			
		||||
	const timeInSeconds = Math.floor(time)
 | 
			
		||||
	if (timeInSeconds < 0) { return '-:--' }
 | 
			
		||||
	if (timeInSeconds < 0) {
 | 
			
		||||
		return '-:--'
 | 
			
		||||
	}
 | 
			
		||||
	const minutes = 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}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听播放进度,更新进度条
 | 
			
		||||
watch(() => playStore.progress.currentTime, () => {
 | 
			
		||||
watch(
 | 
			
		||||
	() => playStore.progress.currentTime,
 | 
			
		||||
	() => {
 | 
			
		||||
		thumbUpdate()
 | 
			
		||||
})
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function thumbUpdate() {
 | 
			
		||||
	const progress = playStore.progress.percentage
 | 
			
		||||
	const containerWidth = progressBarContainer.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 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -156,7 +163,10 @@ function createVolumeDraggable() {
 | 
			
		|||
			const containerWidth = volumeSliderContainer.value?.clientWidth || 0
 | 
			
		||||
			const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
 | 
			
		||||
			// 确保音量值在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
 | 
			
		||||
			updateAudioVolume()
 | 
			
		||||
			// 保存音量到localStorage
 | 
			
		||||
| 
						 | 
				
			
			@ -165,7 +175,7 @@ function createVolumeDraggable() {
 | 
			
		|||
		onDragEnd: () => {
 | 
			
		||||
			// 拖拽结束时也保存一次
 | 
			
		||||
			localStorage.setItem('audioVolume', volume.value.toString())
 | 
			
		||||
		}
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	console.log('Volume draggable created successfully')
 | 
			
		||||
| 
						 | 
				
			
			@ -182,58 +192,64 @@ function updateAudioVolume() {
 | 
			
		|||
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
 | 
			
		||||
function formatDetector() {
 | 
			
		||||
	const format = playStore.currentTrack?.url?.split('.').pop()
 | 
			
		||||
	if (format === 'mp3') { return 'MP3' }
 | 
			
		||||
	if (format === 'flac') { return 'FLAC' }
 | 
			
		||||
	if (format === 'm4a') { return 'M4A' }
 | 
			
		||||
	if (format === 'ape') { return 'APE' }
 | 
			
		||||
	if (format === 'wav') { return 'WAV' }
 | 
			
		||||
	if (format === 'mp3') {
 | 
			
		||||
		return 'MP3'
 | 
			
		||||
	}
 | 
			
		||||
	if (format === 'flac') {
 | 
			
		||||
		return 'FLAC'
 | 
			
		||||
	}
 | 
			
		||||
	if (format === 'm4a') {
 | 
			
		||||
		return 'M4A'
 | 
			
		||||
	}
 | 
			
		||||
	if (format === 'ape') {
 | 
			
		||||
		return 'APE'
 | 
			
		||||
	}
 | 
			
		||||
	if (format === 'wav') {
 | 
			
		||||
		return 'WAV'
 | 
			
		||||
	}
 | 
			
		||||
	return '未知格式'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setupEntranceAnimations() {
 | 
			
		||||
	if (controllerRef.value) {
 | 
			
		||||
		gsap.fromTo(controllerRef.value.children,
 | 
			
		||||
		gsap.fromTo(
 | 
			
		||||
			controllerRef.value.children,
 | 
			
		||||
			{ opacity: 0, y: 30, scale: 0.95 },
 | 
			
		||||
			{
 | 
			
		||||
				opacity: 1, y: 0, scale: 1,
 | 
			
		||||
				duration: 0.6, ease: "power2.out", stagger: 0.1
 | 
			
		||||
			}
 | 
			
		||||
				opacity: 1,
 | 
			
		||||
				y: 0,
 | 
			
		||||
				scale: 1,
 | 
			
		||||
				duration: 0.6,
 | 
			
		||||
				ease: 'power2.out',
 | 
			
		||||
				stagger: 0.1,
 | 
			
		||||
			},
 | 
			
		||||
		)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (lyricsSection.value) {
 | 
			
		||||
		gsap.fromTo(lyricsSection.value,
 | 
			
		||||
		gsap.fromTo(
 | 
			
		||||
			lyricsSection.value,
 | 
			
		||||
			{ 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() {
 | 
			
		||||
	if (playButton.value) {
 | 
			
		||||
		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
 | 
			
		||||
	}
 | 
			
		||||
	playStore.togglePlay()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleShuffle() {
 | 
			
		||||
	playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
 | 
			
		||||
	playQueueStore.shuffleCurrent = false
 | 
			
		||||
	// playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
 | 
			
		||||
	// playQueueStore.shuffleCurrent = false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleRepeat() {
 | 
			
		||||
	switch (playQueueStore.playMode.repeat) {
 | 
			
		||||
		case 'off': playQueueStore.playMode.repeat = 'all'; break
 | 
			
		||||
		case 'all': playQueueStore.playMode.repeat = 'single'; break
 | 
			
		||||
		case 'single': playQueueStore.playMode.repeat = 'off'; break
 | 
			
		||||
	}
 | 
			
		||||
	// switch (playQueueStore.playMode.repeat) {
 | 
			
		||||
	// 	case 'off': playQueueStore.playMode.repeat = 'all'; break
 | 
			
		||||
	// 	case 'all': playQueueStore.playMode.repeat = 'single'; break
 | 
			
		||||
	// 	case 'single': playQueueStore.playMode.repeat = 'off'; break
 | 
			
		||||
	// }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function makePlayQueueListPresent() {
 | 
			
		||||
| 
						 | 
				
			
			@ -244,15 +260,26 @@ function makePlayQueueListPresent() {
 | 
			
		|||
 | 
			
		||||
		const tl = gsap.timeline()
 | 
			
		||||
		tl.to(playQueueDialogContainer.value, {
 | 
			
		||||
			backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
 | 
			
		||||
		}).to(playQueueDialog.value, {
 | 
			
		||||
			x: 0, duration: 0.4, ease: 'power3.out'
 | 
			
		||||
		}, '<0.1')
 | 
			
		||||
			backgroundColor: '#17171780',
 | 
			
		||||
			duration: 0.3,
 | 
			
		||||
			ease: 'power2.out',
 | 
			
		||||
		}).to(
 | 
			
		||||
			playQueueDialog.value,
 | 
			
		||||
			{
 | 
			
		||||
				x: 0,
 | 
			
		||||
				duration: 0.4,
 | 
			
		||||
				ease: 'power3.out',
 | 
			
		||||
			},
 | 
			
		||||
			'<0.1',
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		if (playQueueDialog.value.children.length > 0) {
 | 
			
		||||
			tl.fromTo(playQueueDialog.value.children,
 | 
			
		||||
			tl.fromTo(
 | 
			
		||||
				playQueueDialog.value.children,
 | 
			
		||||
				{ 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',
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -270,23 +297,40 @@ function makePlayQueueListDismiss() {
 | 
			
		|||
				gsap.set(playQueueDialog.value, { x: -384 })
 | 
			
		||||
			}
 | 
			
		||||
			if (playQueueDialogContainer.value) {
 | 
			
		||||
				gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
 | 
			
		||||
			}
 | 
			
		||||
				gsap.set(playQueueDialogContainer.value, {
 | 
			
		||||
					backgroundColor: 'transparent',
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if (playQueueDialog.value.children.length > 0) {
 | 
			
		||||
		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, {
 | 
			
		||||
		x: -384, 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'
 | 
			
		||||
		}, '<')
 | 
			
		||||
	tl.to(
 | 
			
		||||
		playQueueDialog.value,
 | 
			
		||||
		{
 | 
			
		||||
			x: -384,
 | 
			
		||||
			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() {
 | 
			
		||||
| 
						 | 
				
			
			@ -299,15 +343,23 @@ function toggleMoreOptions() {
 | 
			
		|||
		nextTick(() => {
 | 
			
		||||
			if (moreOptionsDialog.value) {
 | 
			
		||||
				const tl = gsap.timeline()
 | 
			
		||||
				tl.fromTo(moreOptionsDialog.value,
 | 
			
		||||
				tl.fromTo(
 | 
			
		||||
					moreOptionsDialog.value,
 | 
			
		||||
					{ 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) {
 | 
			
		||||
					tl.fromTo(moreOptionsDialog.value.children[0].children,
 | 
			
		||||
					tl.fromTo(
 | 
			
		||||
						moreOptionsDialog.value.children[0].children,
 | 
			
		||||
						{ 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',
 | 
			
		||||
					)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -317,16 +369,21 @@ function toggleMoreOptions() {
 | 
			
		|||
			const tl = gsap.timeline({
 | 
			
		||||
				onComplete: () => {
 | 
			
		||||
					showMoreOptions.value = false
 | 
			
		||||
				}
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
			if (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 }
 | 
			
		||||
				)
 | 
			
		||||
				tl.to(moreOptionsDialog.value.children[0].children, {
 | 
			
		||||
					opacity: 0,
 | 
			
		||||
					x: -10,
 | 
			
		||||
					duration: 0.1,
 | 
			
		||||
					ease: 'power2.in',
 | 
			
		||||
					stagger: 0.02,
 | 
			
		||||
				})
 | 
			
		||||
			}
 | 
			
		||||
			tl.to(moreOptionsDialog.value,
 | 
			
		||||
				{ opacity: 0, scale: 0.9, y: 10, duration: 0.15, ease: "power2.in" },
 | 
			
		||||
				moreOptionsDialog.value.children[0]?.children ? "<0.05" : "0"
 | 
			
		||||
			tl.to(
 | 
			
		||||
				moreOptionsDialog.value,
 | 
			
		||||
				{ opacity: 0, scale: 0.9, y: 10, duration: 0.15, ease: 'power2.in' },
 | 
			
		||||
				moreOptionsDialog.value.children[0]?.children ? '<0.05' : '0',
 | 
			
		||||
			)
 | 
			
		||||
		} else {
 | 
			
		||||
			showMoreOptions.value = false
 | 
			
		||||
| 
						 | 
				
			
			@ -335,71 +392,96 @@ function toggleMoreOptions() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// TODO: lyrics
 | 
			
		||||
// watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newValue, oldValue) => {
 | 
			
		||||
// 	if (!getCurrentTrack()) { return }
 | 
			
		||||
watch(
 | 
			
		||||
	() => [preferences.presentLyrics, getCurrentTrack()?.extra?.lyric],
 | 
			
		||||
	(newValue, oldValue) => {
 | 
			
		||||
		if (!getCurrentTrack()) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
// 	const [showLyrics, hasLyricUrl] = newValue
 | 
			
		||||
// 	const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
 | 
			
		||||
		const [showLyrics, hasLyricUrl] = newValue
 | 
			
		||||
		const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
 | 
			
		||||
 | 
			
		||||
// 	// Show lyrics when both conditions are met
 | 
			
		||||
// 	if (showLyrics && hasLyricUrl) {
 | 
			
		||||
// 		presentLyrics.value = true
 | 
			
		||||
// 		nextTick(() => {
 | 
			
		||||
// 			const tl = gsap.timeline()
 | 
			
		||||
// 			tl.from(controllerRef.value, {
 | 
			
		||||
// 				marginRight: '-40rem',
 | 
			
		||||
// 			}).fromTo(lyricsSection.value,
 | 
			
		||||
// 				{ opacity: 0, x: 50, scale: 0.95 },
 | 
			
		||||
// 				{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
 | 
			
		||||
// 				"-=0.3"
 | 
			
		||||
// 			)
 | 
			
		||||
// 		})
 | 
			
		||||
// 	}
 | 
			
		||||
// 	// Hide lyrics with different animations based on reason
 | 
			
		||||
// 	else if (presentLyrics.value) {
 | 
			
		||||
// 		let animationConfig
 | 
			
		||||
		// Show lyrics when both conditions are met
 | 
			
		||||
		if (showLyrics && hasLyricUrl) {
 | 
			
		||||
			presentLyrics.value = true
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				const tl = gsap.timeline()
 | 
			
		||||
				tl.from(controllerRef.value, {
 | 
			
		||||
					marginRight: '-40rem',
 | 
			
		||||
				}).fromTo(
 | 
			
		||||
					lyricsSection.value,
 | 
			
		||||
					{ opacity: 0, x: 50, scale: 0.95 },
 | 
			
		||||
					{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: 'power2.out' },
 | 
			
		||||
					'-=0.3',
 | 
			
		||||
				)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
		// Hide lyrics with different animations based on reason
 | 
			
		||||
		else if (presentLyrics.value) {
 | 
			
		||||
			let animationConfig: {
 | 
			
		||||
				opacity: number
 | 
			
		||||
				x: number
 | 
			
		||||
				scale?: number
 | 
			
		||||
				duration: number
 | 
			
		||||
				ease: string
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
// 		// If lyrics were toggled off
 | 
			
		||||
// 		if (prevShowLyrics && !showLyrics) {
 | 
			
		||||
// 			animationConfig = {
 | 
			
		||||
// 				opacity: 0, x: -50, scale: 0.95,
 | 
			
		||||
// 				duration: 0.3, ease: "power2.in"
 | 
			
		||||
// 			}
 | 
			
		||||
// 		}
 | 
			
		||||
// 		// If no lyrics available (song changed)
 | 
			
		||||
// 		else if (!hasLyricUrl) {
 | 
			
		||||
// 			animationConfig = {
 | 
			
		||||
// 				opacity: 0, y: -20, scale: 0.98,
 | 
			
		||||
// 				duration: 0.3, ease: "power1.in"
 | 
			
		||||
// 			}
 | 
			
		||||
// 		}
 | 
			
		||||
// 		// Default animation
 | 
			
		||||
// 		else {
 | 
			
		||||
// 			animationConfig = {
 | 
			
		||||
// 				opacity: 0, x: -50,
 | 
			
		||||
// 				duration: 0.3, ease: "power2.in"
 | 
			
		||||
// 			}
 | 
			
		||||
// 		}
 | 
			
		||||
			// If lyrics were toggled off
 | 
			
		||||
			if (prevShowLyrics && !showLyrics) {
 | 
			
		||||
				animationConfig = {
 | 
			
		||||
					opacity: 0,
 | 
			
		||||
					x: -50,
 | 
			
		||||
					scale: 0.95,
 | 
			
		||||
					duration: 0.3,
 | 
			
		||||
					ease: 'power2.in',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// If no lyrics available (song changed)
 | 
			
		||||
			else if (!hasLyricUrl) {
 | 
			
		||||
				animationConfig = {
 | 
			
		||||
					opacity: 0,
 | 
			
		||||
					y: -20,
 | 
			
		||||
					scale: 0.98,
 | 
			
		||||
					duration: 0.3,
 | 
			
		||||
					ease: 'power1.in',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// Default animation
 | 
			
		||||
			else {
 | 
			
		||||
				animationConfig = {
 | 
			
		||||
					opacity: 0,
 | 
			
		||||
					x: -50,
 | 
			
		||||
					duration: 0.3,
 | 
			
		||||
					ease: 'power2.in',
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
// 		const tl = gsap.timeline({
 | 
			
		||||
// 			onComplete: () => {
 | 
			
		||||
// 				presentLyrics.value = false
 | 
			
		||||
// 			}
 | 
			
		||||
// 		})
 | 
			
		||||
			const tl = gsap.timeline({
 | 
			
		||||
				onComplete: () => {
 | 
			
		||||
					presentLyrics.value = false
 | 
			
		||||
				},
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
// 		tl.to(controllerRef.value, {
 | 
			
		||||
// 			marginLeft: '44rem',
 | 
			
		||||
// 			duration: 0.3, ease: "power2.out"
 | 
			
		||||
// 		})
 | 
			
		||||
// 			.to(lyricsSection.value, animationConfig, '<')
 | 
			
		||||
// 			.set(lyricsSection.value, {
 | 
			
		||||
// 				opacity: 1, x: 0, y: 0, scale: 1 // Reset for next time
 | 
			
		||||
// 			})
 | 
			
		||||
// 			.set(controllerRef.value, {
 | 
			
		||||
// 				marginLeft: '0rem' // Reset for next time
 | 
			
		||||
// 			})
 | 
			
		||||
// 	}
 | 
			
		||||
// }, { immediate: true })
 | 
			
		||||
			tl.to(controllerRef.value, {
 | 
			
		||||
				marginLeft: '44rem',
 | 
			
		||||
				duration: 0.3,
 | 
			
		||||
				ease: 'power2.out',
 | 
			
		||||
			})
 | 
			
		||||
				.to(lyricsSection.value, animationConfig, '<')
 | 
			
		||||
				.set(lyricsSection.value, {
 | 
			
		||||
					opacity: 1,
 | 
			
		||||
					x: 0,
 | 
			
		||||
					y: 0,
 | 
			
		||||
					scale: 1, // Reset for next time
 | 
			
		||||
				})
 | 
			
		||||
				.set(controllerRef.value, {
 | 
			
		||||
					marginLeft: '0rem', // Reset for next time
 | 
			
		||||
				})
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{ immediate: true },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 页面焦点处理函数变量声明
 | 
			
		||||
let handleVisibilityChange: (() => void) | null = null
 | 
			
		||||
| 
						 | 
				
			
			@ -445,7 +527,9 @@ function setupPageFocusHandlers() {
 | 
			
		|||
// 重新同步歌词状态
 | 
			
		||||
function resyncLyricsState() {
 | 
			
		||||
	const currentTrack = getCurrentTrack()
 | 
			
		||||
	if (!currentTrack) { return }
 | 
			
		||||
	if (!currentTrack) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	console.log('[Playroom] 重新同步歌词状态')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -453,7 +537,7 @@ function resyncLyricsState() {
 | 
			
		|||
	if (controllerRef.value) {
 | 
			
		||||
		gsap.set(controllerRef.value, {
 | 
			
		||||
			marginLeft: '0rem',
 | 
			
		||||
			marginRight: '0rem' 
 | 
			
		||||
			marginRight: '0rem',
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -462,15 +546,19 @@ function resyncLyricsState() {
 | 
			
		|||
			opacity: 1,
 | 
			
		||||
			x: 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) {
 | 
			
		||||
		console.log(`[Playroom] 歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`)
 | 
			
		||||
		console.log(
 | 
			
		||||
			`[Playroom] 歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		// 直接设置状态,不触发动画
 | 
			
		||||
		presentLyrics.value = shouldShowLyrics
 | 
			
		||||
| 
						 | 
				
			
			@ -482,11 +570,12 @@ function resyncLyricsState() {
 | 
			
		|||
				tl.from(controllerRef.value, {
 | 
			
		||||
					marginRight: '-40rem',
 | 
			
		||||
					duration: 0.4,
 | 
			
		||||
					ease: "power2.out"
 | 
			
		||||
				}).fromTo(lyricsSection.value,
 | 
			
		||||
					ease: 'power2.out',
 | 
			
		||||
				}).fromTo(
 | 
			
		||||
					lyricsSection.value,
 | 
			
		||||
					{ opacity: 0, x: 50, scale: 0.95 },
 | 
			
		||||
					{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
 | 
			
		||||
					"-=0.2"
 | 
			
		||||
					{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: 'power2.out' },
 | 
			
		||||
					'-=0.2',
 | 
			
		||||
				)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -494,31 +583,39 @@ function resyncLyricsState() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// New: Watch for track changes and animate
 | 
			
		||||
watch(() => playStore.currentTrack, () => {
 | 
			
		||||
watch(
 | 
			
		||||
	() => playStore.currentTrack,
 | 
			
		||||
	() => {
 | 
			
		||||
		if (albumCover.value) {
 | 
			
		||||
			gsap.to(albumCover.value, {
 | 
			
		||||
			scale: 0.95, opacity: 0.7, duration: 0.2,
 | 
			
		||||
			ease: "power2.inOut", yoyo: true, repeat: 1
 | 
			
		||||
				scale: 0.95,
 | 
			
		||||
				opacity: 0.7,
 | 
			
		||||
				duration: 0.2,
 | 
			
		||||
				ease: 'power2.inOut',
 | 
			
		||||
				yoyo: true,
 | 
			
		||||
				repeat: 1,
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (songInfo.value) {
 | 
			
		||||
		gsap.fromTo(songInfo.value,
 | 
			
		||||
			gsap.fromTo(
 | 
			
		||||
				songInfo.value,
 | 
			
		||||
				{ 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>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<div v-if="getCurrentTrack() !== null">
 | 
			
		||||
		<!-- Background remains unchanged -->
 | 
			
		||||
		<!-- <div class="z-0 absolute top-0 left-0 w-screen h-screen overflow-hidden"
 | 
			
		||||
			v-if="getCurrentTrack()?.album?.coverDeUrl">
 | 
			
		||||
			<img class="w-full h-full blur-2xl object-cover scale-110" :src="getCurrentTrack()?.album?.coverDeUrl" />
 | 
			
		||||
		<div class="z-0 absolute top-0 left-0 w-screen h-screen overflow-hidden"
 | 
			
		||||
			v-if="getCurrentTrack()?.extra?.background">
 | 
			
		||||
			<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> -->
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<!-- Main content area - new centered flex layout -->
 | 
			
		||||
		<div class="absolute top-0 left-0 flex justify-center h-screen w-screen overflow-y-auto z-10 select-none">
 | 
			
		||||
| 
						 | 
				
			
			@ -727,7 +824,7 @@ watch(() => playStore.currentTrack, () => {
 | 
			
		|||
								</button>
 | 
			
		||||
								<!-- Show tooltip only on hover, with transition -->
 | 
			
		||||
								<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]">
 | 
			
		||||
										<div class="font-semibold text-white">这首曲目不提供歌词文本</div>
 | 
			
		||||
										<div class="text-white/60">启用歌词时,将会在下一首有歌词的曲目中显示歌词文本。</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -781,7 +878,7 @@ watch(() => playStore.currentTrack, () => {
 | 
			
		|||
 | 
			
		||||
				<!-- Lyrics section - full screen height -->
 | 
			
		||||
				<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" />
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { defineStore } from "pinia"
 | 
			
		||||
import { ref, watch, computed } from "vue"
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import { ref, watch, computed } from 'vue'
 | 
			
		||||
 | 
			
		||||
// 声明全局类型
 | 
			
		||||
declare global {
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +21,11 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
	const detectAvailableAPIs = () => {
 | 
			
		||||
		// 检查原生 chrome API
 | 
			
		||||
		try {
 | 
			
		||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
 | 
			
		||||
			if (
 | 
			
		||||
				typeof chrome !== 'undefined' &&
 | 
			
		||||
				chrome.storage &&
 | 
			
		||||
				chrome.storage.local
 | 
			
		||||
			) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -31,7 +35,11 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
 | 
			
		||||
		// 检查 window.chrome
 | 
			
		||||
		try {
 | 
			
		||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.local) {
 | 
			
		||||
			if (
 | 
			
		||||
				window.chrome &&
 | 
			
		||||
				window.chrome.storage &&
 | 
			
		||||
				window.chrome.storage.local
 | 
			
		||||
			) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +139,8 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
	const normalizeFavourites = (data: any[]): InternalQueueItem[] => {
 | 
			
		||||
		if (!Array.isArray(data)) return []
 | 
			
		||||
 | 
			
		||||
		return data.map(item => {
 | 
			
		||||
		return data
 | 
			
		||||
			.map((item) => {
 | 
			
		||||
				if (!item || !item.song) return null
 | 
			
		||||
 | 
			
		||||
				// 规范化 Song 对象
 | 
			
		||||
| 
						 | 
				
			
			@ -144,37 +153,48 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
					mvUrl: item.song.mvUrl,
 | 
			
		||||
					mvCoverUrl: item.song.mvCoverUrl,
 | 
			
		||||
					// 确保 artistes 和 artists 是数组
 | 
			
		||||
				artistes: Array.isArray(item.song.artistes) ? item.song.artistes :
 | 
			
		||||
					typeof item.song.artistes === 'object' ? Object.values(item.song.artistes) :
 | 
			
		||||
						[],
 | 
			
		||||
				artists: Array.isArray(item.song.artists) ? item.song.artists :
 | 
			
		||||
					typeof item.song.artists === 'object' ? Object.values(item.song.artists) :
 | 
			
		||||
						[]
 | 
			
		||||
					artistes: Array.isArray(item.song.artistes)
 | 
			
		||||
						? item.song.artistes
 | 
			
		||||
						: typeof item.song.artistes === 'object'
 | 
			
		||||
							? Object.values(item.song.artistes)
 | 
			
		||||
							: [],
 | 
			
		||||
					artists: Array.isArray(item.song.artists)
 | 
			
		||||
						? item.song.artists
 | 
			
		||||
						: typeof item.song.artists === 'object'
 | 
			
		||||
							? Object.values(item.song.artists)
 | 
			
		||||
							: [],
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 规范化 Album 对象(如果存在)
 | 
			
		||||
			const album = item.album ? {
 | 
			
		||||
				const album = item.album
 | 
			
		||||
					? {
 | 
			
		||||
							cid: item.album.cid || '',
 | 
			
		||||
							name: item.album.name || '',
 | 
			
		||||
							intro: item.album.intro,
 | 
			
		||||
							belong: item.album.belong,
 | 
			
		||||
							coverUrl: item.album.coverUrl || '',
 | 
			
		||||
							coverDeUrl: item.album.coverDeUrl,
 | 
			
		||||
				artistes: Array.isArray(item.album.artistes) ? item.album.artistes :
 | 
			
		||||
					typeof item.album.artistes === 'object' ? Object.values(item.album.artistes) :
 | 
			
		||||
						[],
 | 
			
		||||
				songs: item.album.songs
 | 
			
		||||
			} : undefined
 | 
			
		||||
							artistes: Array.isArray(item.album.artistes)
 | 
			
		||||
								? item.album.artistes
 | 
			
		||||
								: typeof item.album.artistes === 'object'
 | 
			
		||||
									? Object.values(item.album.artistes)
 | 
			
		||||
									: [],
 | 
			
		||||
							songs: item.album.songs,
 | 
			
		||||
						}
 | 
			
		||||
					: undefined
 | 
			
		||||
 | 
			
		||||
				return { song, album }
 | 
			
		||||
		}).filter(Boolean) as InternalQueueItem[]
 | 
			
		||||
			})
 | 
			
		||||
			.filter(Boolean) as InternalQueueItem[]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 获取收藏列表
 | 
			
		||||
	const getFavourites = async () => {
 | 
			
		||||
		const result = await getStoredValue('favourites', defaultFavourites)
 | 
			
		||||
		// 确保返回的是数组并进行数据规范化
 | 
			
		||||
		const normalizedResult = Array.isArray(result) ? normalizeFavourites(result) : defaultFavourites
 | 
			
		||||
		const normalizedResult = Array.isArray(result)
 | 
			
		||||
			? normalizeFavourites(result)
 | 
			
		||||
			: defaultFavourites
 | 
			
		||||
		return normalizedResult
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -187,7 +207,7 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
 | 
			
		||||
	// 检查歌曲是否已收藏
 | 
			
		||||
	const isFavourite = (songCid: string): boolean => {
 | 
			
		||||
		return favourites.value.some(item => item.song.cid === songCid)
 | 
			
		||||
		return favourites.value.some((item) => item.song.cid === songCid)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 添加到收藏
 | 
			
		||||
| 
						 | 
				
			
			@ -208,7 +228,9 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
 | 
			
		||||
	// 从收藏中移除
 | 
			
		||||
	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) {
 | 
			
		||||
			const removedItem = favourites.value.splice(index, 1)[0]
 | 
			
		||||
			if (isLoaded.value) {
 | 
			
		||||
| 
						 | 
				
			
			@ -265,7 +287,9 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
 | 
			
		||||
	// 监听变化并保存(防抖处理)
 | 
			
		||||
	let saveTimeout: NodeJS.Timeout | null = null
 | 
			
		||||
	watch(favourites, async () => {
 | 
			
		||||
	watch(
 | 
			
		||||
		favourites,
 | 
			
		||||
		async () => {
 | 
			
		||||
			if (isLoaded.value) {
 | 
			
		||||
				// 清除之前的定时器
 | 
			
		||||
				if (saveTimeout) {
 | 
			
		||||
| 
						 | 
				
			
			@ -280,14 +304,21 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
					}
 | 
			
		||||
				}, 300)
 | 
			
		||||
			}
 | 
			
		||||
	}, { deep: true })
 | 
			
		||||
		},
 | 
			
		||||
		{ deep: true },
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// 更新收藏列表中的歌曲信息
 | 
			
		||||
	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) {
 | 
			
		||||
			// 更新歌曲信息,保持其他属性不变
 | 
			
		||||
			favourites.value[index].song = { ...favourites.value[index].song, ...updatedSong }
 | 
			
		||||
			favourites.value[index].song = {
 | 
			
		||||
				...favourites.value[index].song,
 | 
			
		||||
				...updatedSong,
 | 
			
		||||
			}
 | 
			
		||||
			if (isLoaded.value) {
 | 
			
		||||
				try {
 | 
			
		||||
					await saveFavourites()
 | 
			
		||||
| 
						 | 
				
			
			@ -317,7 +348,6 @@ export const useFavourites = defineStore('favourites', () => {
 | 
			
		|||
		clearFavourites,
 | 
			
		||||
		getStoredValue,
 | 
			
		||||
		setStoredValue,
 | 
			
		||||
		updateSongInFavourites
 | 
			
		||||
		updateSongInFavourites,
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,31 +12,43 @@ export const usePlayStore = defineStore('player', () => {
 | 
			
		|||
	const progress = ref({
 | 
			
		||||
		currentTime: 0,
 | 
			
		||||
		duration: 0,
 | 
			
		||||
		percentage: 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 loop = ref<'off' | 'entire_queue' | 'single_track'>('off')
 | 
			
		||||
 | 
			
		||||
	const replaceQueue = (queue: {
 | 
			
		||||
	const replaceQueue = (
 | 
			
		||||
		queue: {
 | 
			
		||||
			song: Song
 | 
			
		||||
			album: Album | undefined
 | 
			
		||||
	}[]) => {
 | 
			
		||||
		}[],
 | 
			
		||||
	) => {
 | 
			
		||||
		const newQueue = []
 | 
			
		||||
		for (const item of queue) {
 | 
			
		||||
			newQueue.push({
 | 
			
		||||
				url: item.song.sourceUrl ?? "",
 | 
			
		||||
				url: item.song.sourceUrl ?? '',
 | 
			
		||||
				metadata: {
 | 
			
		||||
					title: item.song.name,
 | 
			
		||||
					artist: artistsOrganize(item.song.artists ?? item.song.artistes ?? []),
 | 
			
		||||
					artwork: [{
 | 
			
		||||
						src: item.album?.coverUrl ?? "",
 | 
			
		||||
						sizes: "500x500",
 | 
			
		||||
						type: ((item.album?.coverUrl ?? "").split(".").at(-1) === "jpg" ? 'image/jpeg' : 'image/png') as "image/jpeg" | "image/png"
 | 
			
		||||
					}]
 | 
			
		||||
				}
 | 
			
		||||
					artist: artistsOrganize(
 | 
			
		||||
						item.song.artists ?? item.song.artistes ?? [],
 | 
			
		||||
					),
 | 
			
		||||
					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)
 | 
			
		||||
| 
						 | 
				
			
			@ -51,40 +63,48 @@ export const usePlayStore = defineStore('player', () => {
 | 
			
		|||
		album: Album | undefined
 | 
			
		||||
	}) => {
 | 
			
		||||
		player.value.appendTrack({
 | 
			
		||||
			url: item.song.sourceUrl ?? "",
 | 
			
		||||
			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"
 | 
			
		||||
				}]
 | 
			
		||||
			}
 | 
			
		||||
				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 => {
 | 
			
		||||
	player.value.onProgressChange((params) => {
 | 
			
		||||
		progress.value = params
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	player.value.onCurrentPlayingChange(params => {
 | 
			
		||||
	player.value.onCurrentPlayingChange((params) => {
 | 
			
		||||
		currentTrack.value = params
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	player.value.onPlayStateChange(params => {
 | 
			
		||||
	player.value.onPlayStateChange((params) => {
 | 
			
		||||
		isPlaying.value = params
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	player.value.onQueueChange(params => {
 | 
			
		||||
	player.value.onQueueChange((params) => {
 | 
			
		||||
		queue.value = params
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	player.value.onShuffleChange(params => {
 | 
			
		||||
	player.value.onShuffleChange((params) => {
 | 
			
		||||
		shuffle.value = params
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	player.value.onLoopChange(params => {
 | 
			
		||||
	player.value.onLoopChange((params) => {
 | 
			
		||||
		loop.value = params
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -109,6 +129,6 @@ export const usePlayStore = defineStore('player', () => {
 | 
			
		|||
		queue,
 | 
			
		||||
		shuffle,
 | 
			
		||||
		loop,
 | 
			
		||||
		replaceQueue
 | 
			
		||||
		replaceQueue,
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { defineStore } from "pinia"
 | 
			
		||||
import { ref, watch } from "vue"
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import { ref, watch } from 'vue'
 | 
			
		||||
 | 
			
		||||
// 声明全局类型
 | 
			
		||||
declare global {
 | 
			
		||||
| 
						 | 
				
			
			@ -20,14 +20,18 @@ export const usePreferences = defineStore('preferences', () => {
 | 
			
		|||
	const defaultPreferences = {
 | 
			
		||||
		displayTimeLeft: false,
 | 
			
		||||
		presentLyrics: false,
 | 
			
		||||
		autoRedirect: true
 | 
			
		||||
		autoRedirect: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检测可用的 API
 | 
			
		||||
	const detectAvailableAPIs = () => {
 | 
			
		||||
		// 检查原生 chrome API
 | 
			
		||||
		try {
 | 
			
		||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
 | 
			
		||||
			if (
 | 
			
		||||
				typeof chrome !== 'undefined' &&
 | 
			
		||||
				chrome.storage &&
 | 
			
		||||
				chrome.storage.sync
 | 
			
		||||
			) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -37,7 +41,11 @@ export const usePreferences = defineStore('preferences', () => {
 | 
			
		|||
 | 
			
		||||
		// 检查 window.chrome
 | 
			
		||||
		try {
 | 
			
		||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
 | 
			
		||||
			if (
 | 
			
		||||
				window.chrome &&
 | 
			
		||||
				window.chrome.storage &&
 | 
			
		||||
				window.chrome.storage.sync
 | 
			
		||||
			) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -143,7 +151,7 @@ export const usePreferences = defineStore('preferences', () => {
 | 
			
		|||
		const preferences = {
 | 
			
		||||
			displayTimeLeft: displayTimeLeft.value,
 | 
			
		||||
			presentLyrics: presentLyrics.value,
 | 
			
		||||
			autoRedirect: autoRedirect.value
 | 
			
		||||
			autoRedirect: autoRedirect.value,
 | 
			
		||||
		}
 | 
			
		||||
		await setStoredValue('preferences', preferences)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -188,6 +196,6 @@ export const usePreferences = defineStore('preferences', () => {
 | 
			
		|||
		getStoredValue,
 | 
			
		||||
		setStoredValue,
 | 
			
		||||
		getPreferences,
 | 
			
		||||
		savePreferences
 | 
			
		||||
		savePreferences,
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
import { defineStore } from "pinia"
 | 
			
		||||
import { ref } from "vue"
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
// 声明全局类型
 | 
			
		||||
declare global {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,7 +26,11 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
			
		|||
	const detectAvailableAPIs = () => {
 | 
			
		||||
		// 检查原生 chrome API
 | 
			
		||||
		try {
 | 
			
		||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
 | 
			
		||||
			if (
 | 
			
		||||
				typeof chrome !== 'undefined' &&
 | 
			
		||||
				chrome.storage &&
 | 
			
		||||
				chrome.storage.sync
 | 
			
		||||
			) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +40,11 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
			
		|||
 | 
			
		||||
		// 检查 window.chrome
 | 
			
		||||
		try {
 | 
			
		||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
 | 
			
		||||
			if (
 | 
			
		||||
				window.chrome &&
 | 
			
		||||
				window.chrome.storage &&
 | 
			
		||||
				window.chrome.storage.sync
 | 
			
		||||
			) {
 | 
			
		||||
				storageType.value = '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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -199,6 +210,6 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
			
		|||
		getLastShownVersion,
 | 
			
		||||
		initializeUpdatePopup,
 | 
			
		||||
		getStoredValue,
 | 
			
		||||
		setStoredValue
 | 
			
		||||
		setStoredValue,
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +1,15 @@
 | 
			
		|||
@import "tailwindcss";
 | 
			
		||||
/* 导入来自 /assets/MiSans_VF.ttf 的字体 */
 | 
			
		||||
@font-face {
 | 
			
		||||
	font-family: 'MiSans';
 | 
			
		||||
	src: url('/assets/MiSans_VF.ttf') format('truetype-variations');
 | 
			
		||||
	font-family: "MiSans";
 | 
			
		||||
	src: url("/assets/MiSans_VF.ttf") format("truetype-variations");
 | 
			
		||||
	font-weight: 1 999;
 | 
			
		||||
	font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
	font-family: 'Alte DIN';
 | 
			
		||||
	src: url('/assets/din1451alt.ttf') format('truetype-variations');
 | 
			
		||||
	font-family: "Alte DIN";
 | 
			
		||||
	src: url("/assets/din1451alt.ttf") format("truetype-variations");
 | 
			
		||||
	font-weight: 1 999;
 | 
			
		||||
	font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -27,5 +27,5 @@ input {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.track_num {
 | 
			
		||||
	font-family: 'DIN Alternate', 'Alte DIN' !important;
 | 
			
		||||
	font-family: "DIN Alternate", "Alte DIN" !important;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +1,10 @@
 | 
			
		|||
export default (list: string[]) => {
 | 
			
		||||
	if (list.length === 0) { return '未知音乐人' }
 | 
			
		||||
	return list.map((artist) => {
 | 
			
		||||
		return artist
 | 
			
		||||
	}).join(' / ')
 | 
			
		||||
	if (list.length === 0) {
 | 
			
		||||
		return '未知音乐人'
 | 
			
		||||
	}
 | 
			
		||||
	return list
 | 
			
		||||
		.map((artist) => {
 | 
			
		||||
			return artist
 | 
			
		||||
		})
 | 
			
		||||
		.join(' / ')
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +24,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
		midBoost = 1.2, // 提升中音
 | 
			
		||||
		trebleBoost = 1.5, // 提升高音
 | 
			
		||||
		threshold = 15, // 响度门槛,低于此值不产生波动
 | 
			
		||||
    minHeight = 0         // 最小高度百分比
 | 
			
		||||
		minHeight = 0, // 最小高度百分比
 | 
			
		||||
	} = options
 | 
			
		||||
 | 
			
		||||
	console.log('[AudioVisualizer] 初始化平衡频谱,选项:', options)
 | 
			
		||||
| 
						 | 
				
			
			@ -65,8 +65,15 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
		try {
 | 
			
		||||
			log('开始初始化音频上下文...')
 | 
			
		||||
 | 
			
		||||
      audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
 | 
			
		||||
      log('AudioContext 创建成功, 状态:', audioContext.state, '采样率:', audioContext.sampleRate)
 | 
			
		||||
			audioContext = new (
 | 
			
		||||
				window.AudioContext || (window as any).webkitAudioContext
 | 
			
		||||
			)()
 | 
			
		||||
			log(
 | 
			
		||||
				'AudioContext 创建成功, 状态:',
 | 
			
		||||
				audioContext.state,
 | 
			
		||||
				'采样率:',
 | 
			
		||||
				audioContext.sampleRate,
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			// 如果上下文被暂停,尝试恢复
 | 
			
		||||
			if (audioContext.state === 'suspended') {
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +105,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
				frequencyBinCount: analyser.frequencyBinCount,
 | 
			
		||||
				sampleRate: audioContext.sampleRate,
 | 
			
		||||
				frequencyResolution: audioContext.sampleRate / analyser.fftSize,
 | 
			
		||||
        maxDecibels: analyser.maxDecibels
 | 
			
		||||
				maxDecibels: analyser.maxDecibels,
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			// 连接音频节点
 | 
			
		||||
| 
						 | 
				
			
			@ -111,7 +118,6 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
			isInitialized.value = true
 | 
			
		||||
			error.value = null
 | 
			
		||||
			log('✅ 音频可视化器初始化成功')
 | 
			
		||||
      
 | 
			
		||||
		} catch (err) {
 | 
			
		||||
			log('❌ 音频上下文初始化失败:', err)
 | 
			
		||||
			error.value = `初始化失败: ${err instanceof Error ? err.message : String(err)}`
 | 
			
		||||
| 
						 | 
				
			
			@ -150,21 +156,29 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
		analyser.getByteFrequencyData(dataArray)
 | 
			
		||||
 | 
			
		||||
		// 使用平衡的频段分割
 | 
			
		||||
    const frequencyBands = divideFrequencyBandsBalanced(dataArray, barCount, audioContext.sampleRate)
 | 
			
		||||
		const frequencyBands = divideFrequencyBandsBalanced(
 | 
			
		||||
			dataArray,
 | 
			
		||||
			barCount,
 | 
			
		||||
			audioContext.sampleRate,
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		// 应用频段特定的增强
 | 
			
		||||
		const enhancedBands = applyFrequencyEnhancement(frequencyBands)
 | 
			
		||||
 | 
			
		||||
		// 更新竖杠高度 (0-100)
 | 
			
		||||
    barHeights.value = enhancedBands.map(value => 
 | 
			
		||||
      Math.min(100, Math.max(0, (value / 255) * 100 * sensitivity))
 | 
			
		||||
		barHeights.value = enhancedBands.map((value) =>
 | 
			
		||||
			Math.min(100, Math.max(0, (value / 255) * 100 * sensitivity)),
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
		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 result: number[] = []
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -175,11 +189,12 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
			{ min: 250, max: 800, name: '中低音' }, // 索引 2
 | 
			
		||||
			{ min: 800, max: 2500, name: '中音' }, // 索引 3
 | 
			
		||||
			{ 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++) {
 | 
			
		||||
      const range = frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
 | 
			
		||||
			const range =
 | 
			
		||||
				frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
 | 
			
		||||
 | 
			
		||||
			// 将频率转换为 bin 索引
 | 
			
		||||
			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)
 | 
			
		||||
 | 
			
		||||
			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 (均方根) 值,而不是简单平均
 | 
			
		||||
| 
						 | 
				
			
			@ -213,7 +230,14 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
	// 应用频段特定的增强和门槛
 | 
			
		||||
	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) => {
 | 
			
		||||
			// 应用响度门槛
 | 
			
		||||
| 
						 | 
				
			
			@ -221,7 +245,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
				if (debug && Math.random() < 0.01) {
 | 
			
		||||
					log(`频段 ${index} 低于门槛: ${Math.round(value)} < ${threshold}`)
 | 
			
		||||
				}
 | 
			
		||||
        return minHeight * 255 / 100  // 返回最小高度对应的值
 | 
			
		||||
				return (minHeight * 255) / 100 // 返回最小高度对应的值
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const boost = boosts[index] || 1
 | 
			
		||||
| 
						 | 
				
			
			@ -252,9 +276,13 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
		if (audioElement.readyState >= 2) {
 | 
			
		||||
			initAudioContext(audioElement)
 | 
			
		||||
		} else {
 | 
			
		||||
      audioElement.addEventListener('loadeddata', () => {
 | 
			
		||||
			audioElement.addEventListener(
 | 
			
		||||
				'loadeddata',
 | 
			
		||||
				() => {
 | 
			
		||||
					initAudioContext(audioElement)
 | 
			
		||||
      }, { once: true })
 | 
			
		||||
				},
 | 
			
		||||
				{ once: true },
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 监听播放状态
 | 
			
		||||
| 
						 | 
				
			
			@ -309,7 +337,7 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
				Math.random() * 70 + 15, // 中低音:15-85
 | 
			
		||||
				Math.random() * 80 + 10, // 中音:10-90
 | 
			
		||||
				Math.random() * 75 + 10, // 中高音:10-85
 | 
			
		||||
        Math.random() * 65 + 15   // 高音:15-80
 | 
			
		||||
				Math.random() * 65 + 15, // 高音:15-80
 | 
			
		||||
			]
 | 
			
		||||
			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.midBoost = mid
 | 
			
		||||
		options.trebleBoost = treble
 | 
			
		||||
| 
						 | 
				
			
			@ -338,7 +372,13 @@ export function audioVisualizer(options: AudioVisualizerOptions = {}) {
 | 
			
		|||
				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,
 | 
			
		||||
		updateEnhancement,
 | 
			
		||||
		setThreshold,
 | 
			
		||||
    setMaxDecibels
 | 
			
		||||
		setMaxDecibels,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,13 +11,15 @@ export function isSafari(): boolean {
 | 
			
		|||
 | 
			
		||||
	// 检测 Safari 浏览器(包括 iOS 和 macOS)
 | 
			
		||||
	// Safari 的 User Agent 包含 'safari' 但不包含 'chrome' 或 'chromium'
 | 
			
		||||
	const isSafariBrowser = ua.includes('safari') && 
 | 
			
		||||
	const isSafariBrowser =
 | 
			
		||||
		ua.includes('safari') &&
 | 
			
		||||
		!ua.includes('chrome') &&
 | 
			
		||||
		!ua.includes('chromium') &&
 | 
			
		||||
		!ua.includes('android')
 | 
			
		||||
 | 
			
		||||
	// 额外检查:使用 Safari 特有的 API
 | 
			
		||||
	const isSafariByFeature = 'safari' in window || 
 | 
			
		||||
	const isSafariByFeature =
 | 
			
		||||
		'safari' in window ||
 | 
			
		||||
		/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
 | 
			
		||||
 | 
			
		||||
	return isSafariBrowser || isSafariByFeature
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +30,9 @@ export function isSafari(): boolean {
 | 
			
		|||
 * @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false
 | 
			
		||||
 */
 | 
			
		||||
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 的支持有限制
 | 
			
		||||
	// 特别是在处理跨域音频资源时
 | 
			
		||||
	if (isSafari()) {
 | 
			
		||||
		console.log('[BrowserDetection] Safari detected, audio visualization disabled')
 | 
			
		||||
		console.log(
 | 
			
		||||
			'[BrowserDetection] Safari detected, audio visualization disabled',
 | 
			
		||||
		)
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查基本的 Web Audio API 支持
 | 
			
		||||
	const hasAudioContext = 'AudioContext' in window || 'webkitAudioContext' in window
 | 
			
		||||
	const hasAnalyserNode = hasAudioContext && (
 | 
			
		||||
		'AnalyserNode' in window || 
 | 
			
		||||
		((window as any).AudioContext && 'createAnalyser' in (window as any).AudioContext.prototype)
 | 
			
		||||
	)
 | 
			
		||||
	const hasAudioContext =
 | 
			
		||||
		'AudioContext' in window || 'webkitAudioContext' in window
 | 
			
		||||
	const hasAnalyserNode =
 | 
			
		||||
		hasAudioContext &&
 | 
			
		||||
		('AnalyserNode' in window ||
 | 
			
		||||
			((window as any).AudioContext &&
 | 
			
		||||
				'createAnalyser' in (window as any).AudioContext.prototype))
 | 
			
		||||
 | 
			
		||||
	return hasAudioContext && hasAnalyserNode
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -93,6 +101,6 @@ export function getBrowserInfo() {
 | 
			
		|||
		version: browserVersion,
 | 
			
		||||
		isSafari: isSafari(),
 | 
			
		||||
		isMobileSafari: isMobileSafari(),
 | 
			
		||||
		supportsAudioVisualization: supportsWebAudioVisualization()
 | 
			
		||||
		supportsAudioVisualization: supportsWebAudioVisualization(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,16 @@
 | 
			
		|||
import artistsOrganize from "./artistsOrganize"
 | 
			
		||||
import { audioVisualizer } from "./audioVisualizer"
 | 
			
		||||
import cicdInfo from "./cicdInfo"
 | 
			
		||||
import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker"
 | 
			
		||||
import { isSafari, isMobileSafari, supportsWebAudioVisualization, getBrowserInfo } from "./browserDetection"
 | 
			
		||||
import artistsOrganize from './artistsOrganize'
 | 
			
		||||
import { audioVisualizer } from './audioVisualizer'
 | 
			
		||||
import cicdInfo from './cicdInfo'
 | 
			
		||||
import {
 | 
			
		||||
	checkAndRefreshSongResource,
 | 
			
		||||
	checkAndRefreshMultipleSongs,
 | 
			
		||||
} from './songResourceChecker'
 | 
			
		||||
import {
 | 
			
		||||
	isSafari,
 | 
			
		||||
	isMobileSafari,
 | 
			
		||||
	supportsWebAudioVisualization,
 | 
			
		||||
	getBrowserInfo,
 | 
			
		||||
} from './browserDetection'
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
	artistsOrganize,
 | 
			
		||||
| 
						 | 
				
			
			@ -13,5 +21,5 @@ export {
 | 
			
		|||
	isSafari,
 | 
			
		||||
	isMobileSafari,
 | 
			
		||||
	supportsWebAudioVisualization,
 | 
			
		||||
	getBrowserInfo
 | 
			
		||||
	getBrowserInfo,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,7 @@ import apis from '../apis'
 | 
			
		|||
 */
 | 
			
		||||
export const checkAndRefreshSongResource = async (
 | 
			
		||||
	song: Song,
 | 
			
		||||
  updateCallback?: (updatedSong: Song) => void
 | 
			
		||||
	updateCallback?: (updatedSong: Song) => void,
 | 
			
		||||
): Promise<Song> => {
 | 
			
		||||
	if (!song.sourceUrl) {
 | 
			
		||||
		console.warn('[ResourceChecker] 歌曲没有 sourceUrl:', song.name)
 | 
			
		||||
| 
						 | 
				
			
			@ -21,13 +21,13 @@ export const checkAndRefreshSongResource = async (
 | 
			
		|||
		await axios.head(song.sourceUrl, {
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Cache-Control': 'no-cache, no-store, must-revalidate',
 | 
			
		||||
        'Pragma': 'no-cache',
 | 
			
		||||
        'Expires': '0'
 | 
			
		||||
				Pragma: 'no-cache',
 | 
			
		||||
				Expires: '0',
 | 
			
		||||
			},
 | 
			
		||||
			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 (
 | 
			
		||||
	songs: Song[],
 | 
			
		||||
  updateCallback?: (updatedSong: Song, originalIndex: number) => void
 | 
			
		||||
	updateCallback?: (updatedSong: Song, originalIndex: number) => void,
 | 
			
		||||
): Promise<Song[]> => {
 | 
			
		||||
	const results: Song[] = []
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +71,7 @@ export const checkAndRefreshMultipleSongs = async (
 | 
			
		|||
		const originalSong = songs[i]
 | 
			
		||||
		const updatedSong = await checkAndRefreshSongResource(
 | 
			
		||||
			originalSong,
 | 
			
		||||
      updateCallback ? (updated) => updateCallback(updated, i) : undefined
 | 
			
		||||
			updateCallback ? (updated) => updateCallback(updated, i) : undefined,
 | 
			
		||||
		)
 | 
			
		||||
		results.push(updatedSong)
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import tailwindcss from '@tailwindcss/vite'
 | 
			
		||||
import vue from '@vitejs/plugin-vue'
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import path from "node:path"
 | 
			
		||||
import path from 'node:path'
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ export default defineConfig({
 | 
			
		|||
	},
 | 
			
		||||
	resolve: {
 | 
			
		||||
		alias: {
 | 
			
		||||
      "@": path.resolve(__dirname, "./src"),
 | 
			
		||||
			'@': path.resolve(__dirname, './src'),
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user