Compare commits
	
		
			No commits in common. "main" and "feature/safari_compatibility" have entirely different histories.
		
	
	
		
			main
			...
			feature/sa
		
	
		
| 
						 | 
				
			
			@ -90,12 +90,12 @@ jobs:
 | 
			
		|||
        
 | 
			
		||||
    - name: 创建 Safari 扩展项目
 | 
			
		||||
      run: |
 | 
			
		||||
        xcrun safari-web-extension-converter dist --project-location safari-extension --app-name "MSR Mod" --bundle-identifier "moe.astrian.ext-msrmod" --swift --no-open
 | 
			
		||||
        xcrun safari-web-extension-converter dist --project-location safari-extension --app-name "MSR Mod" --bundle-identifier "com.astrian.msrmod" --swift --no-open
 | 
			
		||||
        
 | 
			
		||||
    - name: 构建 Safari 扩展
 | 
			
		||||
      run: |
 | 
			
		||||
        cd "safari-extension/MSR Mod"
 | 
			
		||||
        xcodebuild -project "MSR Mod.xcodeproj" -scheme "MSR Mod (macOS)" -configuration Release -destination "generic/platform=macOS" build
 | 
			
		||||
        cd safari-extension
 | 
			
		||||
        xcodebuild -project "MSR Mod.xcodeproj" -scheme "MSR Mod" -configuration Release -destination "generic/platform=macOS" build
 | 
			
		||||
        
 | 
			
		||||
    - name: 上传构建工件
 | 
			
		||||
      uses: actions/upload-artifact@v3
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										159
									
								
								CLAUDE.md
									
									
									
									
									
								
							
							
						
						
									
										159
									
								
								CLAUDE.md
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -1,159 +0,0 @@
 | 
			
		|||
# CLAUDE.md
 | 
			
		||||
 | 
			
		||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
 | 
			
		||||
 | 
			
		||||
## Project Overview
 | 
			
		||||
 | 
			
		||||
MSR Mod is a browser extension that provides an alternate frontend for Monster Siren Records (monster-siren.hypergryph.com). It's built with Vue 3, TypeScript, and Tailwind CSS, designed to work as both Chrome and Firefox extensions.
 | 
			
		||||
 | 
			
		||||
## Common Commands
 | 
			
		||||
 | 
			
		||||
### Development
 | 
			
		||||
```bash
 | 
			
		||||
npm run dev                 # Start development server with Vite
 | 
			
		||||
npm run dev:refresh         # Build and refresh dist folder for extension development
 | 
			
		||||
npm i                       # Install dependencies
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Building
 | 
			
		||||
```bash
 | 
			
		||||
npm run build:chrome        # Build for Chrome/Chromium browsers
 | 
			
		||||
npm run build:firefox       # Build for Firefox
 | 
			
		||||
npm run build:safari        # Build for Safari (uses background.html)
 | 
			
		||||
npm run build              # Default build (Chrome)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Code Quality
 | 
			
		||||
```bash
 | 
			
		||||
npm run lint               # Format code with Biome
 | 
			
		||||
npm run quality-check      # Run Biome CI checks
 | 
			
		||||
npm run qc                 # Alias for quality-check
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Extension Development Workflow
 | 
			
		||||
1. Run `npm run dev:refresh` to build initial dist folder
 | 
			
		||||
2. Load the `dist` folder as an unpacked extension in browser
 | 
			
		||||
3. Use `npm run dev` for hot-reload development
 | 
			
		||||
4. Use `npm run build:watch` for continuous builds
 | 
			
		||||
 | 
			
		||||
## Architecture
 | 
			
		||||
 | 
			
		||||
### Core Technologies
 | 
			
		||||
- **Vue 3** with Composition API and `<script setup>` syntax
 | 
			
		||||
- **TypeScript** for type safety
 | 
			
		||||
- **Pinia** for state management
 | 
			
		||||
- **Vue Router** with hash history for extension compatibility
 | 
			
		||||
- **Tailwind CSS v4** for styling
 | 
			
		||||
- **GSAP** for animations
 | 
			
		||||
- **Axios** for API communication
 | 
			
		||||
 | 
			
		||||
### Browser Extension Structure
 | 
			
		||||
- **Manifest V3** with platform-specific builds
 | 
			
		||||
- **Content Scripts** inject the frontend on monster-siren.hypergryph.com
 | 
			
		||||
- **Background Service Worker** handles extension lifecycle
 | 
			
		||||
- **Cross-platform compatibility** via prebuild scripts
 | 
			
		||||
 | 
			
		||||
### State Management (Pinia Stores)
 | 
			
		||||
- **usePlayQueueStore**: Music playback queue, shuffle/repeat modes, audio preloading
 | 
			
		||||
- **useFavourites**: User favorites with cross-platform storage (Chrome storage API/localStorage)
 | 
			
		||||
- **usePreferences**: User settings and preferences
 | 
			
		||||
 | 
			
		||||
### Key Components
 | 
			
		||||
- **Player**: Main audio player with preloading and resource validation
 | 
			
		||||
- **Playroom**: Full-screen player interface with lyrics and visualizations
 | 
			
		||||
- **ScrollingLyrics**: Animated lyrics display with auto-scroll and user interaction
 | 
			
		||||
- **PlayListItem/TrackItem**: Reusable music track components
 | 
			
		||||
 | 
			
		||||
### API Integration
 | 
			
		||||
- **Monster Siren API**: Fetches songs, albums, and metadata via `src/apis/index.ts`
 | 
			
		||||
- **Resource URL Validation**: Automatic refresh of cached URLs when servers rotate resources
 | 
			
		||||
- **Preloading System**: Smart audio preloading with cache management
 | 
			
		||||
 | 
			
		||||
### Browser Compatibility
 | 
			
		||||
- **Chrome**: Uses service worker, full CSP support
 | 
			
		||||
- **Firefox**: Uses background scripts, modified CSP, specific gecko settings
 | 
			
		||||
- **Safari**: Uses background page (background.html) instead of service worker
 | 
			
		||||
- **Prebuild Scripts**: Automatically modify manifest.json and HTML for each platform
 | 
			
		||||
 | 
			
		||||
### Storage Strategy
 | 
			
		||||
- **Favorites**: Stored in Chrome storage API (fallback to localStorage)
 | 
			
		||||
- **Preferences**: Browser-specific storage with graceful degradation
 | 
			
		||||
- **Audio Cache**: In-memory preloading with size limits
 | 
			
		||||
 | 
			
		||||
### Resource Management
 | 
			
		||||
- **Audio Preloading**: Validates and preloads next track during playback
 | 
			
		||||
- **URL Refresh Logic**: Checks resource availability before playback/preload
 | 
			
		||||
- **Cache Invalidation**: Automatic cleanup when resource URLs change
 | 
			
		||||
 | 
			
		||||
### Shuffle/Random Play Logic
 | 
			
		||||
- **shuffleList**: Array storing the shuffled order of original list indices
 | 
			
		||||
- **currentIndex**: In shuffle mode, this is the index within shuffleList
 | 
			
		||||
- **Accessing current song**: `list[shuffleList[currentIndex]]` in shuffle mode
 | 
			
		||||
- **getNextIndex**: Returns the actual list index of the next song to play
 | 
			
		||||
 | 
			
		||||
## File Structure Notes
 | 
			
		||||
 | 
			
		||||
### `/src/utils/`
 | 
			
		||||
- **songResourceChecker.ts**: Centralized resource validation and refresh logic
 | 
			
		||||
- **audioVisualizer.ts**: Real-time audio analysis for visual effects
 | 
			
		||||
- **artistsOrganize.ts**: Helper for formatting artist names
 | 
			
		||||
 | 
			
		||||
### `/scripts/`
 | 
			
		||||
- **prebuild-chrome.js**: Removes localhost dev configs for production
 | 
			
		||||
- **prebuild-firefox.js**: Adapts manifest for Firefox compatibility
 | 
			
		||||
- **prebuild-safari.js**: Creates background.html and adapts manifest for Safari
 | 
			
		||||
 | 
			
		||||
### `/public/`
 | 
			
		||||
- **manifest.json**: Extension manifest (modified by prebuild scripts)
 | 
			
		||||
- **content.js**: Injects the Vue app into target websites
 | 
			
		||||
- **background.js**: Extension background script
 | 
			
		||||
 | 
			
		||||
## Code Style and Formatting
 | 
			
		||||
 | 
			
		||||
### Indentation
 | 
			
		||||
- **This project uses Tab indentation (not spaces)**
 | 
			
		||||
- Ensure all code edits maintain consistent Tab indentation
 | 
			
		||||
- When editing files, preserve the existing Tab character formatting
 | 
			
		||||
 | 
			
		||||
## Development Considerations
 | 
			
		||||
 | 
			
		||||
### Extension Context
 | 
			
		||||
- Uses hash routing for browser extension compatibility
 | 
			
		||||
- CSP restrictions require specific script and style handling
 | 
			
		||||
- Cross-origin requests limited to declared host permissions
 | 
			
		||||
 | 
			
		||||
### Performance
 | 
			
		||||
- Audio preloading system prevents playback interruptions
 | 
			
		||||
- Resource validation happens only when needed (playback/preload)
 | 
			
		||||
- GSAP animations with proper cleanup to prevent memory leaks
 | 
			
		||||
 | 
			
		||||
### Error Handling
 | 
			
		||||
- Graceful fallbacks for storage API unavailability
 | 
			
		||||
- Resource URL rotation handling with automatic refresh
 | 
			
		||||
- Cross-browser compatibility with feature detection
 | 
			
		||||
 | 
			
		||||
## Safari Extension Considerations
 | 
			
		||||
 | 
			
		||||
### Background Script Handling
 | 
			
		||||
Safari Web Extensions have different requirements for background scripts:
 | 
			
		||||
 | 
			
		||||
1. **Background Page vs Service Worker**: Safari uses `background.page` instead of `service_worker`
 | 
			
		||||
2. **Background HTML**: The prebuild script creates `background.html` that loads `background.js`
 | 
			
		||||
3. **Manifest Configuration**: Uses `"background": { "page": "background.html", "persistent": false }`
 | 
			
		||||
 | 
			
		||||
### Auto-redirect Functionality
 | 
			
		||||
The auto-redirect feature in Safari may require special handling due to:
 | 
			
		||||
- Different WebKit extension APIs
 | 
			
		||||
- Safari's stricter security policies
 | 
			
		||||
- Tab management differences from Chromium
 | 
			
		||||
 | 
			
		||||
### Building for Safari
 | 
			
		||||
```bash
 | 
			
		||||
npm run build:safari    # Creates background.html and Safari-specific manifest
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
The Safari build process:
 | 
			
		||||
1. Removes localhost development configurations
 | 
			
		||||
2. Converts `service_worker` to `background.page`
 | 
			
		||||
3. Creates `background.html` wrapper for `background.js`
 | 
			
		||||
4. Adds Safari-specific browser settings
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +8,6 @@
 | 
			
		|||
		"build": "echo 'No platform specified, will build for Chromium.' && npm run build-chrome",
 | 
			
		||||
		"build:chrome": "npm run prebuild:chrome && vue-tsc -b && vite build && cp -r public/* dist/",
 | 
			
		||||
		"build:firefox": "npm run prebuild:firefox && vue-tsc -b && vite build && cp -r public/* dist/",
 | 
			
		||||
		"build:safari": "npm run prebuild:safari && vue-tsc -b && vite build && cp -r public/* dist/",
 | 
			
		||||
		"dev:refresh": "vue-tsc -b && vite build && cp -r public/* dist/",
 | 
			
		||||
		"build:watch": "vite build --watch",
 | 
			
		||||
		"preview": "vite preview",
 | 
			
		||||
