lint: format
This commit is contained in:
parent
60740274b7
commit
35f7332bff
14
.env.example
Normal file
14
.env.example
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Debug Configuration
|
||||
# Set DEBUG environment variable to control debug output
|
||||
# Examples:
|
||||
# DEBUG=msr:* # Enable all MSR debug output
|
||||
# DEBUG=msr:player # Enable only player debug
|
||||
# DEBUG=msr:store,msr:api # Enable store and API debug
|
||||
# DEBUG=* # Enable all debug output (including libraries)
|
||||
# DEBUG= # Disable all debug output
|
||||
|
||||
# Development (default: enable all msr:* debug)
|
||||
VITE_DEBUG=msr:*
|
||||
|
||||
# Production (default: disabled)
|
||||
# VITE_DEBUG=
|
|
@ -1,7 +1,7 @@
|
|||
console.log("aaaa")
|
||||
console.log('aaaa')
|
||||
|
||||
// 兼容 Chrome 和 Firefox
|
||||
const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
|
||||
const browserAPI = typeof browser !== 'undefined' ? browser : chrome
|
||||
|
||||
browserAPI.webRequest.onBeforeRequest.addListener(
|
||||
async (details) => {
|
||||
|
@ -16,12 +16,18 @@ browserAPI.webRequest.onBeforeRequest.addListener(
|
|||
console.log('recived request for fontset api, redirecting to index.html')
|
||||
const pref = await browserAPI.storage.sync.get('preferences')
|
||||
|
||||
if (pref === undefined || pref.preferences === undefined || pref.preferences.autoRedirect === undefined || pref.preferences.autoRedirect === true) {
|
||||
const isChrome = typeof browserAPI.runtime.getBrowserInfo === 'undefined';
|
||||
if (
|
||||
pref === undefined ||
|
||||
pref.preferences === undefined ||
|
||||
pref.preferences.autoRedirect === undefined ||
|
||||
pref.preferences.autoRedirect === true
|
||||
) {
|
||||
const isChrome = typeof browserAPI.runtime.getBrowserInfo === 'undefined'
|
||||
|
||||
if (isChrome) {
|
||||
if (
|
||||
details.url === 'https://monster-siren.hypergryph.com/manifest.json' &&
|
||||
details.url ===
|
||||
'https://monster-siren.hypergryph.com/manifest.json' &&
|
||||
details.type === 'other' &&
|
||||
details.frameId === 0
|
||||
) {
|
||||
|
@ -32,15 +38,22 @@ browserAPI.webRequest.onBeforeRequest.addListener(
|
|||
}
|
||||
} else {
|
||||
// Firefox: 直接在当前标签页导航
|
||||
browserAPI.tabs.update(details.tabId, { url: browserAPI.runtime.getURL('index.html') })
|
||||
browserAPI.tabs.update(details.tabId, {
|
||||
url: browserAPI.runtime.getURL('index.html'),
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json'] },
|
||||
{
|
||||
urls: [
|
||||
'https://monster-siren.hypergryph.com/api/fontset',
|
||||
'https://monster-siren.hypergryph.com/manifest.json',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
// 兼容新旧版本的 API
|
||||
const actionAPI = browserAPI.action || browserAPI.browserAction;
|
||||
const actionAPI = browserAPI.action || browserAPI.browserAction
|
||||
if (actionAPI) {
|
||||
actionAPI.onClicked.addListener(() => {
|
||||
browserAPI.tabs.create({ url: browserAPI.runtime.getURL('index.html') })
|
||||
|
|
|
@ -5,12 +5,8 @@
|
|||
"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://monster-siren.hypergryph.com/"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
],
|
||||
"matches": ["https://monster-siren.hypergryph.com/"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
|
@ -36,11 +32,7 @@
|
|||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"webRequest",
|
||||
"storage"
|
||||
],
|
||||
"permissions": ["tabs", "webRequest", "storage"],
|
||||
"content_security_policy": {
|
||||
"extension_pages": "default-src 'self'; script-src 'self' http://localhost:5173; style-src 'self' 'unsafe-inline'; connect-src 'self' ws://localhost:5173 https://monster-siren.hypergryph.com https://web.hycdn.cn https://res01.hycdn.cn; img-src 'self' https://web.hycdn.cn; media-src 'self' https://res01.hycdn.cn;",
|
||||
"sandbox": "sandbox"
|
||||
|
|
|
@ -1,61 +1,65 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// 处理 manifest.json
|
||||
function processManifest() {
|
||||
const manifestPath = path.join(__dirname, '../public/manifest.json');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const manifestPath = path.join(__dirname, '../public/manifest.json')
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
||||
|
||||
// 移除本地调试相关的配置
|
||||
if (manifest.host_permissions) {
|
||||
manifest.host_permissions = manifest.host_permissions.filter(
|
||||
permission => !permission.includes('localhost')
|
||||
);
|
||||
}
|
||||
// 移除本地调试相关的配置
|
||||
if (manifest.host_permissions) {
|
||||
manifest.host_permissions = manifest.host_permissions.filter(
|
||||
(permission) => !permission.includes('localhost'),
|
||||
)
|
||||
}
|
||||
|
||||
if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) {
|
||||
// 移除 CSP 中的本地开发相关配置
|
||||
manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages
|
||||
.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
|
||||
.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/;\s+/g, '; ') // 标准化分号后的空格
|
||||
.replace(/\s+/g, ' ') // 合并多个空格为一个
|
||||
.trim();
|
||||
}
|
||||
if (
|
||||
manifest.content_security_policy &&
|
||||
manifest.content_security_policy.extension_pages
|
||||
) {
|
||||
// 移除 CSP 中的本地开发相关配置
|
||||
manifest.content_security_policy.extension_pages =
|
||||
manifest.content_security_policy.extension_pages
|
||||
.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
|
||||
.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/;\s+/g, '; ') // 标准化分号后的空格
|
||||
.replace(/\s+/g, ' ') // 合并多个空格为一个
|
||||
.trim()
|
||||
}
|
||||
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
console.log('✅ Manifest.json processed');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
console.log('✅ Manifest.json processed')
|
||||
}
|
||||
|
||||
// 处理 index.html
|
||||
function processIndexHtml() {
|
||||
const indexPath = path.join(__dirname, '../index.html');
|
||||
let content = fs.readFileSync(indexPath, 'utf8');
|
||||
const indexPath = path.join(__dirname, '../index.html')
|
||||
let content = fs.readFileSync(indexPath, 'utf8')
|
||||
|
||||
// 替换脚本地址
|
||||
content = content.replace(
|
||||
/src="[^"]*\/src\/main\.ts"/g,
|
||||
'src="./src/main.ts"'
|
||||
);
|
||||
// 替换脚本地址
|
||||
content = content.replace(
|
||||
/src="[^"]*\/src\/main\.ts"/g,
|
||||
'src="./src/main.ts"',
|
||||
)
|
||||
|
||||
// 移除 crossorigin 属性
|
||||
content = content.replace(/\s+crossorigin/g, '');
|
||||
// 移除 crossorigin 属性
|
||||
content = content.replace(/\s+crossorigin/g, '')
|
||||
|
||||
fs.writeFileSync(indexPath, content);
|
||||
console.log('✅ Index.html processed');
|
||||
fs.writeFileSync(indexPath, content)
|
||||
console.log('✅ Index.html processed')
|
||||
}
|
||||
|
||||
// 执行处理
|
||||
try {
|
||||
processManifest();
|
||||
processIndexHtml();
|
||||
console.log('🎉 Build preparation completed!');
|
||||
processManifest()
|
||||
processIndexHtml()
|
||||
console.log('🎉 Build preparation completed!')
|
||||
} catch (error) {
|
||||
console.error('❌ Error during build preparation:', error);
|
||||
process.exit(1);
|
||||
console.error('❌ Error during build preparation:', error)
|
||||
process.exit(1)
|
||||
}
|
|
@ -1,80 +1,87 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// 处理 manifest.json
|
||||
function processManifest() {
|
||||
const manifestPath = path.join(__dirname, '../public/manifest.json');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const manifestPath = path.join(__dirname, '../public/manifest.json')
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
||||
|
||||
// 移除本地调试相关的配置
|
||||
if (manifest.host_permissions) {
|
||||
manifest.host_permissions = manifest.host_permissions.filter(
|
||||
permission => !permission.includes('localhost')
|
||||
);
|
||||
}
|
||||
// 移除本地调试相关的配置
|
||||
if (manifest.host_permissions) {
|
||||
manifest.host_permissions = manifest.host_permissions.filter(
|
||||
(permission) => !permission.includes('localhost'),
|
||||
)
|
||||
}
|
||||
|
||||
if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) {
|
||||
// 移除 CSP 中的本地开发相关配置
|
||||
manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages
|
||||
.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
|
||||
.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/;\s+/g, '; ') // 标准化分号后的空格
|
||||
.replace(/\s+/g, ' ') // 合并多个空格为一个
|
||||
.trim();
|
||||
}
|
||||
if (
|
||||
manifest.content_security_policy &&
|
||||
manifest.content_security_policy.extension_pages
|
||||
) {
|
||||
// 移除 CSP 中的本地开发相关配置
|
||||
manifest.content_security_policy.extension_pages =
|
||||
manifest.content_security_policy.extension_pages
|
||||
.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
|
||||
.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/;\s+/g, '; ') // 标准化分号后的空格
|
||||
.replace(/\s+/g, ' ') // 合并多个空格为一个
|
||||
.trim()
|
||||
}
|
||||
|
||||
// 移除 CSP 中的 sandbox 配置(Firefox 不支持)
|
||||
if (manifest.content_security_policy && manifest.content_security_policy.sandbox) {
|
||||
delete manifest.content_security_policy.sandbox;
|
||||
}
|
||||
// 移除 CSP 中的 sandbox 配置(Firefox 不支持)
|
||||
if (
|
||||
manifest.content_security_policy &&
|
||||
manifest.content_security_policy.sandbox
|
||||
) {
|
||||
delete manifest.content_security_policy.sandbox
|
||||
}
|
||||
|
||||
// 移除 background.service_worker,替换为 background.scripts
|
||||
if (manifest.background && manifest.background.service_worker) {
|
||||
manifest.background.scripts = [manifest.background.service_worker];
|
||||
delete manifest.background.service_worker;
|
||||
}
|
||||
// 移除 background.service_worker,替换为 background.scripts
|
||||
if (manifest.background && manifest.background.service_worker) {
|
||||
manifest.background.scripts = [manifest.background.service_worker]
|
||||
delete manifest.background.service_worker
|
||||
}
|
||||
|
||||
// 添加 firefox 特有配置
|
||||
manifest.browser_specific_settings = {
|
||||
gecko: {
|
||||
id: 'msr-mod@firefox-addon.astrian.moe',
|
||||
strict_min_version: '115.0',
|
||||
}
|
||||
};
|
||||
// 添加 firefox 特有配置
|
||||
manifest.browser_specific_settings = {
|
||||
gecko: {
|
||||
id: 'msr-mod@firefox-addon.astrian.moe',
|
||||
strict_min_version: '115.0',
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
console.log('✅ Manifest.json processed');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
console.log('✅ Manifest.json processed')
|
||||
}
|
||||
|
||||
// 处理 index.html
|
||||
function processIndexHtml() {
|
||||
const indexPath = path.join(__dirname, '../index.html');
|
||||
let content = fs.readFileSync(indexPath, 'utf8');
|
||||
const indexPath = path.join(__dirname, '../index.html')
|
||||
let content = fs.readFileSync(indexPath, 'utf8')
|
||||
|
||||
// 替换脚本地址
|
||||
content = content.replace(
|
||||
/src="[^"]*\/src\/main\.ts"/g,
|
||||
'src="./src/main.ts"'
|
||||
);
|
||||
// 替换脚本地址
|
||||
content = content.replace(
|
||||
/src="[^"]*\/src\/main\.ts"/g,
|
||||
'src="./src/main.ts"',
|
||||
)
|
||||
|
||||
// 移除 crossorigin 属性
|
||||
content = content.replace(/\s+crossorigin/g, '');
|
||||
// 移除 crossorigin 属性
|
||||
content = content.replace(/\s+crossorigin/g, '')
|
||||
|
||||
fs.writeFileSync(indexPath, content);
|
||||
console.log('✅ Index.html processed');
|
||||
fs.writeFileSync(indexPath, content)
|
||||
console.log('✅ Index.html processed')
|
||||
}
|
||||
|
||||
// 执行处理
|
||||
try {
|
||||
processManifest();
|
||||
processIndexHtml();
|
||||
console.log('🎉 Build preparation completed!');
|
||||
processManifest()
|
||||
processIndexHtml()
|
||||
console.log('🎉 Build preparation completed!')
|
||||
} catch (error) {
|
||||
console.error('❌ Error during build preparation:', error);
|
||||
process.exit(1);
|
||||
console.error('❌ Error during build preparation:', error)
|
||||
process.exit(1)
|
||||
}
|
|
@ -1,55 +1,59 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// 处理 manifest.json for Safari
|
||||
function processManifest() {
|
||||
const manifestPath = path.join(__dirname, '../public/manifest.json');
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const manifestPath = path.join(__dirname, '../public/manifest.json')
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
||||
|
||||
// 移除本地调试相关的配置
|
||||
if (manifest.host_permissions) {
|
||||
manifest.host_permissions = manifest.host_permissions.filter(
|
||||
permission => !permission.includes('localhost')
|
||||
);
|
||||
}
|
||||
// 移除本地调试相关的配置
|
||||
if (manifest.host_permissions) {
|
||||
manifest.host_permissions = manifest.host_permissions.filter(
|
||||
(permission) => !permission.includes('localhost'),
|
||||
)
|
||||
}
|
||||
|
||||
if (manifest.content_security_policy && manifest.content_security_policy.extension_pages) {
|
||||
// 移除 CSP 中的本地开发相关配置
|
||||
manifest.content_security_policy.extension_pages = manifest.content_security_policy.extension_pages
|
||||
.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
|
||||
.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/;\s+/g, '; ') // 标准化分号后的空格
|
||||
.replace(/\s+/g, ' ') // 合并多个空格为一个
|
||||
.trim();
|
||||
}
|
||||
if (
|
||||
manifest.content_security_policy &&
|
||||
manifest.content_security_policy.extension_pages
|
||||
) {
|
||||
// 移除 CSP 中的本地开发相关配置
|
||||
manifest.content_security_policy.extension_pages =
|
||||
manifest.content_security_policy.extension_pages
|
||||
.replace(/script-src 'self' http:\/\/localhost:5173;\s*/g, '')
|
||||
.replace(/\s*http:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/\s*ws:\/\/localhost:5173\s*/g, ' ')
|
||||
.replace(/;\s+/g, '; ') // 标准化分号后的空格
|
||||
.replace(/\s+/g, ' ') // 合并多个空格为一个
|
||||
.trim()
|
||||
}
|
||||
|
||||
// Safari 特殊处理:添加 appShell.html 到 content scripts 匹配
|
||||
if (manifest.content_scripts && manifest.content_scripts[0]) {
|
||||
// 添加 appShell.html 的匹配规则
|
||||
const existingMatches = manifest.content_scripts[0].matches;
|
||||
if (!existingMatches.includes("https://monster-siren.hypergryph.com/")) {
|
||||
existingMatches.push("https://monster-siren.hypergryph.com/");
|
||||
}
|
||||
}
|
||||
// Safari 特殊处理:添加 appShell.html 到 content scripts 匹配
|
||||
if (manifest.content_scripts && manifest.content_scripts[0]) {
|
||||
// 添加 appShell.html 的匹配规则
|
||||
const existingMatches = manifest.content_scripts[0].matches
|
||||
if (!existingMatches.includes('https://monster-siren.hypergryph.com/')) {
|
||||
existingMatches.push('https://monster-siren.hypergryph.com/')
|
||||
}
|
||||
}
|
||||
|
||||
// Safari 特殊处理:使用 background.page 而不是 service_worker
|
||||
if (manifest.background && manifest.background.service_worker) {
|
||||
// Safari 扩展在 Manifest V3 中必须使用 persistent: false
|
||||
// 但为了调试,我们暂时设为 true 来确保页面加载
|
||||
manifest.background = {
|
||||
page: "background.html",
|
||||
persistent: true
|
||||
};
|
||||
}
|
||||
// Safari 特殊处理:使用 background.page 而不是 service_worker
|
||||
if (manifest.background && manifest.background.service_worker) {
|
||||
// Safari 扩展在 Manifest V3 中必须使用 persistent: false
|
||||
// 但为了调试,我们暂时设为 true 来确保页面加载
|
||||
manifest.background = {
|
||||
page: 'background.html',
|
||||
persistent: true,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 background.html 文件用于 Safari
|
||||
const backgroundHtmlPath = path.join(__dirname, '../public/background.html');
|
||||
const backgroundHtmlContent = `<!DOCTYPE html>
|
||||
// 创建 background.html 文件用于 Safari
|
||||
const backgroundHtmlPath = path.join(__dirname, '../public/background.html')
|
||||
const backgroundHtmlContent = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
@ -102,19 +106,19 @@ function processManifest() {
|
|||
log('=== After background.js script tag ===');
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent);
|
||||
</html>`
|
||||
fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent)
|
||||
|
||||
// 创建 Safari 兼容的 background.js
|
||||
const backgroundJsPath = path.join(__dirname, '../public/background.js');
|
||||
let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8');
|
||||
// 创建 Safari 兼容的 background.js
|
||||
const backgroundJsPath = path.join(__dirname, '../public/background.js')
|
||||
let backgroundJsContent = fs.readFileSync(backgroundJsPath, 'utf8')
|
||||
|
||||
// 检查是否已经添加过 Safari 代码,避免重复
|
||||
if (backgroundJsContent.includes('=== Safari background.js starting ===')) {
|
||||
console.log('Safari background.js already processed, skipping...');
|
||||
} else {
|
||||
// 在开头添加 Safari 调试信息(只添加一次)
|
||||
const safariDebugCode = `
|
||||
// 检查是否已经添加过 Safari 代码,避免重复
|
||||
if (backgroundJsContent.includes('=== Safari background.js starting ===')) {
|
||||
console.log('Safari background.js already processed, skipping...')
|
||||
} else {
|
||||
// 在开头添加 Safari 调试信息(只添加一次)
|
||||
const safariDebugCode = `
|
||||
console.log("=== Safari background.js starting ===");
|
||||
console.log("Available APIs:", {
|
||||
chrome: typeof chrome,
|
||||
|
@ -168,40 +172,42 @@ if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage)
|
|||
});
|
||||
}
|
||||
|
||||
`;
|
||||
`
|
||||
|
||||
// 替换 Safari 的重定向 URL 监听
|
||||
backgroundJsContent = backgroundJsContent.replace(
|
||||
/{ urls: \['https:\/\/monster-siren\.hypergryph\.com\/api\/fontset', 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'\] }/g,
|
||||
"{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json', 'https://monster-siren.hypergryph.com/'] }"
|
||||
);
|
||||
// 替换 Safari 的重定向 URL 监听
|
||||
backgroundJsContent = backgroundJsContent.replace(
|
||||
/{ urls: \['https:\/\/monster-siren\.hypergryph\.com\/api\/fontset', 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'\] }/g,
|
||||
"{ urls: ['https://monster-siren.hypergryph.com/api/fontset', 'https://monster-siren.hypergryph.com/manifest.json', 'https://monster-siren.hypergryph.com/'] }",
|
||||
)
|
||||
|
||||
// 替换 Safari 的重定向判断逻辑
|
||||
backgroundJsContent = backgroundJsContent.replace(
|
||||
/details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'/g,
|
||||
"(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')"
|
||||
);
|
||||
// 替换 Safari 的重定向判断逻辑
|
||||
backgroundJsContent = backgroundJsContent.replace(
|
||||
/details\.url === 'https:\/\/monster-siren\.hypergryph\.com\/manifest\.json'/g,
|
||||
"(details.url === 'https://monster-siren.hypergryph.com/manifest.json' || details.url === 'https://monster-siren.hypergryph.com/')",
|
||||
)
|
||||
|
||||
// 清理可能的重复条件
|
||||
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/')"
|
||||
);
|
||||
// 清理可能的重复条件
|
||||
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/')",
|
||||
)
|
||||
|
||||
backgroundJsContent = safariDebugCode + backgroundJsContent;
|
||||
}
|
||||
fs.writeFileSync(backgroundJsPath, backgroundJsContent);
|
||||
console.log('✅ Safari-compatible background.js created');
|
||||
backgroundJsContent = safariDebugCode + backgroundJsContent
|
||||
}
|
||||
fs.writeFileSync(backgroundJsPath, backgroundJsContent)
|
||||
console.log('✅ Safari-compatible background.js created')
|
||||
|
||||
// 创建 Safari 专用的 content.js
|
||||
const contentJsPath = path.join(__dirname, '../public/content.js');
|
||||
// 创建 Safari 专用的 content.js
|
||||
const contentJsPath = path.join(__dirname, '../public/content.js')
|
||||
|
||||
// 检查是否已经处理过 content.js
|
||||
const existingContentJs = fs.existsSync(contentJsPath) ? fs.readFileSync(contentJsPath, 'utf8') : '';
|
||||
if (existingContentJs.includes('checkRedirectPreference')) {
|
||||
console.log('Safari content.js already processed, skipping...');
|
||||
} else {
|
||||
const contentJsContent = `
|
||||
// 检查是否已经处理过 content.js
|
||||
const existingContentJs = fs.existsSync(contentJsPath)
|
||||
? fs.readFileSync(contentJsPath, 'utf8')
|
||||
: ''
|
||||
if (existingContentJs.includes('checkRedirectPreference')) {
|
||||
console.log('Safari content.js already processed, skipping...')
|
||||
} else {
|
||||
const contentJsContent = `
|
||||
// Safari 扩展 content script for redirect
|
||||
console.log('MSR Mod content script loaded on:', window.location.href);
|
||||
|
||||
|
@ -307,53 +313,53 @@ async function main() {
|
|||
main().catch(error => {
|
||||
console.error('Error in main function:', error);
|
||||
});
|
||||
`;
|
||||
`
|
||||
|
||||
fs.writeFileSync(contentJsPath, contentJsContent);
|
||||
}
|
||||
console.log('✅ Safari-compatible content.js created');
|
||||
fs.writeFileSync(contentJsPath, contentJsContent)
|
||||
}
|
||||
console.log('✅ Safari-compatible content.js created')
|
||||
|
||||
// Safari 可能需要额外的权限
|
||||
if (!manifest.permissions.includes('activeTab')) {
|
||||
manifest.permissions.push('activeTab');
|
||||
}
|
||||
// Safari 可能需要额外的权限
|
||||
if (!manifest.permissions.includes('activeTab')) {
|
||||
manifest.permissions.push('activeTab')
|
||||
}
|
||||
|
||||
// 添加 Safari 特有配置
|
||||
manifest.browser_specific_settings = {
|
||||
safari: {
|
||||
minimum_version: "14.0"
|
||||
}
|
||||
};
|
||||
// 添加 Safari 特有配置
|
||||
manifest.browser_specific_settings = {
|
||||
safari: {
|
||||
minimum_version: '14.0',
|
||||
},
|
||||
}
|
||||
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
console.log('✅ Safari Manifest.json processed');
|
||||
console.log('✅ Background.html created for Safari');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
||||
console.log('✅ Safari Manifest.json processed')
|
||||
console.log('✅ Background.html created for Safari')
|
||||
}
|
||||
|
||||
// 处理 index.html
|
||||
function processIndexHtml() {
|
||||
const indexPath = path.join(__dirname, '../index.html');
|
||||
let content = fs.readFileSync(indexPath, 'utf8');
|
||||
const indexPath = path.join(__dirname, '../index.html')
|
||||
let content = fs.readFileSync(indexPath, 'utf8')
|
||||
|
||||
// 替换脚本地址
|
||||
content = content.replace(
|
||||
/src="[^"]*\/src\/main\.ts"/g,
|
||||
'src="./src/main.ts"'
|
||||
);
|
||||
// 替换脚本地址
|
||||
content = content.replace(
|
||||
/src="[^"]*\/src\/main\.ts"/g,
|
||||
'src="./src/main.ts"',
|
||||
)
|
||||
|
||||
// 移除 crossorigin 属性
|
||||
content = content.replace(/\s+crossorigin/g, '');
|
||||
// 移除 crossorigin 属性
|
||||
content = content.replace(/\s+crossorigin/g, '')
|
||||
|
||||
fs.writeFileSync(indexPath, content);
|
||||
console.log('✅ Index.html processed for Safari');
|
||||
fs.writeFileSync(indexPath, content)
|
||||
console.log('✅ Index.html processed for Safari')
|
||||
}
|
||||
|
||||
// 执行处理
|
||||
try {
|
||||
processManifest();
|
||||
processIndexHtml();
|
||||
console.log('🎉 Safari build preparation completed!');
|
||||
processManifest()
|
||||
processIndexHtml()
|
||||
console.log('🎉 Safari build preparation completed!')
|
||||
} catch (error) {
|
||||
console.error('❌ Error during Safari build preparation:', error);
|
||||
process.exit(1);
|
||||
console.error('❌ Error during Safari build preparation:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
|
10
src/App.vue
10
src/App.vue
|
@ -17,10 +17,12 @@ const presentPreferencePanel = ref(false)
|
|||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
watch(() => presentPreferencePanel, (value) => {
|
||||
debug('偏好设置面板显示状态', value)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => presentPreferencePanel,
|
||||
(value) => {
|
||||
debug('偏好设置面板显示状态', value)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -9,33 +9,41 @@ export default {
|
|||
const songs: {
|
||||
data: ApiResponse
|
||||
} = await msrInstance.get('songs')
|
||||
if (songs.data.code !== 0) { throw new Error(`Cannot get songs: ${songs.data.msg}`) }
|
||||
if (songs.data.code !== 0) {
|
||||
throw new Error(`Cannot get songs: ${songs.data.msg}`)
|
||||
}
|
||||
return { songs: songs.data.data as { list: SongList } }
|
||||
},
|
||||
async getSong(cid: string) {
|
||||
const song: {
|
||||
data: ApiResponse
|
||||
} = await msrInstance.get(`song/${cid}`)
|
||||
if (song.data.code!== 0) { throw new Error(`Cannot get song: ${song.data.msg}`) }
|
||||
if (song.data.code !== 0) {
|
||||
throw new Error(`Cannot get song: ${song.data.msg}`)
|
||||
}
|
||||
return song.data.data as Song
|
||||
},
|
||||
async getAlbums() {
|
||||
const albums: {
|
||||
data: ApiResponse
|
||||
} = await msrInstance.get('albums')
|
||||
if (albums.data.code!== 0) { throw new Error(`Cannot get albums: ${albums.data.msg}`) }
|
||||
if (albums.data.code !== 0) {
|
||||
throw new Error(`Cannot get albums: ${albums.data.msg}`)
|
||||
}
|
||||
return albums.data.data as AlbumList
|
||||
},
|
||||
async getAlbum(cid: string) {
|
||||
const album: {
|
||||
data: ApiResponse
|
||||
} = await msrInstance.get(`album/${cid}/detail`)
|
||||
if (album.data.code!== 0) { throw new Error(`Cannot get album: ${album.data.msg}`) }
|
||||
if (album.data.code !== 0) {
|
||||
throw new Error(`Cannot get album: ${album.data.msg}`)
|
||||
}
|
||||
const albumMeta: {
|
||||
data: ApiResponse
|
||||
} = await msrInstance.get(`album/${cid}/data`)
|
||||
let data = album.data.data as Album
|
||||
data.artistes = (albumMeta.data.data as Album).artistes
|
||||
return data
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@ const closeButton = ref<HTMLElement>()
|
|||
|
||||
// Animation functions
|
||||
const animateIn = async () => {
|
||||
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) return
|
||||
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value)
|
||||
return
|
||||
|
||||
// Set initial states
|
||||
gsap.set(dialogBackdrop.value, { opacity: 0 })
|
||||
|
@ -42,74 +43,99 @@ const animateIn = async () => {
|
|||
tl.to(dialogBackdrop.value, {
|
||||
opacity: 1,
|
||||
duration: 0.3,
|
||||
ease: "power2.out"
|
||||
ease: 'power2.out',
|
||||
})
|
||||
.to(dialogContent.value, {
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: "power3.out"
|
||||
}, "-=0.1")
|
||||
.to(closeButton.value, {
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
duration: 0.3,
|
||||
ease: "back.out(1.7)"
|
||||
}, "-=0.2")
|
||||
.to(
|
||||
dialogContent.value,
|
||||
{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: 'power3.out',
|
||||
},
|
||||
'-=0.1',
|
||||
)
|
||||
.to(
|
||||
closeButton.value,
|
||||
{
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
duration: 0.3,
|
||||
ease: 'back.out(1.7)',
|
||||
},
|
||||
'-=0.2',
|
||||
)
|
||||
}
|
||||
|
||||
const animateOut = () => {
|
||||
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) return
|
||||
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value)
|
||||
return
|
||||
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => emit('dismiss')
|
||||
onComplete: () => emit('dismiss'),
|
||||
})
|
||||
|
||||
tl.to(closeButton.value, {
|
||||
scale: 0,
|
||||
rotation: 180,
|
||||
duration: 0.2,
|
||||
ease: "power2.in"
|
||||
ease: 'power2.in',
|
||||
})
|
||||
.to(dialogContent.value, {
|
||||
y: 30,
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
duration: 0.3,
|
||||
ease: "power2.in"
|
||||
}, "-=0.1")
|
||||
.to(dialogBackdrop.value, {
|
||||
opacity: 0,
|
||||
duration: 0.2,
|
||||
ease: "power2.in"
|
||||
}, "-=0.1")
|
||||
.to(
|
||||
dialogContent.value,
|
||||
{
|
||||
y: 30,
|
||||
opacity: 0,
|
||||
scale: 0.95,
|
||||
duration: 0.3,
|
||||
ease: 'power2.in',
|
||||
},
|
||||
'-=0.1',
|
||||
)
|
||||
.to(
|
||||
dialogBackdrop.value,
|
||||
{
|
||||
opacity: 0,
|
||||
duration: 0.2,
|
||||
ease: 'power2.in',
|
||||
},
|
||||
'-=0.1',
|
||||
)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
animateOut()
|
||||
}
|
||||
|
||||
watch(() => props.present, async (newVal) => {
|
||||
if (newVal) {
|
||||
await nextTick()
|
||||
animateIn()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.albumCid, async () => {
|
||||
debugUI('专辑详情对话框加载', props.albumCid)
|
||||
album.value = undefined // Reset album when cid changes
|
||||
try {
|
||||
let res = await apis.getAlbum(props.albumCid)
|
||||
for (const track in res.songs) {
|
||||
res.songs[parseInt(track)] = await apis.getSong(res.songs[parseInt(track)].cid)
|
||||
watch(
|
||||
() => props.present,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await nextTick()
|
||||
animateIn()
|
||||
}
|
||||
album.value = res
|
||||
} catch (error) {
|
||||
debugUI('专辑详情加载失败', error)
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.albumCid,
|
||||
async () => {
|
||||
debugUI('专辑详情对话框加载', props.albumCid)
|
||||
album.value = undefined // Reset album when cid changes
|
||||
try {
|
||||
let res = await apis.getAlbum(props.albumCid)
|
||||
for (const track in res.songs) {
|
||||
res.songs[parseInt(track)] = await apis.getSong(
|
||||
res.songs[parseInt(track)].cid,
|
||||
)
|
||||
}
|
||||
album.value = res
|
||||
} catch (error) {
|
||||
debugUI('专辑详情加载失败', error)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const playQueue = usePlayQueueStore()
|
||||
|
||||
|
@ -118,16 +144,15 @@ async function playTheAlbum(from: number = 0) {
|
|||
}
|
||||
|
||||
function shuffle() {
|
||||
// playTheAlbum()
|
||||
// playQueue.shuffleCurrent = true
|
||||
// playQueue.playMode.shuffle = false
|
||||
// setTimeout(() => {
|
||||
// playQueue.playMode.shuffle = true
|
||||
// playQueue.isPlaying = true
|
||||
// playQueue.isBuffering = true
|
||||
// }, 100)
|
||||
// playTheAlbum()
|
||||
// playQueue.shuffleCurrent = true
|
||||
// playQueue.playMode.shuffle = false
|
||||
// setTimeout(() => {
|
||||
// playQueue.playMode.shuffle = true
|
||||
// playQueue.isPlaying = true
|
||||
// playQueue.isBuffering = true
|
||||
// }, 100)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -59,7 +59,9 @@ function moveUp() {
|
|||
}
|
||||
|
||||
function moveDown() {
|
||||
const listLength = playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length
|
||||
const listLength = playQueueStore.playMode.shuffle
|
||||
? playQueueStore.shuffleList.length
|
||||
: playQueueStore.list.length
|
||||
if (props.index === listLength - 1) return
|
||||
|
||||
playQueueStore.queueReplaceLock = true
|
||||
|
@ -109,7 +111,10 @@ function removeItem() {
|
|||
playQueueStore.currentIndex--
|
||||
} else if (props.index === playQueueStore.currentIndex) {
|
||||
if (queue.length > 0) {
|
||||
playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
|
||||
playQueueStore.currentIndex = Math.min(
|
||||
playQueueStore.currentIndex,
|
||||
queue.length - 1,
|
||||
)
|
||||
} else {
|
||||
playQueueStore.currentIndex = 0
|
||||
}
|
||||
|
@ -140,7 +145,10 @@ function removeItem() {
|
|||
playQueueStore.currentIndex--
|
||||
} else if (props.index === playQueueStore.currentIndex) {
|
||||
if (queue.length > 0) {
|
||||
playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
|
||||
playQueueStore.currentIndex = Math.min(
|
||||
playQueueStore.currentIndex,
|
||||
queue.length - 1,
|
||||
)
|
||||
} else {
|
||||
playQueueStore.currentIndex = 0
|
||||
}
|
||||
|
|
|
@ -8,7 +8,11 @@ import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
|||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
|
||||
import PlayIcon from '../assets/icons/play.vue'
|
||||
import PauseIcon from '../assets/icons/pause.vue'
|
||||
import { audioVisualizer, checkAndRefreshSongResource, supportsWebAudioVisualization } from '../utils'
|
||||
import {
|
||||
audioVisualizer,
|
||||
checkAndRefreshSongResource,
|
||||
supportsWebAudioVisualization,
|
||||
} from '../utils'
|
||||
|
||||
const playQueueStore = usePlayQueueStore()
|
||||
const favourites = useFavourites()
|
||||
|
@ -346,7 +350,11 @@ watch(
|
|||
watch(
|
||||
() => player.value,
|
||||
(audioElement) => {
|
||||
if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) {
|
||||
if (
|
||||
audioElement &&
|
||||
playQueueStore.list.length > 0 &&
|
||||
isAudioVisualizationSupported
|
||||
) {
|
||||
connectAudio(audioElement)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts" setup>
|
||||
|
||||
import XIcon from '../assets/icons/x.vue'
|
||||
import { usePreferences } from '../stores/usePreferences'
|
||||
import { computed } from 'vue'
|
||||
|
|
|
@ -137,7 +137,7 @@ const props = defineProps<{
|
|||
// 滚动指示器相关计算
|
||||
const scrollIndicatorHeight = computed(() => {
|
||||
if (parsedLyrics.value.length === 0) return 0
|
||||
return Math.max(10, 100 / parsedLyrics.value.length * 5) // 显示大约5行的比例
|
||||
return Math.max(10, (100 / parsedLyrics.value.length) * 5) // 显示大约5行的比例
|
||||
})
|
||||
|
||||
const scrollIndicatorPosition = computed(() => {
|
||||
|
@ -145,7 +145,11 @@ const scrollIndicatorPosition = computed(() => {
|
|||
const progress = currentLineIndex.value / (parsedLyrics.value.length - 1)
|
||||
const containerHeight = lyricsContainer.value?.clientHeight || 400
|
||||
const indicatorTrackHeight = containerHeight / 2 // 指示器轨道高度
|
||||
return progress * (indicatorTrackHeight - (scrollIndicatorHeight.value / 100 * indicatorTrackHeight))
|
||||
return (
|
||||
progress *
|
||||
(indicatorTrackHeight -
|
||||
(scrollIndicatorHeight.value / 100) * indicatorTrackHeight)
|
||||
)
|
||||
})
|
||||
|
||||
// 设置行引用
|
||||
|
@ -156,15 +160,19 @@ function setLineRef(el: HTMLElement | null, index: number) {
|
|||
}
|
||||
|
||||
// 歌词解析函数
|
||||
function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine | GapLine)[] {
|
||||
if (!lrcText) return [
|
||||
{
|
||||
type: 'lyric',
|
||||
time: 0,
|
||||
text: '',
|
||||
originalTime: '[00:00]'
|
||||
}
|
||||
]
|
||||
function parseLyrics(
|
||||
lrcText: string,
|
||||
minGapDuration: number = 5,
|
||||
): (LyricsLine | GapLine)[] {
|
||||
if (!lrcText)
|
||||
return [
|
||||
{
|
||||
type: 'lyric',
|
||||
time: 0,
|
||||
text: '',
|
||||
originalTime: '[00:00]',
|
||||
},
|
||||
]
|
||||
|
||||
const lines = lrcText.split('\n')
|
||||
const tempParsedLines: (LyricsLine | GapLine)[] = []
|
||||
|
@ -189,13 +197,13 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
|
|||
type: 'lyric',
|
||||
time: totalSeconds,
|
||||
text: text,
|
||||
originalTime: match[0]
|
||||
originalTime: match[0],
|
||||
})
|
||||
} else {
|
||||
tempParsedLines.push({
|
||||
type: 'gap',
|
||||
time: totalSeconds,
|
||||
originalTime: match[0]
|
||||
originalTime: match[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -204,14 +212,18 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
|
|||
tempParsedLines.sort((a, b) => a.time - b.time)
|
||||
|
||||
const finalLines: (LyricsLine | GapLine)[] = []
|
||||
const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
|
||||
const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
|
||||
const lyricLines = tempParsedLines.filter(
|
||||
(line) => line.type === 'lyric',
|
||||
) as LyricsLine[]
|
||||
const gapLines = tempParsedLines.filter(
|
||||
(line) => line.type === 'gap',
|
||||
) as GapLine[]
|
||||
|
||||
if (lyricLines.length === 0) return tempParsedLines
|
||||
|
||||
for (let i = 0; i < gapLines.length; i++) {
|
||||
const gapLine = gapLines[i]
|
||||
const nextLyricLine = lyricLines.find(lyric => lyric.time > gapLine.time)
|
||||
const nextLyricLine = lyricLines.find((lyric) => lyric.time > gapLine.time)
|
||||
|
||||
if (nextLyricLine) {
|
||||
const duration = nextLyricLine.time - gapLine.time
|
||||
|
@ -230,7 +242,7 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
|
|||
type: 'lyric',
|
||||
time: 0,
|
||||
text: '',
|
||||
originalTime: '[00:00]'
|
||||
originalTime: '[00:00]',
|
||||
})
|
||||
return sortedLines
|
||||
}
|
||||
|
@ -253,7 +265,12 @@ function findCurrentLineIndex(time: number): number {
|
|||
|
||||
// 使用 GSAP 滚动到指定行
|
||||
function scrollToLine(lineIndex: number, smooth = true) {
|
||||
if (!lyricsContainer.value || !lyricsWrapper.value || !lineRefs.value[lineIndex]) return
|
||||
if (
|
||||
!lyricsContainer.value ||
|
||||
!lyricsWrapper.value ||
|
||||
!lineRefs.value[lineIndex]
|
||||
)
|
||||
return
|
||||
|
||||
const container = lyricsContainer.value
|
||||
const wrapper = lyricsWrapper.value
|
||||
|
@ -277,10 +294,10 @@ function scrollToLine(lineIndex: number, smooth = true) {
|
|||
scrollTween = gsap.to(wrapper, {
|
||||
y: targetY,
|
||||
duration: 0.8,
|
||||
ease: "power2.out",
|
||||
ease: 'power2.out',
|
||||
onComplete: () => {
|
||||
scrollTween = null
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
gsap.set(wrapper, { y: targetY })
|
||||
|
@ -305,7 +322,7 @@ function highlightCurrentLine(lineIndex: number) {
|
|||
scale: 1,
|
||||
opacity: index < lineIndex ? 0.6 : 0.4,
|
||||
duration: 0.3,
|
||||
ease: "power2.out"
|
||||
ease: 'power2.out',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -315,10 +332,10 @@ function highlightCurrentLine(lineIndex: number) {
|
|||
scale: 1.05,
|
||||
opacity: 1,
|
||||
duration: 0.2,
|
||||
ease: "back.out(1.7)",
|
||||
ease: 'back.out(1.7)',
|
||||
onComplete: () => {
|
||||
highlightTween = null
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -335,7 +352,7 @@ function handleWheel(event: WheelEvent) {
|
|||
scrollTween.kill()
|
||||
}
|
||||
|
||||
const currentY = gsap.getProperty(lyricsWrapper.value, "y") as number
|
||||
const currentY = gsap.getProperty(lyricsWrapper.value, 'y') as number
|
||||
const newY = currentY - event.deltaY * 0.5
|
||||
|
||||
// 修正滚动范围计算
|
||||
|
@ -348,7 +365,7 @@ function handleWheel(event: WheelEvent) {
|
|||
gsap.to(lyricsWrapper.value, {
|
||||
y: limitedY,
|
||||
duration: 0.1,
|
||||
ease: "power2.out"
|
||||
ease: 'power2.out',
|
||||
})
|
||||
|
||||
if (userScrollTimeout) {
|
||||
|
@ -378,15 +395,16 @@ function handleLineClick(line: LyricsLine | GapLine, index: number) {
|
|||
|
||||
// 添加点击反馈动画
|
||||
if (lineRefs.value[index]) {
|
||||
gsap.fromTo(lineRefs.value[index],
|
||||
gsap.fromTo(
|
||||
lineRefs.value[index],
|
||||
{ scale: 1 },
|
||||
{
|
||||
scale: 1.1,
|
||||
duration: 0.1,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
ease: "power2.inOut"
|
||||
}
|
||||
ease: 'power2.inOut',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -398,15 +416,16 @@ function toggleAutoScroll() {
|
|||
|
||||
// 按钮点击动画
|
||||
if (controlPanel.value) {
|
||||
gsap.fromTo(controlPanel.value.children[0],
|
||||
gsap.fromTo(
|
||||
controlPanel.value.children[0],
|
||||
{ scale: 1 },
|
||||
{
|
||||
scale: 0.95,
|
||||
duration: 0.1,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
ease: "power2.inOut"
|
||||
}
|
||||
ease: 'power2.inOut',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -429,7 +448,7 @@ function resetScroll() {
|
|||
gsap.to(lyricsWrapper.value, {
|
||||
y: 0,
|
||||
duration: 0.3,
|
||||
ease: "power2.out"
|
||||
ease: 'power2.out',
|
||||
})
|
||||
|
||||
autoScroll.value = true
|
||||
|
@ -437,15 +456,16 @@ function resetScroll() {
|
|||
|
||||
// 按钮点击动画
|
||||
if (controlPanel.value) {
|
||||
gsap.fromTo(controlPanel.value.children[1],
|
||||
gsap.fromTo(
|
||||
controlPanel.value.children[1],
|
||||
{ scale: 1 },
|
||||
{
|
||||
scale: 0.95,
|
||||
duration: 0.1,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
ease: "power2.inOut"
|
||||
}
|
||||
ease: 'power2.inOut',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -471,79 +491,87 @@ function getGapDotOpacities(line: GapLine) {
|
|||
// 每个圆点的阈值
|
||||
const thresholds = [1 / 4, 2 / 4, 3 / 4]
|
||||
// 透明度从 0.3 到 1
|
||||
return thresholds.map(t => progress >= t ? 1 : progress >= t - 1 / 3 ? 0.6 : 0.3)
|
||||
return thresholds.map((t) =>
|
||||
progress >= t ? 1 : progress >= t - 1 / 3 ? 0.6 : 0.3,
|
||||
)
|
||||
}
|
||||
|
||||
// 监听播放时间变化
|
||||
watch(() => playQueueStore.currentTime, (time) => {
|
||||
const newIndex = findCurrentLineIndex(time)
|
||||
watch(
|
||||
() => playQueueStore.currentTime,
|
||||
(time) => {
|
||||
const newIndex = findCurrentLineIndex(time)
|
||||
|
||||
if (newIndex !== currentLineIndex.value && newIndex >= 0) {
|
||||
currentLineIndex.value = newIndex
|
||||
if (newIndex !== currentLineIndex.value && newIndex >= 0) {
|
||||
currentLineIndex.value = newIndex
|
||||
|
||||
// 高亮动画
|
||||
highlightCurrentLine(newIndex)
|
||||
// 高亮动画
|
||||
highlightCurrentLine(newIndex)
|
||||
|
||||
// 自动滚动
|
||||
if (autoScroll.value && !userScrolling.value) {
|
||||
nextTick(() => {
|
||||
scrollToLine(newIndex, true)
|
||||
})
|
||||
// 自动滚动
|
||||
if (autoScroll.value && !userScrolling.value) {
|
||||
nextTick(() => {
|
||||
scrollToLine(newIndex, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
// 监听歌词源变化
|
||||
watch(() => props.lrcSrc, async (newSrc) => {
|
||||
debugLyrics('加载新歌词', newSrc)
|
||||
// 重置状态
|
||||
currentLineIndex.value = -1
|
||||
lineRefs.value = []
|
||||
watch(
|
||||
() => props.lrcSrc,
|
||||
async (newSrc) => {
|
||||
debugLyrics('加载新歌词', newSrc)
|
||||
// 重置状态
|
||||
currentLineIndex.value = -1
|
||||
lineRefs.value = []
|
||||
|
||||
// 停止所有动画
|
||||
if (scrollTween) scrollTween.kill()
|
||||
if (highlightTween) highlightTween.kill()
|
||||
// 停止所有动画
|
||||
if (scrollTween) scrollTween.kill()
|
||||
if (highlightTween) highlightTween.kill()
|
||||
|
||||
if (newSrc) {
|
||||
loading.value = true
|
||||
if (newSrc) {
|
||||
loading.value = true
|
||||
|
||||
// 加载动画
|
||||
if (loadingIndicator.value) {
|
||||
gsap.fromTo(loadingIndicator.value,
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{ opacity: 1, scale: 1, duration: 0.3, ease: "back.out(1.7)" }
|
||||
)
|
||||
}
|
||||
// 加载动画
|
||||
if (loadingIndicator.value) {
|
||||
gsap.fromTo(
|
||||
loadingIndicator.value,
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{ opacity: 1, scale: 1, duration: 0.3, ease: 'back.out(1.7)' },
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get(newSrc)
|
||||
parsedLyrics.value = parseLyrics(response.data)
|
||||
debugLyrics('歌词解析完成', parsedLyrics.value)
|
||||
try {
|
||||
const response = await axios.get(newSrc)
|
||||
parsedLyrics.value = parseLyrics(response.data)
|
||||
debugLyrics('歌词解析完成', parsedLyrics.value)
|
||||
|
||||
autoScroll.value = true
|
||||
userScrolling.value = false
|
||||
autoScroll.value = true
|
||||
userScrolling.value = false
|
||||
|
||||
// 重置滚动位置
|
||||
if (lyricsWrapper.value) {
|
||||
gsap.set(lyricsWrapper.value, { y: 0 })
|
||||
}
|
||||
} catch (error) {
|
||||
debugLyrics('歌词加载失败', error)
|
||||
parsedLyrics.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
parsedLyrics.value = []
|
||||
|
||||
// 重置滚动位置
|
||||
if (lyricsWrapper.value) {
|
||||
gsap.set(lyricsWrapper.value, { y: 0 })
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
debugLyrics('歌词加载失败', error)
|
||||
parsedLyrics.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
parsedLyrics.value = []
|
||||
|
||||
// 重置滚动位置
|
||||
if (lyricsWrapper.value) {
|
||||
gsap.set(lyricsWrapper.value, { y: 0 })
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 页面焦点处理函数变量声明
|
||||
let handleVisibilityChange: (() => void) | null = null
|
||||
|
@ -562,7 +590,11 @@ function setupPageFocusHandlers() {
|
|||
|
||||
// 重新同步歌词位置
|
||||
nextTick(() => {
|
||||
if (currentLineIndex.value >= 0 && autoScroll.value && !userScrolling.value) {
|
||||
if (
|
||||
currentLineIndex.value >= 0 &&
|
||||
autoScroll.value &&
|
||||
!userScrolling.value
|
||||
) {
|
||||
scrollToLine(currentLineIndex.value, false) // 不使用动画,直接定位
|
||||
}
|
||||
})
|
||||
|
@ -579,9 +611,10 @@ onMounted(() => {
|
|||
|
||||
// 控制面板入场动画
|
||||
if (controlPanel.value) {
|
||||
gsap.fromTo(controlPanel.value,
|
||||
gsap.fromTo(
|
||||
controlPanel.value,
|
||||
{ opacity: 0, x: 20 },
|
||||
{ opacity: 0, x: 0, duration: 0.2, ease: "power2.out", delay: 0.2 }
|
||||
{ opacity: 0, x: 0, duration: 0.2, ease: 'power2.out', delay: 0.2 },
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -589,15 +622,16 @@ onMounted(() => {
|
|||
nextTick(() => {
|
||||
lineRefs.value.forEach((el, index) => {
|
||||
if (el) {
|
||||
gsap.fromTo(el,
|
||||
gsap.fromTo(
|
||||
el,
|
||||
{ opacity: 0, y: 30 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.2,
|
||||
ease: "power2.out",
|
||||
delay: index * 0.1
|
||||
}
|
||||
ease: 'power2.out',
|
||||
delay: index * 0.1,
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -621,7 +655,10 @@ defineExpose({
|
|||
scrollToLine,
|
||||
toggleAutoScroll,
|
||||
resetScroll,
|
||||
getCurrentLine: () => currentLineIndex.value >= 0 ? parsedLyrics.value[currentLineIndex.value] : null
|
||||
getCurrentLine: () =>
|
||||
currentLineIndex.value >= 0
|
||||
? parsedLyrics.value[currentLineIndex.value]
|
||||
: null,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { RouterLink } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -11,10 +11,10 @@ import StarEmptyIcon from '../assets/icons/starempty.vue'
|
|||
import StarFilledIcon from '../assets/icons/starfilled.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
album?: Album,
|
||||
track: Song,
|
||||
index: number,
|
||||
playfrom: (index: number) => void,
|
||||
album?: Album
|
||||
track: Song
|
||||
index: number
|
||||
playfrom: (index: number) => void
|
||||
}>()
|
||||
|
||||
const hover = ref(false)
|
||||
|
@ -25,7 +25,7 @@ const favourites = useFavourites()
|
|||
|
||||
function appendToQueue() {
|
||||
debugUI('添加歌曲到队列')
|
||||
let queue = playQueueStore.list
|
||||
const queue = playQueueStore.list
|
||||
queue.push({
|
||||
song: props.track,
|
||||
album: props.album,
|
||||
|
|
13
src/main.ts
13
src/main.ts
|
@ -12,18 +12,17 @@ import Playroom from './pages/Playroom.vue'
|
|||
import Library from './pages/Library.vue'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: HomePage },
|
||||
{ path: '/albums/:albumId', component: AlbumDetailView },
|
||||
{ path: '/playroom', component: Playroom },
|
||||
{ path: '/library', component: Library }
|
||||
{ path: '/', component: HomePage },
|
||||
{ path: '/albums/:albumId', component: AlbumDetailView },
|
||||
{ path: '/playroom', component: Playroom },
|
||||
{ path: '/library', component: Library },
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
routes
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
createApp(App).use(router).use(pinia).use(ToastPlugin).mount('#app')
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@ onMounted(async () => {
|
|||
try {
|
||||
let res = await apis.getAlbum(albumId as string)
|
||||
for (const track in res.songs) {
|
||||
res.songs[parseInt(track)] = await apis.getSong(res.songs[parseInt(track)].cid)
|
||||
res.songs[parseInt(track)] = await apis.getSong(
|
||||
res.songs[parseInt(track)].cid,
|
||||
)
|
||||
}
|
||||
album.value = res
|
||||
debugUI('专辑详情加载完成', res)
|
||||
|
@ -33,7 +35,13 @@ onMounted(async () => {
|
|||
|
||||
function playTheAlbum(from: number = 0) {
|
||||
if (playQueue.queueReplaceLock) {
|
||||
if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
|
||||
if (
|
||||
!confirm(
|
||||
'当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?',
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
playQueue.queueReplaceLock = false
|
||||
}
|
||||
|
||||
|
@ -42,7 +50,7 @@ function playTheAlbum(from: number = 0) {
|
|||
debugUI('添加歌曲到播放队列', track)
|
||||
newPlayQueue.push({
|
||||
song: track,
|
||||
album: album.value
|
||||
album: album.value,
|
||||
})
|
||||
}
|
||||
playQueue.playMode.shuffle = false
|
||||
|
|
|
@ -6,7 +6,7 @@ import AlbumDetailDialog from '../components/AlbumDetailDialog.vue'
|
|||
const albums = ref([] as AlbumList)
|
||||
|
||||
const presentAlbumDetailDialog = ref(false)
|
||||
const presentedAlbum = ref("")
|
||||
const presentedAlbum = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await apis.getAlbums()
|
||||
|
|
|
@ -15,10 +15,18 @@ const playQueueStore = usePlayQueueStore()
|
|||
const currentList = ref<'favourites' | number>('favourites')
|
||||
|
||||
function playTheList(list: 'favourites' | number, playFrom: number = 0) {
|
||||
if (playFrom < 0 || playFrom >= favourites.favouritesCount) { playFrom = 0 }
|
||||
if (playFrom < 0 || playFrom >= favourites.favouritesCount) {
|
||||
playFrom = 0
|
||||
}
|
||||
|
||||
if (usePlayQueueStore().queueReplaceLock) {
|
||||
if (!confirm("当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?")) { return }
|
||||
if (
|
||||
!confirm(
|
||||
'当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?',
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
usePlayQueueStore().queueReplaceLock = false
|
||||
}
|
||||
playQueueStore.list = []
|
||||
|
@ -26,9 +34,9 @@ function playTheList(list: 'favourites' | number, playFrom: number = 0) {
|
|||
if (list === 'favourites') {
|
||||
if (favourites.favouritesCount === 0) return
|
||||
|
||||
let newPlayQueue = favourites.favourites.map(item => ({
|
||||
let newPlayQueue = favourites.favourites.map((item) => ({
|
||||
song: item.song,
|
||||
album: item.album
|
||||
album: item.album,
|
||||
}))
|
||||
playQueueStore.list = newPlayQueue.slice().reverse()
|
||||
playQueueStore.currentIndex = playFrom
|
||||
|
@ -50,7 +58,6 @@ function shuffle(list: 'favourites' | number) {
|
|||
playQueueStore.isBuffering = true
|
||||
}, 100)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
||||
import { artistsOrganize } from '../utils'
|
||||
import gsap from 'gsap'
|
||||
import { Draggable } from "gsap/Draggable"
|
||||
import { Draggable } from 'gsap/Draggable'
|
||||
import { onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
|
@ -69,7 +69,7 @@ onMounted(async () => {
|
|||
const containerWidth = progressBarContainer.value?.clientWidth || 0
|
||||
const newTime = (thumbPosition / containerWidth) * playQueueStore.duration
|
||||
playQueueStore.updatedCurrentTime = newTime
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// 等待DOM完全渲染后再初始化拖拽
|
||||
|
@ -91,17 +91,24 @@ onMounted(async () => {
|
|||
|
||||
function timeFormatter(time: number) {
|
||||
const timeInSeconds = Math.floor(time)
|
||||
if (timeInSeconds < 0) { return '-:--' }
|
||||
if (timeInSeconds < 0) {
|
||||
return '-:--'
|
||||
}
|
||||
const minutes = Math.floor(timeInSeconds / 60)
|
||||
const seconds = Math.floor(timeInSeconds % 60)
|
||||
if (Number.isNaN(minutes) || Number.isNaN(seconds)) { return '-:--' }
|
||||
if (Number.isNaN(minutes) || Number.isNaN(seconds)) {
|
||||
return '-:--'
|
||||
}
|
||||
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||
}
|
||||
|
||||
// 监听播放进度,更新进度条
|
||||
watch(() => playQueueStore.currentTime, () => {
|
||||
thumbUpdate()
|
||||
})
|
||||
watch(
|
||||
() => playQueueStore.currentTime,
|
||||
() => {
|
||||
thumbUpdate()
|
||||
},
|
||||
)
|
||||
|
||||
function thumbUpdate() {
|
||||
const progress = playQueueStore.currentTime / playQueueStore.duration
|
||||
|
@ -153,7 +160,10 @@ function createVolumeDraggable() {
|
|||
const containerWidth = volumeSliderContainer.value?.clientWidth || 0
|
||||
const thumbWidth = volumeSliderThumb.value?.clientWidth || 0
|
||||
// 确保音量值在0-1之间
|
||||
const newVolume = Math.max(0, Math.min(1, thumbPosition / (containerWidth - thumbWidth)))
|
||||
const newVolume = Math.max(
|
||||
0,
|
||||
Math.min(1, thumbPosition / (containerWidth - thumbWidth)),
|
||||
)
|
||||
volume.value = newVolume
|
||||
updateAudioVolume()
|
||||
// 保存音量到localStorage
|
||||
|
@ -162,7 +172,7 @@ function createVolumeDraggable() {
|
|||
onDragEnd: () => {
|
||||
// 拖拽结束时也保存一次
|
||||
localStorage.setItem('audioVolume', volume.value.toString())
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
debugPlayroom('音量滑块拖拽创建成功')
|
||||
|
@ -208,19 +218,25 @@ function playPrevious() {
|
|||
|
||||
function setupEntranceAnimations() {
|
||||
if (controllerRef.value) {
|
||||
gsap.fromTo(controllerRef.value.children,
|
||||
gsap.fromTo(
|
||||
controllerRef.value.children,
|
||||
{ opacity: 0, y: 30, scale: 0.95 },
|
||||
{
|
||||
opacity: 1, y: 0, scale: 1,
|
||||
duration: 0.6, ease: "power2.out", stagger: 0.1
|
||||
}
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
ease: 'power2.out',
|
||||
stagger: 0.1,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (lyricsSection.value) {
|
||||
gsap.fromTo(lyricsSection.value,
|
||||
gsap.fromTo(
|
||||
lyricsSection.value,
|
||||
{ opacity: 0, x: 50 },
|
||||
{ opacity: 1, x: 0, duration: 0.8, ease: "power2.out", delay: 0.3 }
|
||||
{ opacity: 1, x: 0, duration: 0.8, ease: 'power2.out', delay: 0.3 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -228,11 +244,14 @@ function setupEntranceAnimations() {
|
|||
function handlePlayPause() {
|
||||
if (playButton.value) {
|
||||
gsap.to(playButton.value, {
|
||||
scale: 0.9, duration: 0.1, yoyo: true, repeat: 1,
|
||||
ease: "power2.inOut",
|
||||
scale: 0.9,
|
||||
duration: 0.1,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
ease: 'power2.inOut',
|
||||
onComplete: () => {
|
||||
playQueueStore.isPlaying = !playQueueStore.isPlaying
|
||||
}
|
||||
},
|
||||
})
|
||||
} else {
|
||||
playQueueStore.isPlaying = !playQueueStore.isPlaying
|
||||
|
@ -246,9 +265,15 @@ function toggleShuffle() {
|
|||
|
||||
function toggleRepeat() {
|
||||
switch (playQueueStore.playMode.repeat) {
|
||||
case 'off': playQueueStore.playMode.repeat = 'all'; break
|
||||
case 'all': playQueueStore.playMode.repeat = 'single'; break
|
||||
case 'single': playQueueStore.playMode.repeat = 'off'; break
|
||||
case 'off':
|
||||
playQueueStore.playMode.repeat = 'all'
|
||||
break
|
||||
case 'all':
|
||||
playQueueStore.playMode.repeat = 'single'
|
||||
break
|
||||
case 'single':
|
||||
playQueueStore.playMode.repeat = 'off'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,15 +285,26 @@ function makePlayQueueListPresent() {
|
|||
|
||||
const tl = gsap.timeline()
|
||||
tl.to(playQueueDialogContainer.value, {
|
||||
backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
|
||||
}).to(playQueueDialog.value, {
|
||||
x: 0, duration: 0.4, ease: 'power3.out'
|
||||
}, '<0.1')
|
||||
backgroundColor: '#17171780',
|
||||
duration: 0.3,
|
||||
ease: 'power2.out',
|
||||
}).to(
|
||||
playQueueDialog.value,
|
||||
{
|
||||
x: 0,
|
||||
duration: 0.4,
|
||||
ease: 'power3.out',
|
||||
},
|
||||
'<0.1',
|
||||
)
|
||||
|
||||
if (playQueueDialog.value.children.length > 0) {
|
||||
tl.fromTo(playQueueDialog.value.children,
|
||||
tl.fromTo(
|
||||
playQueueDialog.value.children,
|
||||
{ opacity: 0, x: -20 },
|
||||
{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 }, '<0.2')
|
||||
{ opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 },
|
||||
'<0.2',
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -286,23 +322,40 @@ function makePlayQueueListDismiss() {
|
|||
gsap.set(playQueueDialog.value, { x: -384 })
|
||||
}
|
||||
if (playQueueDialogContainer.value) {
|
||||
gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' })
|
||||
gsap.set(playQueueDialogContainer.value, {
|
||||
backgroundColor: 'transparent',
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (playQueueDialog.value.children.length > 0) {
|
||||
tl.to(playQueueDialog.value.children, {
|
||||
opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03
|
||||
opacity: 0,
|
||||
x: -20,
|
||||
duration: 0.2,
|
||||
ease: 'power2.in',
|
||||
stagger: 0.03,
|
||||
})
|
||||
}
|
||||
|
||||
tl.to(playQueueDialog.value, {
|
||||
x: -384, duration: 0.3, ease: 'power2.in'
|
||||
}, playQueueDialog.value.children.length > 0 ? '<0.1' : '0')
|
||||
.to(playQueueDialogContainer.value, {
|
||||
backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in'
|
||||
}, '<')
|
||||
tl.to(
|
||||
playQueueDialog.value,
|
||||
{
|
||||
x: -384,
|
||||
duration: 0.3,
|
||||
ease: 'power2.in',
|
||||
},
|
||||
playQueueDialog.value.children.length > 0 ? '<0.1' : '0',
|
||||
).to(
|
||||
playQueueDialogContainer.value,
|
||||
{
|
||||
backgroundColor: 'transparent',
|
||||
duration: 0.2,
|
||||
ease: 'power2.in',
|
||||
},
|
||||
'<',
|
||||
)
|
||||
}
|
||||
|
||||
function getCurrentTrack() {
|
||||
|
@ -319,15 +372,23 @@ function toggleMoreOptions() {
|
|||
nextTick(() => {
|
||||
if (moreOptionsDialog.value) {
|
||||
const tl = gsap.timeline()
|
||||
tl.fromTo(moreOptionsDialog.value,
|
||||
tl.fromTo(
|
||||
moreOptionsDialog.value,
|
||||
{ opacity: 0, scale: 0.9, y: 10 },
|
||||
{ opacity: 1, scale: 1, y: 0, duration: 0.2, ease: "power2.out" }
|
||||
{ opacity: 1, scale: 1, y: 0, duration: 0.2, ease: 'power2.out' },
|
||||
)
|
||||
if (moreOptionsDialog.value.children[0]?.children) {
|
||||
tl.fromTo(moreOptionsDialog.value.children[0].children,
|
||||
tl.fromTo(
|
||||
moreOptionsDialog.value.children[0].children,
|
||||
{ opacity: 0, x: -10 },
|
||||
{ opacity: 1, x: 0, duration: 0.15, ease: "power2.out", stagger: 0.05 },
|
||||
"<0.1"
|
||||
{
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
duration: 0.15,
|
||||
ease: 'power2.out',
|
||||
stagger: 0.05,
|
||||
},
|
||||
'<0.1',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -337,16 +398,21 @@ function toggleMoreOptions() {
|
|||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
showMoreOptions.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
if (moreOptionsDialog.value.children[0]?.children) {
|
||||
tl.to(moreOptionsDialog.value.children[0].children,
|
||||
{ opacity: 0, x: -10, duration: 0.1, ease: "power2.in", stagger: 0.02 }
|
||||
)
|
||||
tl.to(moreOptionsDialog.value.children[0].children, {
|
||||
opacity: 0,
|
||||
x: -10,
|
||||
duration: 0.1,
|
||||
ease: 'power2.in',
|
||||
stagger: 0.02,
|
||||
})
|
||||
}
|
||||
tl.to(moreOptionsDialog.value,
|
||||
{ opacity: 0, scale: 0.9, y: 10, duration: 0.15, ease: "power2.in" },
|
||||
moreOptionsDialog.value.children[0]?.children ? "<0.05" : "0"
|
||||
tl.to(
|
||||
moreOptionsDialog.value,
|
||||
{ opacity: 0, scale: 0.9, y: 10, duration: 0.15, ease: 'power2.in' },
|
||||
moreOptionsDialog.value.children[0]?.children ? '<0.05' : '0',
|
||||
)
|
||||
} else {
|
||||
showMoreOptions.value = false
|
||||
|
@ -354,71 +420,90 @@ function toggleMoreOptions() {
|
|||
}
|
||||
}
|
||||
|
||||
watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newValue, oldValue) => {
|
||||
if (!getCurrentTrack()) { return }
|
||||
|
||||
const [showLyrics, hasLyricUrl] = newValue
|
||||
const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
|
||||
|
||||
// Show lyrics when both conditions are met
|
||||
if (showLyrics && hasLyricUrl) {
|
||||
presentLyrics.value = true
|
||||
nextTick(() => {
|
||||
const tl = gsap.timeline()
|
||||
tl.from(controllerRef.value, {
|
||||
marginRight: '-40rem',
|
||||
}).fromTo(lyricsSection.value,
|
||||
{ opacity: 0, x: 50, scale: 0.95 },
|
||||
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
|
||||
"-=0.3"
|
||||
)
|
||||
})
|
||||
}
|
||||
// Hide lyrics with different animations based on reason
|
||||
else if (presentLyrics.value) {
|
||||
let animationConfig
|
||||
|
||||
// If lyrics were toggled off
|
||||
if (prevShowLyrics && !showLyrics) {
|
||||
animationConfig = {
|
||||
opacity: 0, x: -50, scale: 0.95,
|
||||
duration: 0.3, ease: "power2.in"
|
||||
}
|
||||
}
|
||||
// If no lyrics available (song changed)
|
||||
else if (!hasLyricUrl) {
|
||||
animationConfig = {
|
||||
opacity: 0, y: -20, scale: 0.98,
|
||||
duration: 0.3, ease: "power1.in"
|
||||
}
|
||||
}
|
||||
// Default animation
|
||||
else {
|
||||
animationConfig = {
|
||||
opacity: 0, x: -50,
|
||||
duration: 0.3, ease: "power2.in"
|
||||
}
|
||||
watch(
|
||||
() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl],
|
||||
(newValue, oldValue) => {
|
||||
if (!getCurrentTrack()) {
|
||||
return
|
||||
}
|
||||
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
presentLyrics.value = false
|
||||
}
|
||||
})
|
||||
const [showLyrics, hasLyricUrl] = newValue
|
||||
const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
|
||||
|
||||
tl.to(controllerRef.value, {
|
||||
marginLeft: '44rem',
|
||||
duration: 0.3, ease: "power2.out"
|
||||
})
|
||||
.to(lyricsSection.value, animationConfig, '<')
|
||||
.set(lyricsSection.value, {
|
||||
opacity: 1, x: 0, y: 0, scale: 1 // Reset for next time
|
||||
// Show lyrics when both conditions are met
|
||||
if (showLyrics && hasLyricUrl) {
|
||||
presentLyrics.value = true
|
||||
nextTick(() => {
|
||||
const tl = gsap.timeline()
|
||||
tl.from(controllerRef.value, {
|
||||
marginRight: '-40rem',
|
||||
}).fromTo(
|
||||
lyricsSection.value,
|
||||
{ opacity: 0, x: 50, scale: 0.95 },
|
||||
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: 'power2.out' },
|
||||
'-=0.3',
|
||||
)
|
||||
})
|
||||
.set(controllerRef.value, {
|
||||
marginLeft: '0rem' // Reset for next time
|
||||
}
|
||||
// Hide lyrics with different animations based on reason
|
||||
else if (presentLyrics.value) {
|
||||
let animationConfig
|
||||
|
||||
// If lyrics were toggled off
|
||||
if (prevShowLyrics && !showLyrics) {
|
||||
animationConfig = {
|
||||
opacity: 0,
|
||||
x: -50,
|
||||
scale: 0.95,
|
||||
duration: 0.3,
|
||||
ease: 'power2.in',
|
||||
}
|
||||
}
|
||||
// If no lyrics available (song changed)
|
||||
else if (!hasLyricUrl) {
|
||||
animationConfig = {
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
scale: 0.98,
|
||||
duration: 0.3,
|
||||
ease: 'power1.in',
|
||||
}
|
||||
}
|
||||
// Default animation
|
||||
else {
|
||||
animationConfig = {
|
||||
opacity: 0,
|
||||
x: -50,
|
||||
duration: 0.3,
|
||||
ease: 'power2.in',
|
||||
}
|
||||
}
|
||||
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
presentLyrics.value = false
|
||||
},
|
||||
})
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
tl.to(controllerRef.value, {
|
||||
marginLeft: '44rem',
|
||||
duration: 0.3,
|
||||
ease: 'power2.out',
|
||||
})
|
||||
.to(lyricsSection.value, animationConfig, '<')
|
||||
.set(lyricsSection.value, {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1, // Reset for next time
|
||||
})
|
||||
.set(controllerRef.value, {
|
||||
marginLeft: '0rem', // Reset for next time
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 页面焦点处理函数变量声明
|
||||
let handleVisibilityChange: (() => void) | null = null
|
||||
|
@ -464,7 +549,9 @@ function setupPageFocusHandlers() {
|
|||
// 重新同步歌词状态
|
||||
function resyncLyricsState() {
|
||||
const currentTrack = getCurrentTrack()
|
||||
if (!currentTrack) { return }
|
||||
if (!currentTrack) {
|
||||
return
|
||||
}
|
||||
|
||||
debugPlayroom('重新同步歌词状态')
|
||||
|
||||
|
@ -472,7 +559,7 @@ function resyncLyricsState() {
|
|||
if (controllerRef.value) {
|
||||
gsap.set(controllerRef.value, {
|
||||
marginLeft: '0rem',
|
||||
marginRight: '0rem'
|
||||
marginRight: '0rem',
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -481,15 +568,18 @@ function resyncLyricsState() {
|
|||
opacity: 1,
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1
|
||||
scale: 1,
|
||||
})
|
||||
}
|
||||
|
||||
// 检查当前歌词显示状态应该是什么
|
||||
const shouldShowLyrics = preferences.presentLyrics && currentTrack.song.lyricUrl ? true : false
|
||||
const shouldShowLyrics =
|
||||
preferences.presentLyrics && currentTrack.song.lyricUrl ? true : false
|
||||
|
||||
if (shouldShowLyrics !== presentLyrics.value) {
|
||||
debugPlayroom(`歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`)
|
||||
debugPlayroom(
|
||||
`歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`,
|
||||
)
|
||||
|
||||
// 直接设置状态,不触发动画
|
||||
presentLyrics.value = shouldShowLyrics
|
||||
|
@ -501,11 +591,12 @@ function resyncLyricsState() {
|
|||
tl.from(controllerRef.value, {
|
||||
marginRight: '-40rem',
|
||||
duration: 0.4,
|
||||
ease: "power2.out"
|
||||
}).fromTo(lyricsSection.value,
|
||||
ease: 'power2.out',
|
||||
}).fromTo(
|
||||
lyricsSection.value,
|
||||
{ opacity: 0, x: 50, scale: 0.95 },
|
||||
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
|
||||
"-=0.2"
|
||||
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: 'power2.out' },
|
||||
'-=0.2',
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -513,21 +604,29 @@ function resyncLyricsState() {
|
|||
}
|
||||
|
||||
// New: Watch for track changes and animate
|
||||
watch(() => playQueueStore.currentIndex, () => {
|
||||
if (albumCover.value) {
|
||||
gsap.to(albumCover.value, {
|
||||
scale: 0.95, opacity: 0.7, duration: 0.2,
|
||||
ease: "power2.inOut", yoyo: true, repeat: 1
|
||||
})
|
||||
}
|
||||
watch(
|
||||
() => playQueueStore.currentIndex,
|
||||
() => {
|
||||
if (albumCover.value) {
|
||||
gsap.to(albumCover.value, {
|
||||
scale: 0.95,
|
||||
opacity: 0.7,
|
||||
duration: 0.2,
|
||||
ease: 'power2.inOut',
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
})
|
||||
}
|
||||
|
||||
if (songInfo.value) {
|
||||
gsap.fromTo(songInfo.value,
|
||||
{ opacity: 0, y: 10 },
|
||||
{ opacity: 1, y: 0, duration: 0.4, ease: "power2.out", delay: 0.3 }
|
||||
)
|
||||
}
|
||||
})
|
||||
if (songInfo.value) {
|
||||
gsap.fromTo(
|
||||
songInfo.value,
|
||||
{ opacity: 0, y: 10 },
|
||||
{ opacity: 1, y: 0, duration: 0.4, ease: 'power2.out', delay: 0.3 },
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { ref, watch, computed } from "vue"
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { debugStore } from '../utils/debug'
|
||||
|
||||
// 声明全局类型
|
||||
|
@ -22,7 +22,11 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
const detectAvailableAPIs = () => {
|
||||
// 检查原生 chrome API
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local) {
|
||||
if (
|
||||
typeof chrome !== 'undefined' &&
|
||||
chrome.storage &&
|
||||
chrome.storage.local
|
||||
) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -32,7 +36,11 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
|
||||
// 检查 window.chrome
|
||||
try {
|
||||
if (window.chrome && window.chrome.storage && window.chrome.storage.local) {
|
||||
if (
|
||||
window.chrome &&
|
||||
window.chrome.storage &&
|
||||
window.chrome.storage.local
|
||||
) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -132,50 +140,62 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
const normalizeFavourites = (data: any[]): QueueItem[] => {
|
||||
if (!Array.isArray(data)) return []
|
||||
|
||||
return data.map(item => {
|
||||
if (!item || !item.song) return null
|
||||
return data
|
||||
.map((item) => {
|
||||
if (!item || !item.song) return null
|
||||
|
||||
// 规范化 Song 对象
|
||||
const song: Song = {
|
||||
cid: item.song.cid || '',
|
||||
name: item.song.name || '',
|
||||
albumCid: item.song.albumCid,
|
||||
sourceUrl: item.song.sourceUrl,
|
||||
lyricUrl: item.song.lyricUrl,
|
||||
mvUrl: item.song.mvUrl,
|
||||
mvCoverUrl: item.song.mvCoverUrl,
|
||||
// 确保 artistes 和 artists 是数组
|
||||
artistes: Array.isArray(item.song.artistes) ? item.song.artistes :
|
||||
typeof item.song.artistes === 'object' ? Object.values(item.song.artistes) :
|
||||
[],
|
||||
artists: Array.isArray(item.song.artists) ? item.song.artists :
|
||||
typeof item.song.artists === 'object' ? Object.values(item.song.artists) :
|
||||
[]
|
||||
}
|
||||
// 规范化 Song 对象
|
||||
const song: Song = {
|
||||
cid: item.song.cid || '',
|
||||
name: item.song.name || '',
|
||||
albumCid: item.song.albumCid,
|
||||
sourceUrl: item.song.sourceUrl,
|
||||
lyricUrl: item.song.lyricUrl,
|
||||
mvUrl: item.song.mvUrl,
|
||||
mvCoverUrl: item.song.mvCoverUrl,
|
||||
// 确保 artistes 和 artists 是数组
|
||||
artistes: Array.isArray(item.song.artistes)
|
||||
? item.song.artistes
|
||||
: typeof item.song.artistes === 'object'
|
||||
? Object.values(item.song.artistes)
|
||||
: [],
|
||||
artists: Array.isArray(item.song.artists)
|
||||
? item.song.artists
|
||||
: typeof item.song.artists === 'object'
|
||||
? Object.values(item.song.artists)
|
||||
: [],
|
||||
}
|
||||
|
||||
// 规范化 Album 对象(如果存在)
|
||||
const album = item.album ? {
|
||||
cid: item.album.cid || '',
|
||||
name: item.album.name || '',
|
||||
intro: item.album.intro,
|
||||
belong: item.album.belong,
|
||||
coverUrl: item.album.coverUrl || '',
|
||||
coverDeUrl: item.album.coverDeUrl,
|
||||
artistes: Array.isArray(item.album.artistes) ? item.album.artistes :
|
||||
typeof item.album.artistes === 'object' ? Object.values(item.album.artistes) :
|
||||
[],
|
||||
songs: item.album.songs
|
||||
} : undefined
|
||||
// 规范化 Album 对象(如果存在)
|
||||
const album = item.album
|
||||
? {
|
||||
cid: item.album.cid || '',
|
||||
name: item.album.name || '',
|
||||
intro: item.album.intro,
|
||||
belong: item.album.belong,
|
||||
coverUrl: item.album.coverUrl || '',
|
||||
coverDeUrl: item.album.coverDeUrl,
|
||||
artistes: Array.isArray(item.album.artistes)
|
||||
? item.album.artistes
|
||||
: typeof item.album.artistes === 'object'
|
||||
? Object.values(item.album.artistes)
|
||||
: [],
|
||||
songs: item.album.songs,
|
||||
}
|
||||
: undefined
|
||||
|
||||
return { song, album }
|
||||
}).filter(Boolean) as QueueItem[]
|
||||
return { song, album }
|
||||
})
|
||||
.filter(Boolean) as QueueItem[]
|
||||
}
|
||||
|
||||
// 获取收藏列表
|
||||
const getFavourites = async () => {
|
||||
const result = await getStoredValue('favourites', defaultFavourites)
|
||||
// 确保返回的是数组并进行数据规范化
|
||||
const normalizedResult = Array.isArray(result) ? normalizeFavourites(result) : defaultFavourites
|
||||
const normalizedResult = Array.isArray(result)
|
||||
? normalizeFavourites(result)
|
||||
: defaultFavourites
|
||||
return normalizedResult
|
||||
}
|
||||
|
||||
|
@ -188,7 +208,7 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
|
||||
// 检查歌曲是否已收藏
|
||||
const isFavourite = (songCid: string): boolean => {
|
||||
return favourites.value.some(item => item.song.cid === songCid)
|
||||
return favourites.value.some((item) => item.song.cid === songCid)
|
||||
}
|
||||
|
||||
// 添加到收藏
|
||||
|
@ -209,7 +229,9 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
|
||||
// 从收藏中移除
|
||||
const removeFromFavourites = async (songCid: string) => {
|
||||
const index = favourites.value.findIndex(item => item.song.cid === songCid)
|
||||
const index = favourites.value.findIndex(
|
||||
(item) => item.song.cid === songCid,
|
||||
)
|
||||
if (index !== -1) {
|
||||
const removedItem = favourites.value.splice(index, 1)[0]
|
||||
if (isLoaded.value) {
|
||||
|
@ -266,29 +288,38 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
|
||||
// 监听变化并保存(防抖处理)
|
||||
let saveTimeout: NodeJS.Timeout | null = null
|
||||
watch(favourites, async () => {
|
||||
if (isLoaded.value) {
|
||||
// 清除之前的定时器
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout)
|
||||
}
|
||||
// 设置新的定时器,防抖保存
|
||||
saveTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await saveFavourites()
|
||||
} catch (error) {
|
||||
// Silent fail
|
||||
watch(
|
||||
favourites,
|
||||
async () => {
|
||||
if (isLoaded.value) {
|
||||
// 清除之前的定时器
|
||||
if (saveTimeout) {
|
||||
clearTimeout(saveTimeout)
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}, { deep: true })
|
||||
// 设置新的定时器,防抖保存
|
||||
saveTimeout = setTimeout(async () => {
|
||||
try {
|
||||
await saveFavourites()
|
||||
} catch (error) {
|
||||
// Silent fail
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// 更新收藏列表中的歌曲信息
|
||||
const updateSongInFavourites = async (songCid: string, updatedSong: Song) => {
|
||||
const index = favourites.value.findIndex(item => item.song.cid === songCid)
|
||||
const index = favourites.value.findIndex(
|
||||
(item) => item.song.cid === songCid,
|
||||
)
|
||||
if (index !== -1) {
|
||||
// 更新歌曲信息,保持其他属性不变
|
||||
favourites.value[index].song = { ...favourites.value[index].song, ...updatedSong }
|
||||
favourites.value[index].song = {
|
||||
...favourites.value[index].song,
|
||||
...updatedSong,
|
||||
}
|
||||
if (isLoaded.value) {
|
||||
try {
|
||||
await saveFavourites()
|
||||
|
@ -318,7 +349,6 @@ export const useFavourites = defineStore('favourites', () => {
|
|||
clearFavourites,
|
||||
getStoredValue,
|
||||
setStoredValue,
|
||||
updateSongInFavourites
|
||||
updateSongInFavourites,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
// 暴露给外部的响应式只读引用
|
||||
const queueState = computed(() =>
|
||||
// 按 queueOrder 的顺序排序输出队列
|
||||
queueOrder.value.map(index => queue.value[index]).filter(Boolean)
|
||||
queueOrder.value
|
||||
.map((index) => queue.value[index])
|
||||
.filter(Boolean),
|
||||
)
|
||||
const shuffleState = computed(() => isShuffle.value)
|
||||
const loopModeState = computed(() => loopingMode.value)
|
||||
|
@ -26,13 +28,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
})
|
||||
|
||||
/************
|
||||
* 播放队列相关
|
||||
***********/
|
||||
* 播放队列相关
|
||||
***********/
|
||||
// 使用新队列替换老队列
|
||||
// 队列替换锁开启时启用确认,确认后重置该锁
|
||||
async function replaceQueue(songs: Song[]) {
|
||||
if (queueReplaceLock.value) {
|
||||
if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
|
||||
if (
|
||||
!confirm(
|
||||
'当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?',
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// 重置队列替换锁
|
||||
queueReplaceLock.value = false
|
||||
}
|
||||
|
@ -47,13 +55,14 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
newQueue[newQueue.length] = {
|
||||
song: songs[i],
|
||||
album: await (async () => {
|
||||
if (albums[songs[i].albumCid ?? "0"]) return albums[songs[i].albumCid ?? "0"]
|
||||
if (albums[songs[i].albumCid ?? '0'])
|
||||
return albums[songs[i].albumCid ?? '0']
|
||||
else {
|
||||
const album = await apis.getAlbum(songs[i].albumCid ?? "0")
|
||||
albums[songs[i].albumCid ?? "0"] = album
|
||||
const album = await apis.getAlbum(songs[i].albumCid ?? '0')
|
||||
albums[songs[i].albumCid ?? '0'] = album
|
||||
return album
|
||||
}
|
||||
})()
|
||||
})(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,8 +79,8 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
}
|
||||
|
||||
/************
|
||||
* 播放模式相关
|
||||
**********/
|
||||
* 播放模式相关
|
||||
**********/
|
||||
// 切换随机播放模式
|
||||
const toggleShuffle = (turnTo?: boolean) => {
|
||||
// 未指定随机状态时自动开关
|
||||
|
@ -122,19 +131,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
// 切换循环播放模式
|
||||
const toggleLoop = (mode?: 'single' | 'all' | 'off') => {
|
||||
// 如果指定了循环模式
|
||||
if (mode) return loopingMode.value = mode
|
||||
if (mode) return (loopingMode.value = mode)
|
||||
|
||||
// 如果没有指定,那么按照「无 -> 列表循环 -> 单曲循环」的顺序轮换
|
||||
switch (loopingMode.value) {
|
||||
case 'off':
|
||||
loopingMode.value = 'all'
|
||||
break
|
||||
case 'all':
|
||||
loopingMode.value = 'single'
|
||||
break
|
||||
case 'single':
|
||||
loopingMode.value = 'off'
|
||||
break
|
||||
case 'off':
|
||||
loopingMode.value = 'all'
|
||||
break
|
||||
case 'all':
|
||||
loopingMode.value = 'single'
|
||||
break
|
||||
case 'single':
|
||||
loopingMode.value = 'off'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,6 +158,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
|||
// 修改方法
|
||||
replaceQueue,
|
||||
toggleShuffle,
|
||||
toggleLoop
|
||||
toggleLoop,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { ref, watch } from "vue"
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
// 声明全局类型
|
||||
declare global {
|
||||
|
@ -20,14 +20,18 @@ export const usePreferences = defineStore('preferences', () => {
|
|||
const defaultPreferences = {
|
||||
displayTimeLeft: false,
|
||||
presentLyrics: false,
|
||||
autoRedirect: true
|
||||
autoRedirect: true,
|
||||
}
|
||||
|
||||
// 检测可用的 API
|
||||
const detectAvailableAPIs = () => {
|
||||
// 检查原生 chrome API
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||
if (
|
||||
typeof chrome !== 'undefined' &&
|
||||
chrome.storage &&
|
||||
chrome.storage.sync
|
||||
) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -37,7 +41,11 @@ export const usePreferences = defineStore('preferences', () => {
|
|||
|
||||
// 检查 window.chrome
|
||||
try {
|
||||
if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
|
||||
if (
|
||||
window.chrome &&
|
||||
window.chrome.storage &&
|
||||
window.chrome.storage.sync
|
||||
) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -143,7 +151,7 @@ export const usePreferences = defineStore('preferences', () => {
|
|||
const preferences = {
|
||||
displayTimeLeft: displayTimeLeft.value,
|
||||
presentLyrics: presentLyrics.value,
|
||||
autoRedirect: autoRedirect.value
|
||||
autoRedirect: autoRedirect.value,
|
||||
}
|
||||
await setStoredValue('preferences', preferences)
|
||||
}
|
||||
|
@ -188,6 +196,6 @@ export const usePreferences = defineStore('preferences', () => {
|
|||
getStoredValue,
|
||||
setStoredValue,
|
||||
getPreferences,
|
||||
savePreferences
|
||||
savePreferences,
|
||||
}
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { ref } from "vue"
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { debugStore } from '../utils/debug'
|
||||
|
||||
// 声明全局类型
|
||||
|
@ -27,7 +27,11 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
|
|||
const detectAvailableAPIs = () => {
|
||||
// 检查原生 chrome API
|
||||
try {
|
||||
if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
|
||||
if (
|
||||
typeof chrome !== 'undefined' &&
|
||||
chrome.storage &&
|
||||
chrome.storage.sync
|
||||
) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -37,7 +41,11 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
|
|||
|
||||
// 检查 window.chrome
|
||||
try {
|
||||
if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
|
||||
if (
|
||||
window.chrome &&
|
||||
window.chrome.storage &&
|
||||
window.chrome.storage.sync
|
||||
) {
|
||||
storageType.value = 'chrome'
|
||||
return 'chrome'
|
||||
}
|
||||
|
@ -144,7 +152,10 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
|
|||
}
|
||||
|
||||
// 获取上次显示弹窗的版本号
|
||||
const lastShownVersion = await getStoredValue('lastUpdatePopupVersion', '')
|
||||
const lastShownVersion = await getStoredValue(
|
||||
'lastUpdatePopupVersion',
|
||||
'',
|
||||
)
|
||||
|
||||
// 如果版本号不同,需要显示弹窗并更新存储的版本号
|
||||
if (lastShownVersion !== currentVersion) {
|
||||
|
@ -200,6 +211,6 @@ export const useUpdatePopup = defineStore('updatePopup', () => {
|
|||
getLastShownVersion,
|
||||
initializeUpdatePopup,
|
||||
getStoredValue,
|
||||
setStoredValue
|
||||
setStoredValue,
|
||||
}
|
||||
})
|
|
@ -1,15 +1,15 @@
|
|||
@import "tailwindcss";
|
||||
/* 导入来自 /assets/MiSans_VF.ttf 的字体 */
|
||||
@font-face {
|
||||
font-family: 'MiSans';
|
||||
src: url('/assets/MiSans_VF.ttf') format('truetype-variations');
|
||||
font-family: "MiSans";
|
||||
src: url("/assets/MiSans_VF.ttf") format("truetype-variations");
|
||||
font-weight: 1 999;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Alte DIN';
|
||||
src: url('/assets/din1451alt.ttf') format('truetype-variations');
|
||||
font-family: "Alte DIN";
|
||||
src: url("/assets/din1451alt.ttf") format("truetype-variations");
|
||||
font-weight: 1 999;
|
||||
font-display: swap;
|
||||
}
|
||||
|
@ -27,5 +27,5 @@ input {
|
|||
}
|
||||
|
||||
.track_num {
|
||||
font-family: 'DIN Alternate', 'Alte DIN' !important;
|
||||
font-family: "DIN Alternate", "Alte DIN" !important;
|
||||
}
|
|
@ -1,6 +1,10 @@
|
|||
export default (list: string[]) => {
|
||||
if (list.length === 0) { return '未知音乐人' }
|
||||
return list.map((artist) => {
|
||||
return artist
|
||||
}).join(' / ')
|
||||
if (list.length === 0) {
|
||||
return '未知音乐人'
|
||||
}
|
||||
return list
|
||||
.map((artist) => {
|
||||
return artist
|
||||
})
|
||||
.join(' / ')
|
||||
}
|
|
@ -3,377 +3,417 @@ import { ref, onUnmounted, Ref } from 'vue'
|
|||
import { debugVisualizer } from './debug'
|
||||
|
||||
interface AudioVisualizerOptions {
|
||||
sensitivity?: number
|
||||
smoothing?: number
|
||||
barCount?: number
|
||||
debug?: boolean
|
||||
bassBoost?: number // 低音增强倍数 (默认 0.7,降低低音)
|
||||
midBoost?: number // 中音增强倍数 (默认 1.2)
|
||||
trebleBoost?: number // 高音增强倍数 (默认 1.5)
|
||||
threshold?: number // 响度门槛 (0-255,默认 15)
|
||||
minHeight?: number // 最小高度百分比 (默认 0)
|
||||
maxDecibels?: number // 最大分贝门槛 (默认 -10,越大越难顶满)
|
||||
sensitivity?: number
|
||||
smoothing?: number
|
||||
barCount?: number
|
||||
debug?: boolean
|
||||
bassBoost?: number // 低音增强倍数 (默认 0.7,降低低音)
|
||||
midBoost?: number // 中音增强倍数 (默认 1.2)
|
||||
trebleBoost?: number // 高音增强倍数 (默认 1.5)
|
||||
threshold?: number // 响度门槛 (0-255,默认 15)
|
||||
minHeight?: number // 最小高度百分比 (默认 0)
|
||||
maxDecibels?: number // 最大分贝门槛 (默认 -10,越大越难顶满)
|
||||
}
|
||||
|
||||
export function audioVisualizer(options: AudioVisualizerOptions = {}) {
|
||||
const {
|
||||
sensitivity = 1,
|
||||
smoothing = 0.7,
|
||||
barCount = 4,
|
||||
debug = false,
|
||||
bassBoost = 0.7, // 降低低音权重
|
||||
midBoost = 1.2, // 提升中音
|
||||
trebleBoost = 1.5, // 提升高音
|
||||
threshold = 15, // 响度门槛,低于此值不产生波动
|
||||
minHeight = 0 // 最小高度百分比
|
||||
} = options
|
||||
const {
|
||||
sensitivity = 1,
|
||||
smoothing = 0.7,
|
||||
barCount = 4,
|
||||
debug = false,
|
||||
bassBoost = 0.7, // 降低低音权重
|
||||
midBoost = 1.2, // 提升中音
|
||||
trebleBoost = 1.5, // 提升高音
|
||||
threshold = 15, // 响度门槛,低于此值不产生波动
|
||||
minHeight = 0, // 最小高度百分比
|
||||
} = options
|
||||
|
||||
debugVisualizer('初始化平衡频谱', options)
|
||||
debugVisualizer('初始化平衡频谱', options)
|
||||
|
||||
// 导出的竖杠高度值数组 (0-100)
|
||||
const barHeights: Ref<number[]> = ref(Array(barCount).fill(0))
|
||||
const isAnalyzing = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
// 导出的竖杠高度值数组 (0-100)
|
||||
const barHeights: Ref<number[]> = ref(Array(barCount).fill(0))
|
||||
const isAnalyzing = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
|
||||
// 内部变量
|
||||
let audioContext: AudioContext | null = null
|
||||
let analyser: AnalyserNode | null = null
|
||||
let source: MediaElementAudioSourceNode | null = null
|
||||
let dataArray: Uint8Array | null = null
|
||||
let animationId: number | null = null
|
||||
let currentAudioElement: HTMLAudioElement | null = null
|
||||
// 内部变量
|
||||
let audioContext: AudioContext | null = null
|
||||
let analyser: AnalyserNode | null = null
|
||||
let source: MediaElementAudioSourceNode | null = null
|
||||
let dataArray: Uint8Array | null = null
|
||||
let animationId: number | null = null
|
||||
let currentAudioElement: HTMLAudioElement | null = null
|
||||
|
||||
// 调试日志
|
||||
function log(...args: any[]) {
|
||||
if (debug) {
|
||||
debugVisualizer(...args)
|
||||
}
|
||||
}
|
||||
// 调试日志
|
||||
function log(...args: any[]) {
|
||||
if (debug) {
|
||||
debugVisualizer(...args)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化音频分析
|
||||
function initAudioContext(audioElement: HTMLAudioElement) {
|
||||
if (!audioElement) {
|
||||
log('错误: 音频元素为空')
|
||||
return
|
||||
}
|
||||
// 初始化音频分析
|
||||
function initAudioContext(audioElement: HTMLAudioElement) {
|
||||
if (!audioElement) {
|
||||
log('错误: 音频元素为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (audioContext) {
|
||||
log('AudioContext 已存在,跳过初始化')
|
||||
return
|
||||
}
|
||||
if (audioContext) {
|
||||
log('AudioContext 已存在,跳过初始化')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
log('开始初始化音频上下文...')
|
||||
try {
|
||||
log('开始初始化音频上下文...')
|
||||
|
||||
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
|
||||
log('AudioContext 创建成功, 状态:', audioContext.state, '采样率:', audioContext.sampleRate)
|
||||
audioContext = new (
|
||||
window.AudioContext || (window as any).webkitAudioContext
|
||||
)()
|
||||
log(
|
||||
'AudioContext 创建成功, 状态:',
|
||||
audioContext.state,
|
||||
'采样率:',
|
||||
audioContext.sampleRate,
|
||||
)
|
||||
|
||||
// 如果上下文被暂停,尝试恢复
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume().then(() => {
|
||||
log('AudioContext 已恢复')
|
||||
})
|
||||
}
|
||||
// 如果上下文被暂停,尝试恢复
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume().then(() => {
|
||||
log('AudioContext 已恢复')
|
||||
})
|
||||
}
|
||||
|
||||
analyser = audioContext.createAnalyser()
|
||||
analyser = audioContext.createAnalyser()
|
||||
|
||||
// 尝试创建音频源
|
||||
try {
|
||||
source = audioContext.createMediaElementSource(audioElement)
|
||||
log('MediaElementSource 创建成功')
|
||||
} catch (sourceError) {
|
||||
log('创建 MediaElementSource 失败:', sourceError)
|
||||
error.value = 'CORS 错误: 无法访问跨域音频'
|
||||
return
|
||||
}
|
||||
// 尝试创建音频源
|
||||
try {
|
||||
source = audioContext.createMediaElementSource(audioElement)
|
||||
log('MediaElementSource 创建成功')
|
||||
} catch (sourceError) {
|
||||
log('创建 MediaElementSource 失败:', sourceError)
|
||||
error.value = 'CORS 错误: 无法访问跨域音频'
|
||||
return
|
||||
}
|
||||
|
||||
// 优化分析器配置
|
||||
analyser.fftSize = 2048 // 增加分辨率
|
||||
analyser.smoothingTimeConstant = smoothing
|
||||
analyser.minDecibels = -100 // 更低的最小分贝
|
||||
analyser.maxDecibels = options.maxDecibels || -10 // 使用配置的最大分贝门槛
|
||||
// 优化分析器配置
|
||||
analyser.fftSize = 2048 // 增加分辨率
|
||||
analyser.smoothingTimeConstant = smoothing
|
||||
analyser.minDecibels = -100 // 更低的最小分贝
|
||||
analyser.maxDecibels = options.maxDecibels || -10 // 使用配置的最大分贝门槛
|
||||
|
||||
log('分析器配置:', {
|
||||
fftSize: analyser.fftSize,
|
||||
frequencyBinCount: analyser.frequencyBinCount,
|
||||
sampleRate: audioContext.sampleRate,
|
||||
frequencyResolution: audioContext.sampleRate / analyser.fftSize,
|
||||
maxDecibels: analyser.maxDecibels
|
||||
})
|
||||
log('分析器配置:', {
|
||||
fftSize: analyser.fftSize,
|
||||
frequencyBinCount: analyser.frequencyBinCount,
|
||||
sampleRate: audioContext.sampleRate,
|
||||
frequencyResolution: audioContext.sampleRate / analyser.fftSize,
|
||||
maxDecibels: analyser.maxDecibels,
|
||||
})
|
||||
|
||||
// 连接音频节点
|
||||
source.connect(analyser)
|
||||
analyser.connect(audioContext.destination)
|
||||
// 连接音频节点
|
||||
source.connect(analyser)
|
||||
analyser.connect(audioContext.destination)
|
||||
|
||||
// 创建数据数组
|
||||
dataArray = new Uint8Array(analyser.frequencyBinCount)
|
||||
// 创建数据数组
|
||||
dataArray = new Uint8Array(analyser.frequencyBinCount)
|
||||
|
||||
isInitialized.value = true
|
||||
error.value = null
|
||||
log('✅ 音频可视化器初始化成功')
|
||||
isInitialized.value = true
|
||||
error.value = null
|
||||
log('✅ 音频可视化器初始化成功')
|
||||
} catch (err) {
|
||||
log('❌ 音频上下文初始化失败:', err)
|
||||
error.value = `初始化失败: ${err instanceof Error ? err.message : String(err)}`
|
||||
isInitialized.value = false
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
log('❌ 音频上下文初始化失败:', err)
|
||||
error.value = `初始化失败: ${err instanceof Error ? err.message : String(err)}`
|
||||
isInitialized.value = false
|
||||
}
|
||||
}
|
||||
// 开始分析
|
||||
function startAnalysis() {
|
||||
if (!analyser || !dataArray || !isInitialized.value) {
|
||||
log('❌ 无法开始分析: 分析器未初始化')
|
||||
return
|
||||
}
|
||||
|
||||
// 开始分析
|
||||
function startAnalysis() {
|
||||
if (!analyser || !dataArray || !isInitialized.value) {
|
||||
log('❌ 无法开始分析: 分析器未初始化')
|
||||
return
|
||||
}
|
||||
log('✅ 开始频谱分析')
|
||||
isAnalyzing.value = true
|
||||
animate()
|
||||
}
|
||||
|
||||
log('✅ 开始频谱分析')
|
||||
isAnalyzing.value = true
|
||||
animate()
|
||||
}
|
||||
// 停止分析
|
||||
function stopAnalysis() {
|
||||
log('停止频谱分析')
|
||||
isAnalyzing.value = false
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
}
|
||||
barHeights.value = Array(barCount).fill(0)
|
||||
}
|
||||
|
||||
// 停止分析
|
||||
function stopAnalysis() {
|
||||
log('停止频谱分析')
|
||||
isAnalyzing.value = false
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId)
|
||||
animationId = null
|
||||
}
|
||||
barHeights.value = Array(barCount).fill(0)
|
||||
}
|
||||
// 动画循环
|
||||
function animate() {
|
||||
if (!isAnalyzing.value || !analyser || !dataArray || !audioContext) return
|
||||
|
||||
// 动画循环
|
||||
function animate() {
|
||||
if (!isAnalyzing.value || !analyser || !dataArray || !audioContext) return
|
||||
// 获取频率数据
|
||||
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)
|
||||
barHeights.value = enhancedBands.map((value) =>
|
||||
Math.min(100, Math.max(0, (value / 255) * 100 * sensitivity)),
|
||||
)
|
||||
|
||||
// 更新竖杠高度 (0-100)
|
||||
barHeights.value = enhancedBands.map(value =>
|
||||
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[] {
|
||||
const nyquist = sampleRate / 2
|
||||
const result: number[] = []
|
||||
|
||||
// 平衡的频段分割 - 使用对数分布和人耳感知特性
|
||||
function divideFrequencyBandsBalanced(data: Uint8Array, bands: number, sampleRate: number): number[] {
|
||||
const nyquist = sampleRate / 2
|
||||
const result: number[] = []
|
||||
|
||||
// 定义人耳感知的频率范围 (Hz)
|
||||
const frequencyRanges = [
|
||||
{ min: 20, max: 80, name: '超低音' }, // 索引 0
|
||||
{ min: 80, max: 250, name: '低音' }, // 索引 1
|
||||
{ min: 250, max: 800, name: '中低音' }, // 索引 2
|
||||
{ min: 800, max: 2500, name: '中音' }, // 索引 3
|
||||
{ min: 2500, max: 6000, name: '中高音' }, // 索引 4
|
||||
{ min: 6000, max: 20000, name: '高音' } // 索引 5
|
||||
// 定义人耳感知的频率范围 (Hz)
|
||||
const frequencyRanges = [
|
||||
{ min: 20, max: 80, name: '超低音' }, // 索引 0
|
||||
{ min: 80, max: 250, name: '低音' }, // 索引 1
|
||||
{ min: 250, max: 800, name: '中低音' }, // 索引 2
|
||||
{ min: 800, max: 2500, name: '中音' }, // 索引 3
|
||||
{ min: 2500, max: 6000, name: '中高音' }, // 索引 4
|
||||
{ min: 6000, max: 20000, name: '高音' }, // 索引 5
|
||||
]
|
||||
|
||||
for (let i = 0; i < bands; i++) {
|
||||
const range = frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
|
||||
for (let i = 0; i < bands; i++) {
|
||||
const range =
|
||||
frequencyRanges[i] || frequencyRanges[frequencyRanges.length - 1]
|
||||
|
||||
// 将频率转换为 bin 索引
|
||||
const startBin = Math.floor((range.min / nyquist) * data.length)
|
||||
const endBin = Math.floor((range.max / nyquist) * data.length)
|
||||
// 将频率转换为 bin 索引
|
||||
const startBin = Math.floor((range.min / nyquist) * data.length)
|
||||
const endBin = Math.floor((range.max / nyquist) * data.length)
|
||||
|
||||
// 确保范围有效
|
||||
const actualStart = Math.max(0, startBin)
|
||||
const actualEnd = Math.min(data.length - 1, endBin)
|
||||
// 确保范围有效
|
||||
const actualStart = Math.max(0, startBin)
|
||||
const actualEnd = Math.min(data.length - 1, endBin)
|
||||
|
||||
if (debug && Math.random() < 0.01) {
|
||||
log(`频段 ${i} (${range.name}): ${range.min}-${range.max}Hz, bins ${actualStart}-${actualEnd}`)
|
||||
}
|
||||
if (debug && Math.random() < 0.01) {
|
||||
log(
|
||||
`频段 ${i} (${range.name}): ${range.min}-${range.max}Hz, bins ${actualStart}-${actualEnd}`,
|
||||
)
|
||||
}
|
||||
|
||||
// 计算该频段的 RMS (均方根) 值,而不是简单平均
|
||||
let sumSquares = 0
|
||||
let count = 0
|
||||
// 计算该频段的 RMS (均方根) 值,而不是简单平均
|
||||
let sumSquares = 0
|
||||
let count = 0
|
||||
|
||||
for (let j = actualStart; j <= actualEnd; j++) {
|
||||
const value = data[j]
|
||||
sumSquares += value * value
|
||||
count++
|
||||
}
|
||||
for (let j = actualStart; j <= actualEnd; j++) {
|
||||
const value = data[j]
|
||||
sumSquares += value * value
|
||||
count++
|
||||
}
|
||||
|
||||
const rms = count > 0 ? Math.sqrt(sumSquares / count) : 0
|
||||
result.push(rms)
|
||||
}
|
||||
const rms = count > 0 ? Math.sqrt(sumSquares / count) : 0
|
||||
result.push(rms)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 应用频段特定的增强和门槛
|
||||
function applyFrequencyEnhancement(bands: number[]): number[] {
|
||||
// 六个频段的增强倍数
|
||||
const boosts = [bassBoost, bassBoost, midBoost, midBoost, trebleBoost, trebleBoost]
|
||||
// 应用频段特定的增强和门槛
|
||||
function applyFrequencyEnhancement(bands: number[]): number[] {
|
||||
// 六个频段的增强倍数
|
||||
const boosts = [
|
||||
bassBoost,
|
||||
bassBoost,
|
||||
midBoost,
|
||||
midBoost,
|
||||
trebleBoost,
|
||||
trebleBoost,
|
||||
]
|
||||
|
||||
return bands.map((value, index) => {
|
||||
// 应用响度门槛
|
||||
if (value < threshold) {
|
||||
if (debug && Math.random() < 0.01) {
|
||||
log(`频段 ${index} 低于门槛: ${Math.round(value)} < ${threshold}`)
|
||||
}
|
||||
return minHeight * 255 / 100 // 返回最小高度对应的值
|
||||
}
|
||||
return bands.map((value, index) => {
|
||||
// 应用响度门槛
|
||||
if (value < threshold) {
|
||||
if (debug && Math.random() < 0.01) {
|
||||
log(`频段 ${index} 低于门槛: ${Math.round(value)} < ${threshold}`)
|
||||
}
|
||||
return (minHeight * 255) / 100 // 返回最小高度对应的值
|
||||
}
|
||||
|
||||
const boost = boosts[index] || 1
|
||||
let enhanced = value * boost
|
||||
const boost = boosts[index] || 1
|
||||
let enhanced = value * boost
|
||||
|
||||
// 应用压缩曲线,防止过度增强
|
||||
enhanced = 255 * Math.tanh(enhanced / 255)
|
||||
// 应用压缩曲线,防止过度增强
|
||||
enhanced = 255 * Math.tanh(enhanced / 255)
|
||||
|
||||
return Math.min(255, Math.max(threshold, enhanced))
|
||||
})
|
||||
}
|
||||
return Math.min(255, Math.max(threshold, enhanced))
|
||||
})
|
||||
}
|
||||
|
||||
// 连接音频元素
|
||||
function connectAudio(audioElement: HTMLAudioElement) {
|
||||
log('🔗 连接音频元素...')
|
||||
// 连接音频元素
|
||||
function connectAudio(audioElement: HTMLAudioElement) {
|
||||
log('🔗 连接音频元素...')
|
||||
|
||||
if (currentAudioElement === audioElement) {
|
||||
log('音频元素相同,跳过重复连接')
|
||||
return
|
||||
}
|
||||
if (currentAudioElement === audioElement) {
|
||||
log('音频元素相同,跳过重复连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 清理旧的连接
|
||||
cleanup()
|
||||
// 清理旧的连接
|
||||
cleanup()
|
||||
|
||||
currentAudioElement = audioElement
|
||||
currentAudioElement = audioElement
|
||||
|
||||
// 等待音频加载完成后再初始化
|
||||
if (audioElement.readyState >= 2) {
|
||||
initAudioContext(audioElement)
|
||||
} else {
|
||||
audioElement.addEventListener('loadeddata', () => {
|
||||
initAudioContext(audioElement)
|
||||
}, { once: true })
|
||||
}
|
||||
// 等待音频加载完成后再初始化
|
||||
if (audioElement.readyState >= 2) {
|
||||
initAudioContext(audioElement)
|
||||
} else {
|
||||
audioElement.addEventListener(
|
||||
'loadeddata',
|
||||
() => {
|
||||
initAudioContext(audioElement)
|
||||
},
|
||||
{ once: true },
|
||||
)
|
||||
}
|
||||
|
||||
// 监听播放状态
|
||||
audioElement.addEventListener('play', startAnalysis)
|
||||
audioElement.addEventListener('pause', stopAnalysis)
|
||||
audioElement.addEventListener('ended', stopAnalysis)
|
||||
// 监听播放状态
|
||||
audioElement.addEventListener('play', startAnalysis)
|
||||
audioElement.addEventListener('pause', stopAnalysis)
|
||||
audioElement.addEventListener('ended', stopAnalysis)
|
||||
|
||||
// 监听错误
|
||||
audioElement.addEventListener('error', (e) => {
|
||||
log('❌ 音频加载错误:', e)
|
||||
error.value = '音频加载失败'
|
||||
})
|
||||
}
|
||||
// 监听错误
|
||||
audioElement.addEventListener('error', (e) => {
|
||||
log('❌ 音频加载错误:', e)
|
||||
error.value = '音频加载失败'
|
||||
})
|
||||
}
|
||||
|
||||
// 断开音频元素
|
||||
function disconnectAudio() {
|
||||
if (currentAudioElement) {
|
||||
currentAudioElement.removeEventListener('play', startAnalysis)
|
||||
currentAudioElement.removeEventListener('pause', stopAnalysis)
|
||||
currentAudioElement.removeEventListener('ended', stopAnalysis)
|
||||
currentAudioElement = null
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
// 断开音频元素
|
||||
function disconnectAudio() {
|
||||
if (currentAudioElement) {
|
||||
currentAudioElement.removeEventListener('play', startAnalysis)
|
||||
currentAudioElement.removeEventListener('pause', stopAnalysis)
|
||||
currentAudioElement.removeEventListener('ended', stopAnalysis)
|
||||
currentAudioElement = null
|
||||
}
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
function cleanup() {
|
||||
stopAnalysis()
|
||||
if (audioContext && audioContext.state !== 'closed') {
|
||||
audioContext.close()
|
||||
}
|
||||
audioContext = null
|
||||
analyser = null
|
||||
source = null
|
||||
dataArray = null
|
||||
isInitialized.value = false
|
||||
}
|
||||
// 清理资源
|
||||
function cleanup() {
|
||||
stopAnalysis()
|
||||
if (audioContext && audioContext.state !== 'closed') {
|
||||
audioContext.close()
|
||||
}
|
||||
audioContext = null
|
||||
analyser = null
|
||||
source = null
|
||||
dataArray = null
|
||||
isInitialized.value = false
|
||||
}
|
||||
|
||||
// 手动测试数据
|
||||
function testWithFakeData() {
|
||||
log('🧪 开始六频段模拟测试')
|
||||
isAnalyzing.value = true
|
||||
// 手动测试数据
|
||||
function testWithFakeData() {
|
||||
log('🧪 开始六频段模拟测试')
|
||||
isAnalyzing.value = true
|
||||
|
||||
let testCount = 0
|
||||
const maxTests = 50
|
||||
let testCount = 0
|
||||
const maxTests = 50
|
||||
|
||||
const fakeInterval = setInterval(() => {
|
||||
// 模拟六个频段的数据
|
||||
barHeights.value = [
|
||||
Math.random() * 50 + 10, // 超低音:10-60
|
||||
Math.random() * 60 + 20, // 低音:20-80
|
||||
Math.random() * 70 + 15, // 中低音:15-85
|
||||
Math.random() * 80 + 10, // 中音:10-90
|
||||
Math.random() * 75 + 10, // 中高音:10-85
|
||||
Math.random() * 65 + 15 // 高音:15-80
|
||||
]
|
||||
testCount++
|
||||
const fakeInterval = setInterval(() => {
|
||||
// 模拟六个频段的数据
|
||||
barHeights.value = [
|
||||
Math.random() * 50 + 10, // 超低音:10-60
|
||||
Math.random() * 60 + 20, // 低音:20-80
|
||||
Math.random() * 70 + 15, // 中低音:15-85
|
||||
Math.random() * 80 + 10, // 中音:10-90
|
||||
Math.random() * 75 + 10, // 中高音:10-85
|
||||
Math.random() * 65 + 15, // 高音:15-80
|
||||
]
|
||||
testCount++
|
||||
|
||||
if (testCount >= maxTests) {
|
||||
clearInterval(fakeInterval)
|
||||
barHeights.value = Array(barCount).fill(0)
|
||||
isAnalyzing.value = false
|
||||
log('🧪 模拟测试结束')
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
if (testCount >= maxTests) {
|
||||
clearInterval(fakeInterval)
|
||||
barHeights.value = Array(barCount).fill(0)
|
||||
isAnalyzing.value = false
|
||||
log('🧪 模拟测试结束')
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 动态调整增强参数和门槛
|
||||
function updateEnhancement(bass: number, mid: number, treble: number, newThreshold?: number, newMaxDecibels?: number) {
|
||||
options.bassBoost = bass
|
||||
options.midBoost = mid
|
||||
options.trebleBoost = treble
|
||||
if (newThreshold !== undefined) {
|
||||
options.threshold = newThreshold
|
||||
}
|
||||
if (newMaxDecibels !== undefined) {
|
||||
options.maxDecibels = newMaxDecibels
|
||||
// 如果分析器已经初始化,更新其配置
|
||||
if (analyser) {
|
||||
analyser.maxDecibels = newMaxDecibels
|
||||
log('实时更新 maxDecibels:', newMaxDecibels)
|
||||
}
|
||||
}
|
||||
log('更新频段增强:', { bass, mid, treble, threshold: options.threshold, maxDecibels: options.maxDecibels })
|
||||
}
|
||||
// 动态调整增强参数和门槛
|
||||
function updateEnhancement(
|
||||
bass: number,
|
||||
mid: number,
|
||||
treble: number,
|
||||
newThreshold?: number,
|
||||
newMaxDecibels?: number,
|
||||
) {
|
||||
options.bassBoost = bass
|
||||
options.midBoost = mid
|
||||
options.trebleBoost = treble
|
||||
if (newThreshold !== undefined) {
|
||||
options.threshold = newThreshold
|
||||
}
|
||||
if (newMaxDecibels !== undefined) {
|
||||
options.maxDecibels = newMaxDecibels
|
||||
// 如果分析器已经初始化,更新其配置
|
||||
if (analyser) {
|
||||
analyser.maxDecibels = newMaxDecibels
|
||||
log('实时更新 maxDecibels:', newMaxDecibels)
|
||||
}
|
||||
}
|
||||
log('更新频段增强:', {
|
||||
bass,
|
||||
mid,
|
||||
treble,
|
||||
threshold: options.threshold,
|
||||
maxDecibels: options.maxDecibels,
|
||||
})
|
||||
}
|
||||
|
||||
// 设置响度门槛
|
||||
function setThreshold(newThreshold: number) {
|
||||
options.threshold = Math.max(0, Math.min(255, newThreshold))
|
||||
log('更新响度门槛:', options.threshold)
|
||||
}
|
||||
// 设置响度门槛
|
||||
function setThreshold(newThreshold: number) {
|
||||
options.threshold = Math.max(0, Math.min(255, newThreshold))
|
||||
log('更新响度门槛:', options.threshold)
|
||||
}
|
||||
|
||||
// 设置最大分贝门槛
|
||||
function setMaxDecibels(newMaxDecibels: number) {
|
||||
options.maxDecibels = Math.max(-100, Math.min(0, newMaxDecibels))
|
||||
if (analyser) {
|
||||
analyser.maxDecibels = options.maxDecibels
|
||||
}
|
||||
log('更新最大分贝门槛:', options.maxDecibels)
|
||||
}
|
||||
// 设置最大分贝门槛
|
||||
function setMaxDecibels(newMaxDecibels: number) {
|
||||
options.maxDecibels = Math.max(-100, Math.min(0, newMaxDecibels))
|
||||
if (analyser) {
|
||||
analyser.maxDecibels = options.maxDecibels
|
||||
}
|
||||
log('更新最大分贝门槛:', options.maxDecibels)
|
||||
}
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
disconnectAudio()
|
||||
})
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
disconnectAudio()
|
||||
})
|
||||
|
||||
return {
|
||||
barHeights,
|
||||
isAnalyzing,
|
||||
isInitialized,
|
||||
error,
|
||||
connectAudio,
|
||||
disconnectAudio,
|
||||
startAnalysis,
|
||||
stopAnalysis,
|
||||
testWithFakeData,
|
||||
updateEnhancement,
|
||||
setThreshold,
|
||||
setMaxDecibels
|
||||
}
|
||||
return {
|
||||
barHeights,
|
||||
isAnalyzing,
|
||||
isInitialized,
|
||||
error,
|
||||
connectAudio,
|
||||
disconnectAudio,
|
||||
startAnalysis,
|
||||
stopAnalysis,
|
||||
testWithFakeData,
|
||||
updateEnhancement,
|
||||
setThreshold,
|
||||
setMaxDecibels,
|
||||
}
|
||||
}
|
|
@ -12,13 +12,15 @@ export function isSafari(): boolean {
|
|||
|
||||
// 检测 Safari 浏览器(包括 iOS 和 macOS)
|
||||
// Safari 的 User Agent 包含 'safari' 但不包含 'chrome' 或 'chromium'
|
||||
const isSafariBrowser = ua.includes('safari') &&
|
||||
const isSafariBrowser =
|
||||
ua.includes('safari') &&
|
||||
!ua.includes('chrome') &&
|
||||
!ua.includes('chromium') &&
|
||||
!ua.includes('android')
|
||||
|
||||
// 额外检查:使用 Safari 特有的 API
|
||||
const isSafariByFeature = 'safari' in window ||
|
||||
const isSafariByFeature =
|
||||
'safari' in window ||
|
||||
/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
|
||||
|
||||
return isSafariBrowser || isSafariByFeature
|
||||
|
@ -29,7 +31,9 @@ export function isSafari(): boolean {
|
|||
* @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false
|
||||
*/
|
||||
export function isMobileSafari(): boolean {
|
||||
return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
|
||||
return (
|
||||
/iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,11 +49,13 @@ export function supportsWebAudioVisualization(): boolean {
|
|||
}
|
||||
|
||||
// 检查基本的 Web Audio API 支持
|
||||
const hasAudioContext = 'AudioContext' in window || 'webkitAudioContext' in window
|
||||
const hasAnalyserNode = hasAudioContext && (
|
||||
'AnalyserNode' in window ||
|
||||
((window as any).AudioContext && 'createAnalyser' in (window as any).AudioContext.prototype)
|
||||
)
|
||||
const hasAudioContext =
|
||||
'AudioContext' in window || 'webkitAudioContext' in window
|
||||
const hasAnalyserNode =
|
||||
hasAudioContext &&
|
||||
('AnalyserNode' in window ||
|
||||
((window as any).AudioContext &&
|
||||
'createAnalyser' in (window as any).AudioContext.prototype))
|
||||
|
||||
return hasAudioContext && hasAnalyserNode
|
||||
}
|
||||
|
@ -94,6 +100,6 @@ export function getBrowserInfo() {
|
|||
version: browserVersion,
|
||||
isSafari: isSafari(),
|
||||
isMobileSafari: isMobileSafari(),
|
||||
supportsAudioVisualization: supportsWebAudioVisualization()
|
||||
supportsAudioVisualization: supportsWebAudioVisualization(),
|
||||
}
|
||||
}
|
|
@ -1,8 +1,16 @@
|
|||
import artistsOrganize from "./artistsOrganize"
|
||||
import { audioVisualizer } from "./audioVisualizer"
|
||||
import cicdInfo from "./cicdInfo"
|
||||
import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker"
|
||||
import { isSafari, isMobileSafari, supportsWebAudioVisualization, getBrowserInfo } from "./browserDetection"
|
||||
import artistsOrganize from './artistsOrganize'
|
||||
import { audioVisualizer } from './audioVisualizer'
|
||||
import cicdInfo from './cicdInfo'
|
||||
import {
|
||||
checkAndRefreshSongResource,
|
||||
checkAndRefreshMultipleSongs,
|
||||
} from './songResourceChecker'
|
||||
import {
|
||||
isSafari,
|
||||
isMobileSafari,
|
||||
supportsWebAudioVisualization,
|
||||
getBrowserInfo,
|
||||
} from './browserDetection'
|
||||
|
||||
export {
|
||||
artistsOrganize,
|
||||
|
@ -13,5 +21,5 @@ export {
|
|||
isSafari,
|
||||
isMobileSafari,
|
||||
supportsWebAudioVisualization,
|
||||
getBrowserInfo
|
||||
getBrowserInfo,
|
||||
}
|
||||
|
|
|
@ -9,51 +9,51 @@ import { debugResource } from './debug'
|
|||
* @returns 更新后的歌曲对象(如果需要更新)或原始歌曲对象
|
||||
*/
|
||||
export const checkAndRefreshSongResource = async (
|
||||
song: Song,
|
||||
updateCallback?: (updatedSong: Song) => void
|
||||
song: Song,
|
||||
updateCallback?: (updatedSong: Song) => void,
|
||||
): Promise<Song> => {
|
||||
if (!song.sourceUrl) {
|
||||
debugResource('歌曲没有 sourceUrl', song.name)
|
||||
return song
|
||||
}
|
||||
if (!song.sourceUrl) {
|
||||
debugResource('歌曲没有 sourceUrl', song.name)
|
||||
return song
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查资源是否可用
|
||||
await axios.head(song.sourceUrl, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
},
|
||||
params: {
|
||||
_t: Date.now() // 添加时间戳参数避免缓存
|
||||
},
|
||||
timeout: 5000 // 5秒超时
|
||||
})
|
||||
try {
|
||||
// 检查资源是否可用
|
||||
await axios.head(song.sourceUrl, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
Pragma: 'no-cache',
|
||||
Expires: '0',
|
||||
},
|
||||
params: {
|
||||
_t: Date.now(), // 添加时间戳参数避免缓存
|
||||
},
|
||||
timeout: 5000, // 5秒超时
|
||||
})
|
||||
|
||||
// 资源可用,返回原始歌曲
|
||||
debugResource('资源可用', song.name)
|
||||
return song
|
||||
} catch (error) {
|
||||
// 资源不可用,刷新歌曲信息
|
||||
debugResource('资源不可用,正在刷新', song.name, error)
|
||||
// 资源可用,返回原始歌曲
|
||||
debugResource('资源可用', song.name)
|
||||
return song
|
||||
} catch (error) {
|
||||
// 资源不可用,刷新歌曲信息
|
||||
debugResource('资源不可用,正在刷新', song.name, error)
|
||||
|
||||
try {
|
||||
const updatedSong = await apis.getSong(song.cid)
|
||||
debugResource('歌曲信息已刷新', updatedSong.name)
|
||||
try {
|
||||
const updatedSong = await apis.getSong(song.cid)
|
||||
debugResource('歌曲信息已刷新', updatedSong.name)
|
||||
|
||||
// 调用更新回调(如果提供)
|
||||
if (updateCallback) {
|
||||
updateCallback(updatedSong)
|
||||
}
|
||||
// 调用更新回调(如果提供)
|
||||
if (updateCallback) {
|
||||
updateCallback(updatedSong)
|
||||
}
|
||||
|
||||
return updatedSong
|
||||
} catch (refreshError) {
|
||||
debugResource('刷新歌曲信息失败', refreshError)
|
||||
// 刷新失败,返回原始歌曲
|
||||
return song
|
||||
}
|
||||
}
|
||||
return updatedSong
|
||||
} catch (refreshError) {
|
||||
debugResource('刷新歌曲信息失败', refreshError)
|
||||
// 刷新失败,返回原始歌曲
|
||||
return song
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,19 +63,19 @@ export const checkAndRefreshSongResource = async (
|
|||
* @returns 更新后的歌曲数组
|
||||
*/
|
||||
export const checkAndRefreshMultipleSongs = async (
|
||||
songs: Song[],
|
||||
updateCallback?: (updatedSong: Song, originalIndex: number) => void
|
||||
songs: Song[],
|
||||
updateCallback?: (updatedSong: Song, originalIndex: number) => void,
|
||||
): Promise<Song[]> => {
|
||||
const results: Song[] = []
|
||||
const results: Song[] = []
|
||||
|
||||
for (let i = 0; i < songs.length; i++) {
|
||||
const originalSong = songs[i]
|
||||
const updatedSong = await checkAndRefreshSongResource(
|
||||
originalSong,
|
||||
updateCallback ? (updated) => updateCallback(updated, i) : undefined
|
||||
)
|
||||
results.push(updatedSong)
|
||||
}
|
||||
for (let i = 0; i < songs.length; i++) {
|
||||
const originalSong = songs[i]
|
||||
const updatedSong = await checkAndRefreshSongResource(
|
||||
originalSong,
|
||||
updateCallback ? (updated) => updateCallback(updated, i) : undefined,
|
||||
)
|
||||
results.push(updatedSong)
|
||||
}
|
||||
|
||||
return results
|
||||
return results
|
||||
}
|
54
src/vite-env.d.ts
vendored
54
src/vite-env.d.ts
vendored
|
@ -5,26 +5,26 @@ type SongList = {
|
|||
}
|
||||
|
||||
type Song = {
|
||||
cid: string
|
||||
name: string
|
||||
albumCid?: string
|
||||
mvUrl?: string | null
|
||||
mvCoverUrl?: string | null
|
||||
cid: string
|
||||
name: string
|
||||
albumCid?: string
|
||||
mvUrl?: string | null
|
||||
mvCoverUrl?: string | null
|
||||
sourceUrl?: string | null
|
||||
lyricUrl?: string | null
|
||||
artistes?: string[]
|
||||
artists?: string[]
|
||||
artistes?: string[]
|
||||
artists?: string[]
|
||||
}
|
||||
|
||||
type Album = {
|
||||
cid: string
|
||||
name: string
|
||||
intro?: string
|
||||
belong?: string
|
||||
coverUrl: string
|
||||
coverDeUrl?: string
|
||||
artistes: string[]
|
||||
songs?: Song[]
|
||||
cid: string
|
||||
name: string
|
||||
intro?: string
|
||||
belong?: string
|
||||
coverUrl: string
|
||||
coverDeUrl?: string
|
||||
artistes: string[]
|
||||
songs?: Song[]
|
||||
}
|
||||
|
||||
type AlbumList = Album[]
|
||||
|
@ -36,22 +36,22 @@ interface ApiResponse {
|
|||
}
|
||||
|
||||
interface QueueItem {
|
||||
song: Song
|
||||
album?: Album
|
||||
sourceUrl?: string
|
||||
lyricUrl?: string | null
|
||||
song: Song
|
||||
album?: Album
|
||||
sourceUrl?: string
|
||||
lyricUrl?: string | null
|
||||
}
|
||||
|
||||
interface LyricsLine {
|
||||
type: 'lyric'
|
||||
time: number
|
||||
text: string
|
||||
originalTime: string
|
||||
type: 'lyric'
|
||||
time: number
|
||||
text: string
|
||||
originalTime: string
|
||||
}
|
||||
|
||||
interface GapLine {
|
||||
type: 'gap'
|
||||
time: number
|
||||
originalTime: string
|
||||
duration?: number // 添加间隔持续时间
|
||||
type: 'gap'
|
||||
time: number
|
||||
originalTime: string
|
||||
duration?: number // 添加间隔持续时间
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import path from 'node:path'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
import path from "node:path"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
|
@ -26,9 +26,9 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
}
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue
Block a user