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