| 
						 | 
				
			
			@ -16,8 +15,7 @@
 | 
			
		|||
		"quality-check": "biome ci",
 | 
			
		||||
		"qc": "npm run quality-check",
 | 
			
		||||
		"prebuild:chrome": "node scripts/prebuild-chrome.js",
 | 
			
		||||
		"prebuild:firefox": "node scripts/prebuild-firefox.js",
 | 
			
		||||
		"prebuild:safari": "node scripts/prebuild-safari.js"
 | 
			
		||||
		"prebuild:firefox": "node scripts/prebuild-firefox.js"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@tailwindcss/vite": "^4.1.7",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
{
 | 
			
		||||
	"manifest_version": 3,
 | 
			
		||||
	"name": "MSR Mod",
 | 
			
		||||
	"version": "0.0.7",
 | 
			
		||||
	"version": "0.0.5",
 | 
			
		||||
	"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
 | 
			
		||||
	"content_scripts": [
 | 
			
		||||
		{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,359 +0,0 @@
 | 
			
		|||
import fs from 'fs';
 | 
			
		||||
import path from 'path';
 | 
			
		||||
import { fileURLToPath } from 'url';
 | 
			
		||||
 | 
			
		||||
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'));
 | 
			
		||||
 | 
			
		||||
  // 移除本地调试相关的配置
 | 
			
		||||
  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();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 创建 background.html 文件用于 Safari
 | 
			
		||||
  const backgroundHtmlPath = path.join(__dirname, '../public/background.html');
 | 
			
		||||
  const backgroundHtmlContent = `<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8">
 | 
			
		||||
  <title>MSR Mod Background</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <h1>MSR Mod Background Page</h1>
 | 
			
		||||
  <p>If you can see this page, the background page is loaded!</p>
 | 
			
		||||
  <div id="log"></div>
 | 
			
		||||
  
 | 
			
		||||
  <script>
 | 
			
		||||
    // 创建日志函数,同时显示在页面和控制台
 | 
			
		||||
    function log(message) {
 | 
			
		||||
      console.log(message);
 | 
			
		||||
      const logDiv = document.getElementById('log');
 | 
			
		||||
      if (logDiv) {
 | 
			
		||||
        logDiv.innerHTML += '<div>' + message + '</div>';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    log('=== SAFARI BACKGROUND PAGE LOADED ===');
 | 
			
		||||
    log('Document ready state: ' + document.readyState);
 | 
			
		||||
    log('Location: ' + location.href);
 | 
			
		||||
    log('Time: ' + new Date().toISOString());
 | 
			
		||||
    
 | 
			
		||||
    // 确保在 Safari 中正确加载脚本
 | 
			
		||||
    try {
 | 
			
		||||
      log('Safari extension context: ' + JSON.stringify({
 | 
			
		||||
        chrome: typeof chrome,
 | 
			
		||||
        browser: typeof browser,
 | 
			
		||||
        safari: typeof safari
 | 
			
		||||
      }));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      log('Error in background.html: ' + e.message);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 监听事件
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
			
		||||
      log('=== DOMContentLoaded fired ===');
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    window.addEventListener('load', function() {
 | 
			
		||||
      log('=== Window load fired ===');
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    log('About to load background.js...');
 | 
			
		||||
  </script>
 | 
			
		||||
  <script src="background.js" onload="log('background.js loaded successfully')" onerror="log('Failed to load background.js')"></script>
 | 
			
		||||
  <script>
 | 
			
		||||
    log('=== After background.js script tag ===');
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>`;
 | 
			
		||||
  fs.writeFileSync(backgroundHtmlPath, backgroundHtmlContent);
 | 
			
		||||
 | 
			
		||||
  // 创建 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 = `
 | 
			
		||||
console.log("=== Safari background.js starting ===");
 | 
			
		||||
console.log("Available APIs:", {
 | 
			
		||||
  chrome: typeof chrome,
 | 
			
		||||
  browser: typeof browser,
 | 
			
		||||
  safari: typeof safari
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Safari 特殊处理
 | 
			
		||||
if (typeof chrome === 'undefined' && typeof browser === 'undefined') {
 | 
			
		||||
  console.log("No extension APIs available in Safari");
 | 
			
		||||
  // 如果没有扩展 API,创建一个空的对象避免错误
 | 
			
		||||
  window.chrome = {
 | 
			
		||||
    webRequest: { onBeforeRequest: { addListener: () => {} } },
 | 
			
		||||
    storage: { sync: { get: () => Promise.resolve({}) } },
 | 
			
		||||
    tabs: { create: () => {}, remove: () => {}, update: () => {} },
 | 
			
		||||
    runtime: { 
 | 
			
		||||
      getURL: (path) => path,
 | 
			
		||||
      onMessage: { addListener: () => {} }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Safari 消息监听器:处理来自 content script 的重定向请求
 | 
			
		||||
if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) {
 | 
			
		||||
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 | 
			
		||||
    console.log('Background received message:', message);
 | 
			
		||||
    
 | 
			
		||||
    if (message.action === 'redirect_to_extension') {
 | 
			
		||||
      console.log('Processing redirect request from content script');
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        // 创建新标签页并打开扩展
 | 
			
		||||
        const extensionUrl = chrome.runtime.getURL('index.html');
 | 
			
		||||
        chrome.tabs.create({ url: extensionUrl }, (newTab) => {
 | 
			
		||||
          console.log('New extension tab created:', newTab.id);
 | 
			
		||||
          
 | 
			
		||||
          // 关闭原始标签页
 | 
			
		||||
          if (sender.tab && sender.tab.id) {
 | 
			
		||||
            chrome.tabs.remove(sender.tab.id);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          sendResponse({ success: true, url: extensionUrl });
 | 
			
		||||
        });
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Failed to redirect:', error);
 | 
			
		||||
        sendResponse({ success: false, error: error.message });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return true; // 保持消息通道开放
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
`;
 | 
			
		||||
    
 | 
			
		||||
    // 替换 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/')"
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // 清理可能的重复条件
 | 
			
		||||
    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');
 | 
			
		||||
 | 
			
		||||
  // 创建 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 = `
 | 
			
		||||
// Safari 扩展 content script for redirect
 | 
			
		||||
console.log('MSR Mod content script loaded on:', window.location.href);
 | 
			
		||||
 | 
			
		||||
// 兼容 Safari 的浏览器 API
 | 
			
		||||
const browserAPI = typeof browser !== 'undefined' ? browser : chrome;
 | 
			
		||||
 | 
			
		||||
// 异步函数:检查重定向偏好设置
 | 
			
		||||
async function checkRedirectPreference() {
 | 
			
		||||
  try {
 | 
			
		||||
    console.log('Checking redirect preferences...');
 | 
			
		||||
    
 | 
			
		||||
    // 读取偏好设置
 | 
			
		||||
    const pref = await browserAPI.storage.sync.get('preferences');
 | 
			
		||||
    console.log('Retrieved preferences:', pref);
 | 
			
		||||
    
 | 
			
		||||
    // 检查自动重定向设置(默认为 true)
 | 
			
		||||
    const shouldRedirect = pref === undefined || 
 | 
			
		||||
                          pref.preferences === undefined || 
 | 
			
		||||
                          pref.preferences.autoRedirect === undefined || 
 | 
			
		||||
                          pref.preferences.autoRedirect === true;
 | 
			
		||||
    
 | 
			
		||||
    console.log('Should redirect:', shouldRedirect);
 | 
			
		||||
    return shouldRedirect;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error reading preferences:', error);
 | 
			
		||||
    // 如果读取偏好设置失败,默认重定向
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行重定向的函数
 | 
			
		||||
function performRedirect() {
 | 
			
		||||
  console.log('Performing redirect to extension...');
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    // 对于 Safari,我们需要使用消息传递来请求重定向
 | 
			
		||||
    // 因为 content script 无法直接访问 chrome.runtime.getURL
 | 
			
		||||
    
 | 
			
		||||
    // 方案1:尝试通过消息传递
 | 
			
		||||
    if (typeof chrome !== 'undefined' && chrome.runtime) {
 | 
			
		||||
      chrome.runtime.sendMessage({action: 'redirect_to_extension'}, (response) => {
 | 
			
		||||
        if (chrome.runtime.lastError) {
 | 
			
		||||
          console.log('Message sending failed, trying direct redirect...');
 | 
			
		||||
          // 方案2:尝试直接重定向(可能在某些情况下有效)
 | 
			
		||||
          window.location.href = 'safari-web-extension://[extension-id]/index.html';
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('Chrome runtime not available, trying alternative redirect...');
 | 
			
		||||
      // 方案3:显示提示让用户手动打开扩展
 | 
			
		||||
      document.body.innerHTML = \`
 | 
			
		||||
        <div style="
 | 
			
		||||
          position: fixed; 
 | 
			
		||||
          top: 50%; 
 | 
			
		||||
          left: 50%; 
 | 
			
		||||
          transform: translate(-50%, -50%);
 | 
			
		||||
          background: #1a1a1a; 
 | 
			
		||||
          color: white; 
 | 
			
		||||
          padding: 20px; 
 | 
			
		||||
          border-radius: 8px;
 | 
			
		||||
          text-align: center;
 | 
			
		||||
          font-family: -apple-system, BlinkMacSystemFont, sans-serif;
 | 
			
		||||
          z-index: 10000;
 | 
			
		||||
        ">
 | 
			
		||||
          <h2>MSR Mod Extension Detected</h2>
 | 
			
		||||
          <p>Please click the MSR Mod extension icon in your Safari toolbar to open the app.</p>
 | 
			
		||||
          <button onclick="window.close()" style="
 | 
			
		||||
            background: #007AFF; 
 | 
			
		||||
            color: white; 
 | 
			
		||||
            border: none; 
 | 
			
		||||
            padding: 10px 20px; 
 | 
			
		||||
            border-radius: 4px; 
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            margin-top: 10px;
 | 
			
		||||
          ">Close Tab</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      \`;
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Redirect failed:', error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 主逻辑:检查页面并根据偏好设置决定是否重定向
 | 
			
		||||
async function main() {
 | 
			
		||||
  // 检查是否是目标页面
 | 
			
		||||
  if (window.location.pathname === '/' || window.location.href.includes('appShell.html')) {
 | 
			
		||||
    console.log('Detected target page, checking preferences...');
 | 
			
		||||
    
 | 
			
		||||
    // 检查偏好设置
 | 
			
		||||
    const shouldRedirect = await checkRedirectPreference();
 | 
			
		||||
    
 | 
			
		||||
    if (shouldRedirect) {
 | 
			
		||||
      console.log('Auto-redirect is enabled, proceeding with redirect...');
 | 
			
		||||
      performRedirect();
 | 
			
		||||
    } else {
 | 
			
		||||
      console.log('Auto-redirect is disabled, skipping redirect.');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行主逻辑
 | 
			
		||||
main().catch(error => {
 | 
			
		||||
  console.error('Error in main function:', error);
 | 
			
		||||
});
 | 
			
		||||
`;
 | 
			
		||||
    
 | 
			
		||||
    fs.writeFileSync(contentJsPath, contentJsContent);
 | 
			
		||||
  }
 | 
			
		||||
  console.log('✅ Safari-compatible content.js created');
 | 
			
		||||
 | 
			
		||||
  // Safari 可能需要额外的权限
 | 
			
		||||
  if (!manifest.permissions.includes('activeTab')) {
 | 
			
		||||
    manifest.permissions.push('activeTab');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 添加 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');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理 index.html
 | 
			
		||||
function processIndexHtml() {
 | 
			
		||||
  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"'
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // 移除 crossorigin 属性
 | 
			
		||||
  content = content.replace(/\s+crossorigin/g, '');
 | 
			
		||||
 | 
			
		||||
  fs.writeFileSync(indexPath, content);
 | 
			
		||||
  console.log('✅ Index.html processed for Safari');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行处理
 | 
			
		||||
try {
 | 
			
		||||
  processManifest();
 | 
			
		||||
  processIndexHtml();
 | 
			
		||||
  console.log('🎉 Safari build preparation completed!');
 | 
			
		||||
} catch (error) {
 | 
			
		||||
  console.error('❌ Error during Safari build preparation:', error);
 | 
			
		||||
  process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,8 +9,6 @@ import LeftArrowIcon from './assets/icons/leftarrow.vue'
 | 
			
		|||
import CorgIcon from './assets/icons/corg.vue'
 | 
			
		||||
import { watch } from 'vue'
 | 
			
		||||
 | 
			
		||||
import UpdatePopup from './components/UpdatePopup.vue'
 | 
			
		||||
 | 
			
		||||
const presentPreferencePanel = ref(false)
 | 
			
		||||
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
| 
						 | 
				
			
			@ -23,8 +21,6 @@ watch(() => presentPreferencePanel, (value) => {
 | 
			
		|||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<UpdatePopup />
 | 
			
		||||
 | 
			
		||||
	<div class="w-screen h-screen overflow-hidden bg-[#191919]">
 | 
			
		||||
		<div class="flex flex-col w-full h-full overflow-y-auto">
 | 
			
		||||
			<div class="py-8 px-4 md:px-8 w-screen bg-gradient-to-b from-[#00000080] to-transparent z-20 absolute top-0">
 | 
			
		||||
| 
						 | 
				
			
			@ -81,4 +77,4 @@ watch(() => presentPreferencePanel, (value) => {
 | 
			
		|||
		</div>
 | 
			
		||||
		<PreferencePanel :present="presentPreferencePanel" @dismiss="presentPreferencePanel = false" />
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
defineProps<{
 | 
			
		||||
	size: number
 | 
			
		||||
}>()
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" :class="`w-${size} h-${size}`">
 | 
			
		||||
		<rect x="3" y="9" width="2" height="6" rx="1"></rect>
 | 
			
		||||
		<rect x="7" y="5" width="2" height="14" rx="1"></rect>
 | 
			
		||||
		<rect x="11" y="7" width="2" height="10" rx="1"></rect>
 | 
			
		||||
		<rect x="15" y="4" width="2" height="16" rx="1"></rect>
 | 
			
		||||
		<rect x="19" y="10" width="2" height="4" rx="1"></rect>
 | 
			
		||||
	</svg>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -2,14 +2,17 @@
 | 
			
		|||
import { artistsOrganize } from '../utils'
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
import { useFavourites } from '../stores/useFavourites'
 | 
			
		||||
import apis from '../apis'
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
import StarSlashIcon from '../assets/icons/starslash.vue'
 | 
			
		||||
import { onMounted } from 'vue'
 | 
			
		||||
 | 
			
		||||
const favourites = useFavourites()
 | 
			
		||||
 | 
			
		||||
const hover = ref(false)
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	item: QueueItem
 | 
			
		||||
	index: number
 | 
			
		||||
}>()
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +20,27 @@ defineProps<{
 | 
			
		|||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'play', index: number): void
 | 
			
		||||
}>()
 | 
			
		||||
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	try {
 | 
			
		||||
		// 添加缓存控制头和随机参数来避免缓存
 | 
			
		||||
		await axios.head(props.item.song.sourceUrl ?? '', {
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Cache-Control': 'no-cache, no-store, must-revalidate',
 | 
			
		||||
				'Pragma': 'no-cache',
 | 
			
		||||
				'Expires': '0'
 | 
			
		||||
			},
 | 
			
		||||
			params: {
 | 
			
		||||
				_t: Date.now() // 添加时间戳参数避免缓存
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	} catch (error) {
 | 
			
		||||
		// 刷新资源地址
 | 
			
		||||
		const updatedSong = await apis.getSong(props.item.song.cid)
 | 
			
		||||
		console.log('Updated song:', updatedSong)
 | 
			
		||||
		favourites.updateSongInFavourites(props.item.song.cid, updatedSong)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,10 @@
 | 
			
		|||
<script setup lang="ts">
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
import { artistsOrganize, supportsWebAudioVisualization } from '../utils'
 | 
			
		||||
import { artistsOrganize } from '../utils'
 | 
			
		||||
 | 
			
		||||
import XIcon from '../assets/icons/x.vue'
 | 
			
		||||
import UpHyphenIcon from '../assets/icons/uphypen.vue'
 | 
			
		||||
import DownHyphenIcon from '../assets/icons/downhyphen.vue'
 | 
			
		||||
import SoundwaveIcon from '../assets/icons/soundwave.vue'
 | 
			
		||||
 | 
			
		||||
import { ref } from 'vue'
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,9 +18,6 @@ const playQueueStore = usePlayQueueStore()
 | 
			
		|||
 | 
			
		||||
const hover = ref(false)
 | 
			
		||||
 | 
			
		||||
// 检查浏览器是否支持音频可视化
 | 
			
		||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
 | 
			
		||||
 | 
			
		||||
function moveUp() {
 | 
			
		||||
	if (props.index === 0) return
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -160,14 +156,12 @@ function removeItem() {
 | 
			
		|||
				<img :src="queueItem.album?.coverUrl" />
 | 
			
		||||
				<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
 | 
			
		||||
					v-if="isCurrent">
 | 
			
		||||
					<!-- 在支持的浏览器上显示可视化,否则显示音波图标 -->
 | 
			
		||||
					<div v-if="isAudioVisualizationSupported" style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
					<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
						<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
			
		||||
							:key="index" :style="{
 | 
			
		||||
								height: `${Math.max(10, bar)}%`
 | 
			
		||||
							}" />
 | 
			
		||||
					</div>
 | 
			
		||||
					<SoundwaveIcon v-else :size="6" class="text-white animate-pulse" />
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="flex flex-col text-left flex-auto w-0">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,15 @@
 | 
			
		|||
<!-- Player.vue - 添加预加载功能 -->
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, nextTick, ref, useTemplateRef, watch } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
import { useFavourites } from '../stores/useFavourites'
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
 | 
			
		||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
			
		||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
 | 
			
		||||
import { useTemplateRef, watch, nextTick, computed } from 'vue'
 | 
			
		||||
import { useRoute } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
import PlayIcon from '../assets/icons/play.vue'
 | 
			
		||||
import PauseIcon from '../assets/icons/pause.vue'
 | 
			
		||||
import { audioVisualizer, checkAndRefreshSongResource, supportsWebAudioVisualization } from '../utils'
 | 
			
		||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
 | 
			
		||||
import { audioVisualizer } from '../utils'
 | 
			
		||||
 | 
			
		||||
const playQueueStore = usePlayQueueStore()
 | 
			
		||||
const favourites = useFavourites()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const player = useTemplateRef('playerRef')
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -19,21 +17,16 @@ const player = useTemplateRef('playerRef')
 | 
			
		|||
console.log('[Player] 检查 store 方法:', {
 | 
			
		||||
	preloadNext: typeof playQueueStore.preloadNext,
 | 
			
		||||
	getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
 | 
			
		||||
	clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio,
 | 
			
		||||
	clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 获取当前歌曲的计算属性
 | 
			
		||||
const currentTrack = computed(() => {
 | 
			
		||||
	if (
 | 
			
		||||
		playQueueStore.playMode.shuffle &&
 | 
			
		||||
		playQueueStore.shuffleList.length > 0
 | 
			
		||||
	) {
 | 
			
		||||
		return playQueueStore.list[
 | 
			
		||||
			playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
		]
 | 
			
		||||
	if (playQueueStore.playMode.shuffle && playQueueStore.shuffleList.length > 0) {
 | 
			
		||||
		return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
 | 
			
		||||
	} else {
 | 
			
		||||
		return playQueueStore.list[playQueueStore.currentIndex]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return playQueueStore.list[playQueueStore.currentIndex]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 获取当前歌曲的音频源
 | 
			
		||||
| 
						 | 
				
			
			@ -42,132 +35,86 @@ const currentAudioSrc = computed(() => {
 | 
			
		|||
	return track ? track.song.sourceUrl : ''
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.isPlaying,
 | 
			
		||||
	(newValue) => {
 | 
			
		||||
		if (newValue) {
 | 
			
		||||
			player.value?.play()
 | 
			
		||||
			setMetadata()
 | 
			
		||||
		} else {
 | 
			
		||||
			player.value?.pause()
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
watch(() => playQueueStore.isPlaying, (newValue) => {
 | 
			
		||||
	if (newValue) {
 | 
			
		||||
		player.value?.play()
 | 
			
		||||
		setMetadata()
 | 
			
		||||
	}
 | 
			
		||||
	else { player.value?.pause() }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听当前索引变化,处理预加载逻辑
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.currentIndex,
 | 
			
		||||
	async () => {
 | 
			
		||||
		console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
 | 
			
		||||
watch(() => playQueueStore.currentIndex, async () => {
 | 
			
		||||
	console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
 | 
			
		||||
 | 
			
		||||
		// 检查是否可以使用预加载的音频
 | 
			
		||||
		const track = currentTrack.value
 | 
			
		||||
		if (track) {
 | 
			
		||||
			const songId = track.song.cid
 | 
			
		||||
	// 检查是否可以使用预加载的音频
 | 
			
		||||
	const track = currentTrack.value
 | 
			
		||||
	if (track) {
 | 
			
		||||
		const songId = track.song.cid
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
				// 首先检查和刷新当前歌曲的资源
 | 
			
		||||
				console.log('[Player] 检查当前歌曲资源:', track.song.name)
 | 
			
		||||
				const updatedSong = await checkAndRefreshSongResource(
 | 
			
		||||
					track.song,
 | 
			
		||||
					(updated) => {
 | 
			
		||||
						// 更新播放队列中的歌曲信息
 | 
			
		||||
						// 在随机播放模式下,currentIndex 是 shuffleList 的索引
 | 
			
		||||
						// 需要通过 shuffleList[currentIndex] 获取实际的 list 索引
 | 
			
		||||
						const actualIndex =
 | 
			
		||||
							playQueueStore.playMode.shuffle &&
 | 
			
		||||
							playQueueStore.shuffleList.length > 0
 | 
			
		||||
								? playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
								: playQueueStore.currentIndex
 | 
			
		||||
						if (playQueueStore.list[actualIndex]) {
 | 
			
		||||
							playQueueStore.list[actualIndex].song = updated
 | 
			
		||||
						}
 | 
			
		||||
						// 如果歌曲在收藏夹中,也更新收藏夹
 | 
			
		||||
						favourites.updateSongInFavourites(songId, updated)
 | 
			
		||||
					},
 | 
			
		||||
				)
 | 
			
		||||
		try {
 | 
			
		||||
			const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
 | 
			
		||||
 | 
			
		||||
				// 使用更新后的歌曲信息
 | 
			
		||||
				const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
 | 
			
		||||
			if (preloadedAudio) {
 | 
			
		||||
				console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
 | 
			
		||||
 | 
			
		||||
				if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) {
 | 
			
		||||
					console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
 | 
			
		||||
				// 直接使用预加载的音频数据
 | 
			
		||||
				if (player.value) {
 | 
			
		||||
					// 复制预加载音频的状态到主播放器
 | 
			
		||||
					player.value.src = preloadedAudio.src
 | 
			
		||||
					player.value.currentTime = 0
 | 
			
		||||
 | 
			
		||||
					// 直接使用预加载的音频数据
 | 
			
		||||
					if (player.value) {
 | 
			
		||||
						// 复制预加载音频的状态到主播放器
 | 
			
		||||
						player.value.src = preloadedAudio.src
 | 
			
		||||
						player.value.currentTime = 0
 | 
			
		||||
					// 清理使用过的预加载音频
 | 
			
		||||
					playQueueStore.clearPreloadedAudio(songId)
 | 
			
		||||
 | 
			
		||||
						// 清理使用过的预加载音频
 | 
			
		||||
						playQueueStore.clearPreloadedAudio(songId)
 | 
			
		||||
 | 
			
		||||
						// 如果正在播放状态,立即播放
 | 
			
		||||
						if (playQueueStore.isPlaying) {
 | 
			
		||||
							await nextTick()
 | 
			
		||||
							player.value.play().catch(console.error)
 | 
			
		||||
						}
 | 
			
		||||
 | 
			
		||||
						playQueueStore.isBuffering = false
 | 
			
		||||
					// 如果正在播放状态,立即播放
 | 
			
		||||
					if (playQueueStore.isPlaying) {
 | 
			
		||||
						await nextTick()
 | 
			
		||||
						player.value.play().catch(console.error)
 | 
			
		||||
					}
 | 
			
		||||
				} else {
 | 
			
		||||
					console.log(`[Player] 正常加载音频: ${track.song.name}`)
 | 
			
		||||
					playQueueStore.isBuffering = true
 | 
			
		||||
 | 
			
		||||
					// 如果资源地址已更新,清除旧的预加载音频
 | 
			
		||||
					if (updatedSong.sourceUrl !== track.song.sourceUrl) {
 | 
			
		||||
						playQueueStore.clearPreloadedAudio(songId)
 | 
			
		||||
					}
 | 
			
		||||
					playQueueStore.isBuffering = false
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error('[Player] 处理预加载音频时出错:', error)
 | 
			
		||||
			} else {
 | 
			
		||||
				console.log(`[Player] 正常加载音频: ${track.song.name}`)
 | 
			
		||||
				playQueueStore.isBuffering = true
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('[Player] 处理预加载音频时出错:', error)
 | 
			
		||||
			playQueueStore.isBuffering = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		setMetadata()
 | 
			
		||||
	setMetadata()
 | 
			
		||||
 | 
			
		||||
		// 延迟预加载下一首歌,避免影响当前歌曲加载
 | 
			
		||||
		setTimeout(async () => {
 | 
			
		||||
			try {
 | 
			
		||||
				console.log('[Player] 尝试预加载下一首歌')
 | 
			
		||||
	// 延迟预加载下一首歌,避免影响当前歌曲加载
 | 
			
		||||
	setTimeout(() => {
 | 
			
		||||
		try {
 | 
			
		||||
			console.log('[Player] 尝试预加载下一首歌')
 | 
			
		||||
 | 
			
		||||
				// 检查函数是否存在
 | 
			
		||||
				if (typeof playQueueStore.preloadNext === 'function') {
 | 
			
		||||
					await playQueueStore.preloadNext()
 | 
			
		||||
 | 
			
		||||
					// 预加载完成后,检查播放队列是否有更新,同步到收藏夹
 | 
			
		||||
					playQueueStore.list.forEach((item) => {
 | 
			
		||||
						if (favourites.isFavourite(item.song.cid)) {
 | 
			
		||||
							favourites.updateSongInFavourites(item.song.cid, item.song)
 | 
			
		||||
						}
 | 
			
		||||
					})
 | 
			
		||||
 | 
			
		||||
					playQueueStore.limitPreloadCache()
 | 
			
		||||
				} else {
 | 
			
		||||
					console.error('[Player] preloadNext 不是一个函数')
 | 
			
		||||
				}
 | 
			
		||||
			} catch (error) {
 | 
			
		||||
				console.error('[Player] 预加载失败:', error)
 | 
			
		||||
			// 检查函数是否存在
 | 
			
		||||
			if (typeof playQueueStore.preloadNext === 'function') {
 | 
			
		||||
				playQueueStore.preloadNext()
 | 
			
		||||
				playQueueStore.limitPreloadCache()
 | 
			
		||||
			} else {
 | 
			
		||||
				console.error('[Player] preloadNext 不是一个函数')
 | 
			
		||||
			}
 | 
			
		||||
		}, 1000)
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('[Player] 预加载失败:', error)
 | 
			
		||||
		}
 | 
			
		||||
	}, 1000)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function artistsOrganize(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(' / ')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setMetadata() {
 | 
			
		||||
	if ('mediaSession' in navigator) {
 | 
			
		||||
		const current = currentTrack.value
 | 
			
		||||
		let current = currentTrack.value
 | 
			
		||||
		if (!current) return
 | 
			
		||||
 | 
			
		||||
		navigator.mediaSession.metadata = new MediaMetadata({
 | 
			
		||||
| 
						 | 
				
			
			@ -175,12 +122,8 @@ function setMetadata() {
 | 
			
		|||
			artist: artistsOrganize(current.song.artists ?? []),
 | 
			
		||||
			album: current.album?.name,
 | 
			
		||||
			artwork: [
 | 
			
		||||
				{
 | 
			
		||||
					src: current.album?.coverUrl ?? '',
 | 
			
		||||
					sizes: '500x500',
 | 
			
		||||
					type: 'image/png',
 | 
			
		||||
				},
 | 
			
		||||
			],
 | 
			
		||||
				{ src: current.album?.coverUrl ?? '', sizes: '500x500', type: 'image/png' },
 | 
			
		||||
			]
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
 | 
			
		||||
| 
						 | 
				
			
			@ -190,21 +133,16 @@ function setMetadata() {
 | 
			
		|||
		playQueueStore.currentTime = player.value?.currentTime ?? 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	watch(
 | 
			
		||||
		() => playQueueStore.updatedCurrentTime,
 | 
			
		||||
		(newValue) => {
 | 
			
		||||
			if (newValue === null) {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if (player.value) player.value.currentTime = newValue
 | 
			
		||||
			playQueueStore.updatedCurrentTime = null
 | 
			
		||||
		},
 | 
			
		||||
	)
 | 
			
		||||
	watch(() => playQueueStore.updatedCurrentTime, (newValue) => {
 | 
			
		||||
		if (newValue === null) { return }
 | 
			
		||||
		if (player.value) player.value.currentTime = newValue
 | 
			
		||||
		playQueueStore.updatedCurrentTime = null
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function playNext() {
 | 
			
		||||
	if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
 | 
			
		||||
		console.log('at the bottom, pause')
 | 
			
		||||
		console.log("at the bottom, pause")
 | 
			
		||||
		playQueueStore.currentIndex = 0
 | 
			
		||||
		if (playQueueStore.playMode.repeat === 'all') {
 | 
			
		||||
			playQueueStore.currentIndex = 0
 | 
			
		||||
| 
						 | 
				
			
			@ -220,17 +158,11 @@ function playNext() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function playPrevious() {
 | 
			
		||||
	if (
 | 
			
		||||
		player.value &&
 | 
			
		||||
		(player.value.currentTime ?? 0) < 5 &&
 | 
			
		||||
		playQueueStore.currentIndex > 0
 | 
			
		||||
	) {
 | 
			
		||||
	if (player.value && (player.value.currentTime ?? 0) < 5 && playQueueStore.currentIndex > 0) {
 | 
			
		||||
		playQueueStore.currentIndex--
 | 
			
		||||
		playQueueStore.isPlaying = true
 | 
			
		||||
	} else {
 | 
			
		||||
		if (player.value) {
 | 
			
		||||
			player.value.currentTime = 0
 | 
			
		||||
		}
 | 
			
		||||
		if (player.value) { player.value.currentTime = 0 }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -247,10 +179,8 @@ function updateCurrentTime() {
 | 
			
		|||
		const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
 | 
			
		||||
		const remainingTimeThreshold = config.remainingTimeThreshold || 30
 | 
			
		||||
 | 
			
		||||
		if (
 | 
			
		||||
			(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
 | 
			
		||||
			!playQueueStore.isPreloading
 | 
			
		||||
		) {
 | 
			
		||||
		if ((progress > preloadTrigger || remainingTime < remainingTimeThreshold) && !playQueueStore.isPreloading) {
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
				if (typeof playQueueStore.preloadNext === 'function') {
 | 
			
		||||
					playQueueStore.preloadNext()
 | 
			
		||||
| 
						 | 
				
			
			@ -264,160 +194,115 @@ function updateCurrentTime() {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 检查浏览器是否支持音频可视化
 | 
			
		||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
 | 
			
		||||
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported)
 | 
			
		||||
console.log('[Player] 初始化 audioVisualizer')
 | 
			
		||||
const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
 | 
			
		||||
	sensitivity: 1.5,
 | 
			
		||||
	barCount: 6,
 | 
			
		||||
	maxDecibels: -10,
 | 
			
		||||
	bassBoost: 0.8,
 | 
			
		||||
	midBoost: 1.2,
 | 
			
		||||
	trebleBoost: 1.4,
 | 
			
		||||
	threshold: 0
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 只在支持的浏览器上初始化音频可视化
 | 
			
		||||
let barHeights = ref<number[]>([0, 0, 0, 0, 0, 0])
 | 
			
		||||
let connectAudio = (_audio: HTMLAudioElement) => {}
 | 
			
		||||
let isAnalyzing = ref(false)
 | 
			
		||||
let error = ref<string | null>(null)
 | 
			
		||||
 | 
			
		||||
if (isAudioVisualizationSupported) {
 | 
			
		||||
	console.log('[Player] 初始化 audioVisualizer')
 | 
			
		||||
	const visualizer = audioVisualizer({
 | 
			
		||||
		sensitivity: 1.5,
 | 
			
		||||
		barCount: 6,
 | 
			
		||||
		maxDecibels: -10,
 | 
			
		||||
		bassBoost: 0.8,
 | 
			
		||||
		midBoost: 1.2,
 | 
			
		||||
		trebleBoost: 1.4,
 | 
			
		||||
		threshold: 0,
 | 
			
		||||
	})
 | 
			
		||||
	
 | 
			
		||||
	barHeights = visualizer.barHeights
 | 
			
		||||
	connectAudio = visualizer.connectAudio
 | 
			
		||||
	isAnalyzing = visualizer.isAnalyzing
 | 
			
		||||
	error = visualizer.error
 | 
			
		||||
	
 | 
			
		||||
	console.log('[Player] audioVisualizer 返回值:', {
 | 
			
		||||
		barHeights: barHeights.value,
 | 
			
		||||
		isAnalyzing: isAnalyzing.value,
 | 
			
		||||
	})
 | 
			
		||||
} else {
 | 
			
		||||
	console.log('[Player] 音频可视化被禁用(Safari 或不支持的浏览器)')
 | 
			
		||||
}
 | 
			
		||||
console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value })
 | 
			
		||||
 | 
			
		||||
// 监听播放列表变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.list.length,
 | 
			
		||||
	async (newLength) => {
 | 
			
		||||
		console.log('[Player] 播放列表长度变化:', newLength)
 | 
			
		||||
		if (newLength === 0) {
 | 
			
		||||
			console.log('[Player] 播放列表为空,跳过连接')
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
watch(() => playQueueStore.list.length, async (newLength) => {
 | 
			
		||||
	console.log('[Player] 播放列表长度变化:', newLength)
 | 
			
		||||
	if (newLength === 0) {
 | 
			
		||||
		console.log('[Player] 播放列表为空,跳过连接')
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		// 等待下一帧,确保 audio 元素已经渲染
 | 
			
		||||
		await nextTick()
 | 
			
		||||
	// 等待下一帧,确保 audio 元素已经渲染
 | 
			
		||||
	await nextTick()
 | 
			
		||||
 | 
			
		||||
		if (player.value) {
 | 
			
		||||
			if (isAudioVisualizationSupported) {
 | 
			
		||||
				console.log('[Player] 连接音频元素到可视化器')
 | 
			
		||||
				console.log('[Player] 音频元素状态:', {
 | 
			
		||||
					src: player.value.src?.substring(0, 50) + '...',
 | 
			
		||||
					readyState: player.value.readyState,
 | 
			
		||||
					paused: player.value.paused,
 | 
			
		||||
				})
 | 
			
		||||
				connectAudio(player.value)
 | 
			
		||||
			} else {
 | 
			
		||||
				console.log('[Player] 跳过音频可视化连接(不支持的浏览器)')
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			console.log('[Player] ❌ 音频元素不存在')
 | 
			
		||||
		}
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		console.log('[Player] 连接音频元素到可视化器')
 | 
			
		||||
		console.log('[Player] 音频元素状态:', {
 | 
			
		||||
			src: player.value.src?.substring(0, 50) + '...',
 | 
			
		||||
			readyState: player.value.readyState,
 | 
			
		||||
			paused: player.value.paused
 | 
			
		||||
		})
 | 
			
		||||
		connectAudio(player.value)
 | 
			
		||||
	} else {
 | 
			
		||||
		console.log('[Player] ❌ 音频元素不存在')
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
		playQueueStore.visualizer = barHeights.value
 | 
			
		||||
	playQueueStore.visualizer = barHeights.value
 | 
			
		||||
 | 
			
		||||
		// 开始预加载第一首歌的下一首
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			playQueueStore.preloadNext()
 | 
			
		||||
		}, 2000)
 | 
			
		||||
	// 开始预加载第一首歌的下一首
 | 
			
		||||
	setTimeout(() => {
 | 
			
		||||
		playQueueStore.preloadNext()
 | 
			
		||||
	}, 2000)
 | 
			
		||||
 | 
			
		||||
		// 初始化音量
 | 
			
		||||
		if (player.value) {
 | 
			
		||||
			initializeVolume()
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
	// 初始化音量
 | 
			
		||||
	if (player.value) {
 | 
			
		||||
		initializeVolume()
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听音频元素变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => player.value,
 | 
			
		||||
	(audioElement) => {
 | 
			
		||||
		if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) {
 | 
			
		||||
			connectAudio(audioElement)
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
watch(() => player.value, (audioElement) => {
 | 
			
		||||
	if (audioElement && playQueueStore.list.length > 0) {
 | 
			
		||||
		connectAudio(audioElement)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 监听可视化器数据变化
 | 
			
		||||
watch(
 | 
			
		||||
	() => barHeights.value,
 | 
			
		||||
	(newHeights) => {
 | 
			
		||||
		playQueueStore.visualizer = newHeights
 | 
			
		||||
	},
 | 
			
		||||
	{ deep: true },
 | 
			
		||||
)
 | 
			
		||||
watch(() => barHeights.value, (newHeights) => {
 | 
			
		||||
	playQueueStore.visualizer = newHeights
 | 
			
		||||
}, { deep: true })
 | 
			
		||||
 | 
			
		||||
// 监听错误
 | 
			
		||||
watch(
 | 
			
		||||
	() => error.value,
 | 
			
		||||
	(newError) => {
 | 
			
		||||
		if (newError) {
 | 
			
		||||
			console.error('[Player] 可视化器错误:', newError)
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
watch(() => error.value, (newError) => {
 | 
			
		||||
	if (newError) {
 | 
			
		||||
		console.error('[Player] 可视化器错误:', newError)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 切换播放模式
 | 
			
		||||
watch(
 | 
			
		||||
	() => playQueueStore.playMode.shuffle,
 | 
			
		||||
	(isShuffle) => {
 | 
			
		||||
		if (isShuffle) {
 | 
			
		||||
			const currentIndex = playQueueStore.currentIndex
 | 
			
		||||
			const trackCount = playQueueStore.list.length
 | 
			
		||||
watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
 | 
			
		||||
	if (isShuffle) {
 | 
			
		||||
		const currentIndex = playQueueStore.currentIndex
 | 
			
		||||
		const trackCount = playQueueStore.list.length
 | 
			
		||||
 | 
			
		||||
			// 1. 已播放部分:不变
 | 
			
		||||
			let shuffledList = [...Array(currentIndex).keys()]
 | 
			
		||||
		// 1. 已播放部分:不变
 | 
			
		||||
		let shuffledList = [...Array(currentIndex).keys()]
 | 
			
		||||
 | 
			
		||||
			// 2. 构建待打乱的列表
 | 
			
		||||
			const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
 | 
			
		||||
				playQueueStore.shuffleCurrent
 | 
			
		||||
					? index >= currentIndex
 | 
			
		||||
					: index > currentIndex,
 | 
			
		||||
			)
 | 
			
		||||
		// 2. 构建待打乱的列表
 | 
			
		||||
		let shuffleSpace = [...Array(trackCount).keys()].filter(index =>
 | 
			
		||||
			playQueueStore.shuffleCurrent ? index >= currentIndex : index > currentIndex
 | 
			
		||||
		)
 | 
			
		||||
 | 
			
		||||
			// 3. 随机打乱
 | 
			
		||||
			shuffleSpace.sort(() => Math.random() - 0.5)
 | 
			
		||||
		// 3. 随机打乱
 | 
			
		||||
		shuffleSpace.sort(() => Math.random() - 0.5)
 | 
			
		||||
 | 
			
		||||
			// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
 | 
			
		||||
			if (!playQueueStore.shuffleCurrent) {
 | 
			
		||||
				shuffledList.push(currentIndex)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
 | 
			
		||||
			shuffledList = shuffledList.concat(shuffleSpace)
 | 
			
		||||
 | 
			
		||||
			// 6. 应用 shuffleList
 | 
			
		||||
			playQueueStore.shuffleList = shuffledList
 | 
			
		||||
 | 
			
		||||
			// 清除 shuffleCurrent 状态
 | 
			
		||||
			playQueueStore.shuffleCurrent = undefined
 | 
			
		||||
		} else {
 | 
			
		||||
			// 退出随机播放:恢复当前播放曲目的原索引
 | 
			
		||||
			playQueueStore.currentIndex =
 | 
			
		||||
				playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
		// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
 | 
			
		||||
		if (!playQueueStore.shuffleCurrent) {
 | 
			
		||||
			shuffledList.push(currentIndex)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 切换播放模式后重新预加载
 | 
			
		||||
		setTimeout(() => {
 | 
			
		||||
			playQueueStore.clearAllPreloadedAudio()
 | 
			
		||||
			playQueueStore.preloadNext()
 | 
			
		||||
		}, 500)
 | 
			
		||||
	},
 | 
			
		||||
)
 | 
			
		||||
		// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
 | 
			
		||||
		shuffledList = shuffledList.concat(shuffleSpace)
 | 
			
		||||
 | 
			
		||||
		// 6. 应用 shuffleList
 | 
			
		||||
		playQueueStore.shuffleList = shuffledList
 | 
			
		||||
 | 
			
		||||
		// 清除 shuffleCurrent 状态
 | 
			
		||||
		playQueueStore.shuffleCurrent = undefined
 | 
			
		||||
	} else {
 | 
			
		||||
		// 退出随机播放:恢复当前播放曲目的原索引
 | 
			
		||||
		playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 切换播放模式后重新预加载
 | 
			
		||||
	setTimeout(() => {
 | 
			
		||||
		playQueueStore.clearAllPreloadedAudio()
 | 
			
		||||
		playQueueStore.preloadNext()
 | 
			
		||||
	}, 500)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function getCurrentTrack() {
 | 
			
		||||
	return currentTrack.value
 | 
			
		||||
| 
						 | 
				
			
			@ -428,7 +313,7 @@ function initializeVolume() {
 | 
			
		|||
	if (player.value) {
 | 
			
		||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
		if (savedVolume) {
 | 
			
		||||
			const volumeValue = Number.parseFloat(savedVolume)
 | 
			
		||||
			const volumeValue = parseFloat(savedVolume)
 | 
			
		||||
			player.value.volume = volumeValue
 | 
			
		||||
			console.log('[Player] 初始化音量:', volumeValue)
 | 
			
		||||
		} else {
 | 
			
		||||
| 
						 | 
				
			
			@ -454,7 +339,7 @@ function syncVolumeFromStorage() {
 | 
			
		|||
	if (player.value) {
 | 
			
		||||
		const savedVolume = localStorage.getItem('audioVolume')
 | 
			
		||||
		if (savedVolume) {
 | 
			
		||||
			const volumeValue = Number.parseFloat(savedVolume)
 | 
			
		||||
			const volumeValue = parseFloat(savedVolume)
 | 
			
		||||
			if (player.value.volume !== volumeValue) {
 | 
			
		||||
				player.value.volume = volumeValue
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -510,7 +395,7 @@ setInterval(syncVolumeFromStorage, 100)
 | 
			
		|||
 | 
			
		||||
			<RouterLink to="/playroom">
 | 
			
		||||
				<div class="flex items-center w-32 h-9">
 | 
			
		||||
					<span class="truncate text-xs">{{ getCurrentTrack()?.song.name }}</span>
 | 
			
		||||
					<span class="truncate">{{ getCurrentTrack()?.song.name }}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			</RouterLink>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -519,14 +404,12 @@ setInterval(syncVolumeFromStorage, 100)
 | 
			
		|||
			}">
 | 
			
		||||
				<div v-if="playQueueStore.isPlaying">
 | 
			
		||||
					<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
 | 
			
		||||
					<!-- 在支持的浏览器上显示可视化,否则显示暂停图标 -->
 | 
			
		||||
					<div v-else-if="isAudioVisualizationSupported" class="h-4 flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
					<div v-else class="h-4 flex justify-center items-center gap-[.125rem]">
 | 
			
		||||
						<div class="bg-white/75 w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
 | 
			
		||||
							:key="index" :style="{
 | 
			
		||||
								height: `${Math.max(10, bar)}%`
 | 
			
		||||
							}" />
 | 
			
		||||
					</div>
 | 
			
		||||
					<PauseIcon v-else :size="4" />
 | 
			
		||||
				</div>
 | 
			
		||||
				<PlayIcon v-else :size="4" />
 | 
			
		||||
			</button>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,7 +31,7 @@ const version = computed(() => {
 | 
			
		|||
			class="bg-black/30 w-screen h-screen absolute top-0 left-0 z-30 flex justify-center items-center select-none"
 | 
			
		||||
			@click="$emit('dismiss')">
 | 
			
		||||
			<div
 | 
			
		||||
				class="bg-neutral-900/80 shadow-[0_0_16px_0_rgba(0,0,0,0.5)] backdrop-blur-2xl border border-[#ffffff39] rounded-lg w-[60rem] h-3/4 relative overflow-y-auto modal-content"
 | 
			
		||||
				class="bg-neutral-900/80 shadow-[0_0_16px_0_rgba(0,0,0,0.5)] backdrop-blur-2xl border border-[#ffffff39] rounded-lg w-[60rem] h-3/4 relative overflow-scroll modal-content"
 | 
			
		||||
				@click.stop>
 | 
			
		||||
				<div
 | 
			
		||||
					class="flex justify-between items-center p-8 sticky top-0 bg-gradient-to-b from-neutral-900 to-neutral-900/0 z-10">
 | 
			
		||||
| 
						 | 
				
			
			@ -126,27 +126,7 @@ const version = computed(() => {
 | 
			
		|||
										class="flex justify-between items-center px-6 py-4 w-full text-left hover:bg-neutral-300/10 transition-all">
 | 
			
		||||
										<div class="flex flex-col">
 | 
			
		||||
											<div class="text-base text-white">前往 GitHub 仓库</div>
 | 
			
		||||
											<div class="text-sm text-white/80">在 Issue 中提交问题或建议,或者,修 Bug 的事情就拜托了,大佬桑!(鞠躬)</div>
 | 
			
		||||
										</div>
 | 
			
		||||
									</a>
 | 
			
		||||
								</li>
 | 
			
		||||
 | 
			
		||||
								<li class="odd:bg-neutral-300/5">
 | 
			
		||||
									<a href="https://discord.gg/QQUfeb2gzH" target="_blank"
 | 
			
		||||
										class="flex justify-between items-center px-6 py-4 w-full text-left hover:bg-neutral-300/10 transition-all">
 | 
			
		||||
										<div class="flex flex-col">
 | 
			
		||||
											<div class="text-base text-white">前往 Discord 社群</div>
 | 
			
		||||
											<div class="text-sm text-white/80">在社群中提交问题或建议,或是来聊聊《明日方舟》的音乐吧!</div>
 | 
			
		||||
										</div>
 | 
			
		||||
									</a>
 | 
			
		||||
								</li>
 | 
			
		||||
 | 
			
		||||
								<li class="odd:bg-neutral-300/5">
 | 
			
		||||
									<a href="https://trello.com/b/Ju1TRXla" target="_blank"
 | 
			
		||||
										class="flex justify-between items-center px-6 py-4 w-full text-left hover:bg-neutral-300/10 transition-all">
 | 
			
		||||
										<div class="flex flex-col">
 | 
			
		||||
											<div class="text-base text-white">前往 Trello 看板</div>
 | 
			
		||||
											<div class="text-sm text-white/80">了解 MSR Mod 目前的开发进度。</div>
 | 
			
		||||
											<div class="text-sm text-white/80">修 Bug 的事情就拜托了,大佬桑!(鞠躬)</div>
 | 
			
		||||
										</div>
 | 
			
		||||
									</a>
 | 
			
		||||
								</li>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -543,39 +543,8 @@ watch(() => props.lrcSrc, async (newSrc) => {
 | 
			
		|||
	}
 | 
			
		||||
}, { immediate: true })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 页面焦点处理函数变量声明
 | 
			
		||||
let handleVisibilityChange: (() => void) | null = null
 | 
			
		||||
 | 
			
		||||
// 页面焦点处理
 | 
			
		||||
function setupPageFocusHandlers() {
 | 
			
		||||
	handleVisibilityChange = () => {
 | 
			
		||||
		if (document.hidden) {
 | 
			
		||||
			// 页面失去焦点时暂停动画
 | 
			
		||||
			if (scrollTween) scrollTween.pause()
 | 
			
		||||
			if (highlightTween) highlightTween.pause()
 | 
			
		||||
		} else {
 | 
			
		||||
			// 页面重新获得焦点时恢复并重新同步
 | 
			
		||||
			if (scrollTween && scrollTween.paused()) scrollTween.resume()
 | 
			
		||||
			if (highlightTween && highlightTween.paused()) highlightTween.resume()
 | 
			
		||||
			
 | 
			
		||||
			// 重新同步歌词位置
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				if (currentLineIndex.value >= 0 && autoScroll.value && !userScrolling.value) {
 | 
			
		||||
					scrollToLine(currentLineIndex.value, false) // 不使用动画,直接定位
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	document.addEventListener('visibilitychange', handleVisibilityChange)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件挂载时的入场动画
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	// 设置页面焦点处理
 | 
			
		||||
	setupPageFocusHandlers()
 | 
			
		||||
 | 
			
		||||
	// 控制面板入场动画
 | 
			
		||||
	if (controlPanel.value) {
 | 
			
		||||
		gsap.fromTo(controlPanel.value,
 | 
			
		||||
| 
						 | 
				
			
			@ -608,11 +577,6 @@ onUnmounted(() => {
 | 
			
		|||
	if (scrollTween) scrollTween.kill()
 | 
			
		||||
	if (highlightTween) highlightTween.kill()
 | 
			
		||||
	if (userScrollTimeout) clearTimeout(userScrollTimeout)
 | 
			
		||||
	
 | 
			
		||||
	// 清理页面焦点事件监听器
 | 
			
		||||
	if (handleVisibilityChange) {
 | 
			
		||||
		document.removeEventListener('visibilitychange', handleVisibilityChange)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 暴露方法给父组件
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,57 +0,0 @@
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import XIcon from '../assets/icons/x.vue'
 | 
			
		||||
import { ref, onMounted } from 'vue'
 | 
			
		||||
import { useUpdatePopup } from '../stores/useUpdatePopup'
 | 
			
		||||
 | 
			
		||||
const updatePopupStore = useUpdatePopup()
 | 
			
		||||
const showPopup = ref(false)
 | 
			
		||||
 | 
			
		||||
const version = updatePopupStore.getCurrentVersion()
 | 
			
		||||
 | 
			
		||||
// 关闭弹窗的处理函数
 | 
			
		||||
const handleDismiss = async () => {
 | 
			
		||||
	showPopup.value = false
 | 
			
		||||
	// 标记弹窗已显示,避免再次显示
 | 
			
		||||
	await updatePopupStore.markUpdatePopupShown()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件挂载时检查是否需要显示弹窗
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
	// 等待 store 初始化完成
 | 
			
		||||
	if (!updatePopupStore.isLoaded) {
 | 
			
		||||
		await updatePopupStore.initializeUpdatePopup()
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	// 检查是否需要显示更新弹窗
 | 
			
		||||
	const shouldShow = await updatePopupStore.shouldShowUpdatePopup()
 | 
			
		||||
	showPopup.value = shouldShow
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
	<div v-if="showPopup" class="absolute top-0 left-0 w-screen h-screen bg-neutral-700/30 flex justify-center items-center select-none z-50">
 | 
			
		||||
		<div class="bg-neutral-900/80 shadow-[0_0_16px_0_rgba(0,0,0,0.5)] backdrop-blur-2xl border border-[#ffffff39] rounded-lg w-[60rem] h-3/4 relative overflow-y-auto text-white">
 | 
			
		||||
			<div
 | 
			
		||||
				class="flex justify-between items-center p-8 sticky top-0 bg-gradient-to-b from-neutral-900 to-neutral-900/0 z-10">
 | 
			
		||||
				<div class="text-white text-2xl font-semibold">MSR Mod 已更新至 {{version}}</div>
 | 
			
		||||
				<button
 | 
			
		||||
					class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:bg-neutral-700/80"
 | 
			
		||||
					@click="handleDismiss">
 | 
			
		||||
					<XIcon :size="4" />
 | 
			
		||||
				</button>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<div class="flex flex-col gap-4 mb-8 px-8 text-lg">
 | 
			
		||||
				<p>最近两周有家事,同时我的 MacBook Pro 的 MagSafe 出了故障,还在 Genius Bar 维修,所以开发进程受到了一些影响。</p>
 | 
			
		||||
				<p>MSR Mod 现在有两种渠道接收错误及意见反馈。如果你对 MSR Mod 有任何的意见建议,或是想要回报错误及体验困惑之处,欢迎前往 <a href="https://github.com/Astrian/msr-mod/issues" target="_blank" class="underline">GitHub Issue</a> 或 <a href="https://discord.gg/QQUfeb2gzH" target="_blank" class="underline">Discord 社群</a> 向我们反馈。如果你的意见或错误回报被接受,我们会将其放入 <a href="https://trello.com/b/Ju1TRXla" target="_blank" class="underline">Trello 看板</a> 中进行跟踪,敬请留意。</p>
 | 
			
		||||
 | 
			
		||||
				<ul class="list-disc list-inside">
 | 
			
		||||
					<li>新增版本更新提示对话框,将在 MSR Mod 更新后首次启动显示。</li>
 | 
			
		||||
					<li>增强对 Apple Safari 浏览器的兼容性支持。</li>
 | 
			
		||||
					<li>修复 Windows 和其他操作系统中偏好设置面板滚动条显示异常的问题。</li>
 | 
			
		||||
					<li>在偏好设置面板中新增 Discord 社群和 Trello 看板链接。</li>
 | 
			
		||||
				</ul>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -83,9 +83,6 @@ onMounted(async () => {
 | 
			
		|||
	thumbUpdate()
 | 
			
		||||
 | 
			
		||||
	setupEntranceAnimations()
 | 
			
		||||
 | 
			
		||||
	// 添加页面焦点事件监听
 | 
			
		||||
	setupPageFocusHandlers()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function timeFormatter(time: number) {
 | 
			
		||||
| 
						 | 
				
			
			@ -93,7 +90,7 @@ function timeFormatter(time: number) {
 | 
			
		|||
	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 (isNaN(minutes) || isNaN(seconds)) { return '-:--' }
 | 
			
		||||
	return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +155,7 @@ function createVolumeDraggable() {
 | 
			
		|||
			// 保存音量到localStorage
 | 
			
		||||
			localStorage.setItem('audioVolume', newVolume.toString())
 | 
			
		||||
		},
 | 
			
		||||
		onDragEnd: () => {
 | 
			
		||||
		onDragEnd: function () {
 | 
			
		||||
			// 拖拽结束时也保存一次
 | 
			
		||||
			localStorage.setItem('audioVolume', volume.value.toString())
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -422,98 +419,9 @@ watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newV
 | 
			
		|||
	}
 | 
			
		||||
}, { immediate: true })
 | 
			
		||||
 | 
			
		||||
// 页面焦点处理函数变量声明
 | 
			
		||||
let handleVisibilityChange: (() => void) | null = null
 | 
			
		||||
let handlePageFocus: (() => void) | null = null
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	// 清理页面焦点事件监听器
 | 
			
		||||
	if (handleVisibilityChange) {
 | 
			
		||||
		document.removeEventListener('visibilitychange', handleVisibilityChange)
 | 
			
		||||
	}
 | 
			
		||||
	if (handlePageFocus) {
 | 
			
		||||
		window.removeEventListener('focus', handlePageFocus)
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 页面焦点处理函数
 | 
			
		||||
function setupPageFocusHandlers() {
 | 
			
		||||
	handleVisibilityChange = () => {
 | 
			
		||||
		if (document.hidden) {
 | 
			
		||||
			// 页面失去焦点时,暂停所有动画
 | 
			
		||||
			console.log('[Playroom] 页面失去焦点,暂停动画')
 | 
			
		||||
		} else {
 | 
			
		||||
			// 页面重新获得焦点时,重新同步状态
 | 
			
		||||
			console.log('[Playroom] 页面重新获得焦点,同步状态')
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				resyncLyricsState()
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	handlePageFocus = () => {
 | 
			
		||||
		console.log('[Playroom] 窗口获得焦点,同步状态')
 | 
			
		||||
		nextTick(() => {
 | 
			
		||||
			resyncLyricsState()
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 监听页面可见性变化
 | 
			
		||||
	document.addEventListener('visibilitychange', handleVisibilityChange)
 | 
			
		||||
	window.addEventListener('focus', handlePageFocus)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重新同步歌词状态
 | 
			
		||||
function resyncLyricsState() {
 | 
			
		||||
	const currentTrack = getCurrentTrack()
 | 
			
		||||
	if (!currentTrack) { return }
 | 
			
		||||
 | 
			
		||||
	console.log('[Playroom] 重新同步歌词状态')
 | 
			
		||||
 | 
			
		||||
	// 重置动画状态
 | 
			
		||||
	if (controllerRef.value) {
 | 
			
		||||
		gsap.set(controllerRef.value, { 
 | 
			
		||||
			marginLeft: '0rem', 
 | 
			
		||||
			marginRight: '0rem' 
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (lyricsSection.value) {
 | 
			
		||||
		gsap.set(lyricsSection.value, { 
 | 
			
		||||
			opacity: 1, 
 | 
			
		||||
			x: 0, 
 | 
			
		||||
			y: 0, 
 | 
			
		||||
			scale: 1 
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查当前歌词显示状态应该是什么
 | 
			
		||||
	const shouldShowLyrics = preferences.presentLyrics && currentTrack.song.lyricUrl ? true : false
 | 
			
		||||
 | 
			
		||||
	if (shouldShowLyrics !== presentLyrics.value) {
 | 
			
		||||
		console.log(`[Playroom] 歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`)
 | 
			
		||||
		
 | 
			
		||||
		// 直接设置状态,不触发动画
 | 
			
		||||
		presentLyrics.value = shouldShowLyrics
 | 
			
		||||
		
 | 
			
		||||
		// 如果需要显示歌词,重新执行显示动画
 | 
			
		||||
		if (shouldShowLyrics) {
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				const tl = gsap.timeline()
 | 
			
		||||
				tl.from(controllerRef.value, {
 | 
			
		||||
					marginRight: '-40rem',
 | 
			
		||||
					duration: 0.4,
 | 
			
		||||
					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"
 | 
			
		||||
				)
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New: Watch for track changes and animate
 | 
			
		||||
watch(() => playQueueStore.currentIndex, () => {
 | 
			
		||||
	if (albumCover.value) {
 | 
			
		||||
| 
						 | 
				
			
			@ -605,9 +513,9 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
			
		|||
 | 
			
		||||
						<div class="w-full flex justify-between">
 | 
			
		||||
							<!-- ...existing time display code... -->
 | 
			
		||||
							<div class="font-medium flex-1 text-left text-xs relative">
 | 
			
		||||
							<div class="font-medium flex-1 text-left relative">
 | 
			
		||||
								<span
 | 
			
		||||
									class="text-black blur-lg absolute top-0 text-xs">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
									class="text-black blur-lg absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
								<span
 | 
			
		||||
									class="text-white/90 absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
 | 
			
		||||
							</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -618,7 +526,7 @@ watch(() => playQueueStore.currentIndex, () => {
 | 
			
		|||
							<div class="flex flex-1">
 | 
			
		||||
								<div class="flex-1" />
 | 
			
		||||
								<button
 | 
			
		||||
									class="text-white/90 text-xs font-medium text-right relative transition-colors duration-200 hover:text-white"
 | 
			
		||||
									class="text-white/90 font-medium text-right relative transition-colors duration-200 hover:text-white"
 | 
			
		||||
									@click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
 | 
			
		||||
									<span
 | 
			
		||||
										class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,5 @@
 | 
			
		|||
import { defineStore } from 'pinia'
 | 
			
		||||
import { computed, ref } from 'vue'
 | 
			
		||||
import { checkAndRefreshSongResource } from '../utils'
 | 
			
		||||
import { defineStore } from "pinia"
 | 
			
		||||
import { ref, computed } from "vue"
 | 
			
		||||
 | 
			
		||||
export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		||||
	const list = ref<QueueItem[]>([])
 | 
			
		||||
| 
						 | 
				
			
			@ -14,11 +13,11 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
	const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
 | 
			
		||||
	const shuffleList = ref<number[]>([])
 | 
			
		||||
	const playMode = ref<{
 | 
			
		||||
		shuffle: boolean
 | 
			
		||||
		shuffle: boolean,
 | 
			
		||||
		repeat: 'off' | 'single' | 'all'
 | 
			
		||||
	}>({
 | 
			
		||||
		shuffle: false,
 | 
			
		||||
		repeat: 'off',
 | 
			
		||||
		repeat: 'off'
 | 
			
		||||
	})
 | 
			
		||||
	const shuffleCurrent = ref<boolean | undefined>(undefined)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,13 +35,10 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		if (playMode.value.shuffle && shuffleList.value.length > 0) {
 | 
			
		||||
			// 当前在 shuffleList 中的位置
 | 
			
		||||
			const currentShuffleIndex = currentIndex.value
 | 
			
		||||
			const currentShuffleIndex = shuffleList.value.indexOf(currentIndex.value)
 | 
			
		||||
			if (currentShuffleIndex < shuffleList.value.length - 1) {
 | 
			
		||||
				// 返回下一个位置对应的原始 list 索引
 | 
			
		||||
				return shuffleList.value[currentShuffleIndex + 1]
 | 
			
		||||
			} else if (playMode.value.repeat === 'all') {
 | 
			
		||||
				// 返回第一个位置对应的原始 list 索引
 | 
			
		||||
				return shuffleList.value[0]
 | 
			
		||||
			}
 | 
			
		||||
			return -1
 | 
			
		||||
| 
						 | 
				
			
			@ -59,14 +55,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
 | 
			
		||||
	// 预加载下一首歌
 | 
			
		||||
	const preloadNext = async () => {
 | 
			
		||||
 | 
			
		||||
		const nextIndex = getNextIndex.value
 | 
			
		||||
		if (nextIndex === -1) {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 获取下一首歌曲对象
 | 
			
		||||
		// nextIndex 已经是原始 list 中的索引
 | 
			
		||||
		const nextSong = list.value[nextIndex]
 | 
			
		||||
		let nextSong
 | 
			
		||||
		if (playMode.value.shuffle && shuffleList.value.length > 0) {
 | 
			
		||||
			nextSong = list.value[shuffleList.value[nextIndex]]
 | 
			
		||||
		} else {
 | 
			
		||||
			nextSong = list.value[nextIndex]
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!nextSong || !nextSong.song) {
 | 
			
		||||
			return
 | 
			
		||||
| 
						 | 
				
			
			@ -88,24 +89,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
			isPreloading.value = true
 | 
			
		||||
			preloadProgress.value = 0
 | 
			
		||||
 | 
			
		||||
			// 在预加载前检查和刷新资源
 | 
			
		||||
			console.log('[Store] 预加载前检查资源:', nextSong.song.name)
 | 
			
		||||
			const updatedSong = await checkAndRefreshSongResource(
 | 
			
		||||
				nextSong.song,
 | 
			
		||||
				(updated) => {
 | 
			
		||||
					// 更新播放队列中的歌曲信息
 | 
			
		||||
					// nextIndex 已经是原始 list 中的索引
 | 
			
		||||
					if (list.value[nextIndex]) {
 | 
			
		||||
						list.value[nextIndex].song = updated
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// 如果歌曲在收藏夹中,也更新收藏夹
 | 
			
		||||
					// 注意:这里不直接导入 favourites store 以避免循环依赖
 | 
			
		||||
					// 改为触发一个事件或者在调用方处理
 | 
			
		||||
					console.log('[Store] 预加载时需要更新收藏夹:', updated.name)
 | 
			
		||||
				},
 | 
			
		||||
			)
 | 
			
		||||
 | 
			
		||||
			const audio = new Audio()
 | 
			
		||||
			audio.preload = 'auto'
 | 
			
		||||
			audio.crossOrigin = 'anonymous'
 | 
			
		||||
| 
						 | 
				
			
			@ -124,20 +107,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
				preloadedAudio.value.set(songId, audio)
 | 
			
		||||
				isPreloading.value = false
 | 
			
		||||
				preloadProgress.value = 100
 | 
			
		||||
				console.log('[Store] 预加载完成:', updatedSong.name)
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			// 监听加载错误
 | 
			
		||||
			audio.addEventListener('error', (e) => {
 | 
			
		||||
				console.error(`[Store] 预加载音频失败: ${updatedSong.name}`, e)
 | 
			
		||||
				console.error(`[Store] 预加载音频失败: ${e}`)
 | 
			
		||||
				isPreloading.value = false
 | 
			
		||||
				preloadProgress.value = 0
 | 
			
		||||
			})
 | 
			
		||||
 | 
			
		||||
			// 使用更新后的音频源
 | 
			
		||||
			audio.src = updatedSong.sourceUrl!
 | 
			
		||||
			// 设置音频源并开始加载
 | 
			
		||||
			audio.src = nextSong.song.sourceUrl
 | 
			
		||||
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('[Store] 预加载过程出错:', error)
 | 
			
		||||
			isPreloading.value = false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -185,7 +167,7 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
			progress: preloadProgress.value,
 | 
			
		||||
			cacheSize: preloadedAudio.value.size,
 | 
			
		||||
			cachedSongs: Array.from(preloadedAudio.value.keys()),
 | 
			
		||||
			nextIndex: getNextIndex.value,
 | 
			
		||||
			nextIndex: getNextIndex.value
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -212,6 +194,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
 | 
			
		|||
		clearPreloadedAudio,
 | 
			
		||||
		clearAllPreloadedAudio,
 | 
			
		||||
		limitPreloadCache,
 | 
			
		||||
		debugPreloadState,
 | 
			
		||||
		debugPreloadState
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,204 +0,0 @@
 | 
			
		|||
import { defineStore } from "pinia"
 | 
			
		||||
import { ref } from "vue"
 | 
			
		||||
 | 
			
		||||
// 声明全局类型
 | 
			
		||||
declare global {
 | 
			
		||||
	interface Window {
 | 
			
		||||
		browser?: any
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useUpdatePopup = defineStore('updatePopup', () => {
 | 
			
		||||
	const isLoaded = ref(false)
 | 
			
		||||
	const storageType = ref<'chrome' | 'localStorage' | 'memory'>('chrome')
 | 
			
		||||
 | 
			
		||||
	// 获取当前版本号
 | 
			
		||||
	const getCurrentVersion = (): string => {
 | 
			
		||||
		try {
 | 
			
		||||
			// 尝试从 Chrome 扩展 API 获取版本号
 | 
			
		||||
			return chrome?.runtime?.getManifest?.()?.version || 'unknown'
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			return 'unknown'
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检测可用的 API
 | 
			
		||||
	const detectAvailableAPIs = () => {
 | 
			
		||||
		// 检查原生 chrome API
 | 
			
		||||
		try {
 | 
			
		||||
			if (typeof chrome !== 'undefined' && chrome.storage && chrome.storage.sync) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			// Silent fail
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 检查 window.chrome
 | 
			
		||||
		try {
 | 
			
		||||
			if (window.chrome && window.chrome.storage && window.chrome.storage.sync) {
 | 
			
		||||
				storageType.value = 'chrome'
 | 
			
		||||
				return 'chrome'
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			// Silent fail
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 检查 localStorage
 | 
			
		||||
		try {
 | 
			
		||||
			if (typeof localStorage !== 'undefined') {
 | 
			
		||||
				localStorage.setItem('msr_test', 'test')
 | 
			
		||||
				localStorage.removeItem('msr_test')
 | 
			
		||||
				storageType.value = 'localStorage'
 | 
			
		||||
				return 'localStorage'
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			// Silent fail
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 都不可用,使用内存存储
 | 
			
		||||
		storageType.value = 'memory'
 | 
			
		||||
		return 'memory'
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 通用的获取存储值函数
 | 
			
		||||
	const getStoredValue = async (key: string, defaultValue: any) => {
 | 
			
		||||
		const type = detectAvailableAPIs()
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			switch (type) {
 | 
			
		||||
				case 'chrome':
 | 
			
		||||
					return await new Promise((resolve) => {
 | 
			
		||||
						const api = chrome?.storage?.sync || window.chrome?.storage?.sync
 | 
			
		||||
						if (api) {
 | 
			
		||||
							api.get({ [key]: defaultValue }, (result) => {
 | 
			
		||||
								if (chrome.runtime.lastError) {
 | 
			
		||||
									resolve(defaultValue)
 | 
			
		||||
								} else {
 | 
			
		||||
									resolve(result[key])
 | 
			
		||||
								}
 | 
			
		||||
							})
 | 
			
		||||
						} else {
 | 
			
		||||
							resolve(defaultValue)
 | 
			
		||||
						}
 | 
			
		||||
					})
 | 
			
		||||
 | 
			
		||||
				case 'localStorage':
 | 
			
		||||
					const stored = localStorage.getItem(`msr_${key}`)
 | 
			
		||||
					const value = stored ? JSON.parse(stored) : defaultValue
 | 
			
		||||
					return value
 | 
			
		||||
 | 
			
		||||
				case 'memory':
 | 
			
		||||
				default:
 | 
			
		||||
					return defaultValue
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			return defaultValue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 通用的设置存储值函数
 | 
			
		||||
	const setStoredValue = async (key: string, value: any) => {
 | 
			
		||||
		const type = storageType.value
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			switch (type) {
 | 
			
		||||
				case 'chrome':
 | 
			
		||||
					return await new Promise<void>((resolve, reject) => {
 | 
			
		||||
						const api = chrome?.storage?.sync || window.chrome?.storage?.sync
 | 
			
		||||
						if (api) {
 | 
			
		||||
							api.set({ [key]: value }, () => {
 | 
			
		||||
								if (chrome.runtime.lastError) {
 | 
			
		||||
									reject(new Error(chrome.runtime.lastError.message))
 | 
			
		||||
								} else {
 | 
			
		||||
									resolve()
 | 
			
		||||
								}
 | 
			
		||||
							})
 | 
			
		||||
						} else {
 | 
			
		||||
							reject(new Error('Chrome storage API 不可用'))
 | 
			
		||||
						}
 | 
			
		||||
					})
 | 
			
		||||
 | 
			
		||||
				case 'localStorage':
 | 
			
		||||
					localStorage.setItem(`msr_${key}`, JSON.stringify(value))
 | 
			
		||||
					break
 | 
			
		||||
 | 
			
		||||
				case 'memory':
 | 
			
		||||
					// 内存存储(不持久化)
 | 
			
		||||
					break
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			throw error
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查是否需要显示更新弹窗
 | 
			
		||||
	const shouldShowUpdatePopup = async (): Promise<boolean> => {
 | 
			
		||||
		try {
 | 
			
		||||
			const currentVersion = getCurrentVersion()
 | 
			
		||||
			
 | 
			
		||||
			// 如果无法获取当前版本,不显示弹窗
 | 
			
		||||
			if (currentVersion === 'unknown') {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 获取上次显示弹窗的版本号
 | 
			
		||||
			const lastShownVersion = await getStoredValue('lastUpdatePopupVersion', '')
 | 
			
		||||
 | 
			
		||||
			// 如果版本号不同,需要显示弹窗并更新存储的版本号
 | 
			
		||||
			if (lastShownVersion !== currentVersion) {
 | 
			
		||||
				await setStoredValue('lastUpdatePopupVersion', currentVersion)
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return false
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('检查更新弹窗状态失败:', error)
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 标记已显示过更新弹窗(手动关闭时调用)
 | 
			
		||||
	const markUpdatePopupShown = async () => {
 | 
			
		||||
		try {
 | 
			
		||||
			const currentVersion = getCurrentVersion()
 | 
			
		||||
			if (currentVersion !== 'unknown') {
 | 
			
		||||
				await setStoredValue('lastUpdatePopupVersion', currentVersion)
 | 
			
		||||
			}
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('标记更新弹窗已显示失败:', error)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 获取当前存储的版本号
 | 
			
		||||
	const getLastShownVersion = async (): Promise<string> => {
 | 
			
		||||
		return await getStoredValue('lastUpdatePopupVersion', '')
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 异步初始化函数
 | 
			
		||||
	const initializeUpdatePopup = async () => {
 | 
			
		||||
		try {
 | 
			
		||||
			// 初始化存储类型检测
 | 
			
		||||
			detectAvailableAPIs()
 | 
			
		||||
			isLoaded.value = true
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			console.error('初始化更新弹窗 store 失败:', error)
 | 
			
		||||
			isLoaded.value = true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 立即初始化
 | 
			
		||||
	initializeUpdatePopup()
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		isLoaded,
 | 
			
		||||
		storageType,
 | 
			
		||||
		getCurrentVersion,
 | 
			
		||||
		shouldShowUpdatePopup,
 | 
			
		||||
		markUpdatePopupShown,
 | 
			
		||||
		getLastShownVersion,
 | 
			
		||||
		initializeUpdatePopup,
 | 
			
		||||
		getStoredValue,
 | 
			
		||||
		setStoredValue
 | 
			
		||||
	}
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -1,98 +0,0 @@
 | 
			
		|||
/**
 | 
			
		||||
 * 浏览器检测工具
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 检测是否为 Safari 浏览器
 | 
			
		||||
 * @returns {boolean} 如果是 Safari 返回 true,否则返回 false
 | 
			
		||||
 */
 | 
			
		||||
export function isSafari(): boolean {
 | 
			
		||||
	const ua = navigator.userAgent.toLowerCase()
 | 
			
		||||
	
 | 
			
		||||
	// 检测 Safari 浏览器(包括 iOS 和 macOS)
 | 
			
		||||
	// Safari 的 User Agent 包含 'safari' 但不包含 'chrome' 或 'chromium'
 | 
			
		||||
	const isSafariBrowser = ua.includes('safari') && 
 | 
			
		||||
		!ua.includes('chrome') && 
 | 
			
		||||
		!ua.includes('chromium') &&
 | 
			
		||||
		!ua.includes('android')
 | 
			
		||||
	
 | 
			
		||||
	// 额外检查:使用 Safari 特有的 API
 | 
			
		||||
	const isSafariByFeature = 'safari' in window || 
 | 
			
		||||
		/^((?!chrome|android).)*safari/i.test(navigator.userAgent)
 | 
			
		||||
	
 | 
			
		||||
	return isSafariBrowser || isSafariByFeature
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 检测是否为移动版 Safari
 | 
			
		||||
 * @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false
 | 
			
		||||
 */
 | 
			
		||||
export function isMobileSafari(): boolean {
 | 
			
		||||
	return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 检测是否支持 Web Audio API 的完整功能
 | 
			
		||||
 * @returns {boolean} 如果支持返回 true,否则返回 false
 | 
			
		||||
 */
 | 
			
		||||
export function supportsWebAudioVisualization(): boolean {
 | 
			
		||||
	// Safari 在某些情况下对 AudioContext 的支持有限制
 | 
			
		||||
	// 特别是在处理跨域音频资源时
 | 
			
		||||
	if (isSafari()) {
 | 
			
		||||
		console.log('[BrowserDetection] Safari detected, audio visualization disabled')
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	// 检查基本的 Web Audio API 支持
 | 
			
		||||
	const hasAudioContext = 'AudioContext' in window || 'webkitAudioContext' in window
 | 
			
		||||
	const hasAnalyserNode = hasAudioContext && (
 | 
			
		||||
		'AnalyserNode' in window || 
 | 
			
		||||
		((window as any).AudioContext && 'createAnalyser' in (window as any).AudioContext.prototype)
 | 
			
		||||
	)
 | 
			
		||||
	
 | 
			
		||||
	return hasAudioContext && hasAnalyserNode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 获取浏览器信息
 | 
			
		||||
 * @returns {object} 包含浏览器类型和版本信息的对象
 | 
			
		||||
 */
 | 
			
		||||
export function getBrowserInfo() {
 | 
			
		||||
	const ua = navigator.userAgent
 | 
			
		||||
	let browserName = 'Unknown'
 | 
			
		||||
	let browserVersion = 'Unknown'
 | 
			
		||||
	
 | 
			
		||||
	if (isSafari()) {
 | 
			
		||||
		browserName = 'Safari'
 | 
			
		||||
		const versionMatch = ua.match(/Version\/(\d+\.\d+)/)
 | 
			
		||||
		if (versionMatch) {
 | 
			
		||||
			browserVersion = versionMatch[1]
 | 
			
		||||
		}
 | 
			
		||||
	} else if (ua.includes('Chrome')) {
 | 
			
		||||
		browserName = 'Chrome'
 | 
			
		||||
		const versionMatch = ua.match(/Chrome\/(\d+\.\d+)/)
 | 
			
		||||
		if (versionMatch) {
 | 
			
		||||
			browserVersion = versionMatch[1]
 | 
			
		||||
		}
 | 
			
		||||
	} else if (ua.includes('Firefox')) {
 | 
			
		||||
		browserName = 'Firefox'
 | 
			
		||||
		const versionMatch = ua.match(/Firefox\/(\d+\.\d+)/)
 | 
			
		||||
		if (versionMatch) {
 | 
			
		||||
			browserVersion = versionMatch[1]
 | 
			
		||||
		}
 | 
			
		||||
	} else if (ua.includes('Edge')) {
 | 
			
		||||
		browserName = 'Edge'
 | 
			
		||||
		const versionMatch = ua.match(/Edge\/(\d+\.\d+)/)
 | 
			
		||||
		if (versionMatch) {
 | 
			
		||||
			browserVersion = versionMatch[1]
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	return {
 | 
			
		||||
		name: browserName,
 | 
			
		||||
		version: browserVersion,
 | 
			
		||||
		isSafari: isSafari(),
 | 
			
		||||
		isMobileSafari: isMobileSafari(),
 | 
			
		||||
		supportsAudioVisualization: supportsWebAudioVisualization()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +1,5 @@
 | 
			
		|||
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, 
 | 
			
		||||
	audioVisualizer, 
 | 
			
		||||
	cicdInfo, 
 | 
			
		||||
	checkAndRefreshSongResource, 
 | 
			
		||||
	checkAndRefreshMultipleSongs,
 | 
			
		||||
	isSafari,
 | 
			
		||||
	isMobileSafari,
 | 
			
		||||
	supportsWebAudioVisualization,
 | 
			
		||||
	getBrowserInfo
 | 
			
		||||
}
 | 
			
		||||
export { artistsOrganize, audioVisualizer, cicdInfo }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,80 +0,0 @@
 | 
			
		|||
import axios from 'axios'
 | 
			
		||||
import apis from '../apis'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 检查歌曲资源 URL 是否可用,如果不可用则刷新
 | 
			
		||||
 * @param song 要检查的歌曲对象
 | 
			
		||||
 * @param updateCallback 更新歌曲信息的回调函数
 | 
			
		||||
 * @returns 更新后的歌曲对象(如果需要更新)或原始歌曲对象
 | 
			
		||||
 */
 | 
			
		||||
export const checkAndRefreshSongResource = async (
 | 
			
		||||
  song: Song,
 | 
			
		||||
  updateCallback?: (updatedSong: Song) => void
 | 
			
		||||
): Promise<Song> => {
 | 
			
		||||
  if (!song.sourceUrl) {
 | 
			
		||||
    console.warn('[ResourceChecker] 歌曲没有 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秒超时
 | 
			
		||||
    })
 | 
			
		||||
    
 | 
			
		||||
    // 资源可用,返回原始歌曲
 | 
			
		||||
    console.log('[ResourceChecker] 资源可用:', song.name)
 | 
			
		||||
    return song
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    // 资源不可用,刷新歌曲信息
 | 
			
		||||
    console.log('[ResourceChecker] 资源不可用,正在刷新:', song.name, error)
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      const updatedSong = await apis.getSong(song.cid)
 | 
			
		||||
      console.log('[ResourceChecker] 歌曲信息已刷新:', updatedSong.name)
 | 
			
		||||
      
 | 
			
		||||
      // 调用更新回调(如果提供)
 | 
			
		||||
      if (updateCallback) {
 | 
			
		||||
        updateCallback(updatedSong)
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return updatedSong
 | 
			
		||||
    } catch (refreshError) {
 | 
			
		||||
      console.error('[ResourceChecker] 刷新歌曲信息失败:', refreshError)
 | 
			
		||||
      // 刷新失败,返回原始歌曲
 | 
			
		||||
      return song
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 批量检查多首歌曲的资源
 | 
			
		||||
 * @param songs 要检查的歌曲数组
 | 
			
		||||
 * @param updateCallback 更新单首歌曲信息的回调函数
 | 
			
		||||
 * @returns 更新后的歌曲数组
 | 
			
		||||
 */
 | 
			
		||||
export const checkAndRefreshMultipleSongs = async (
 | 
			
		||||
  songs: Song[],
 | 
			
		||||
  updateCallback?: (updatedSong: Song, originalIndex: number) => void
 | 
			
		||||
): Promise<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)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return results
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user