diff --git a/CLAUDE.md b/CLAUDE.md index d72ce3f..77f48a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ npm i # Install dependencies ```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) ``` @@ -71,6 +72,7 @@ npm run qc # Alias for quality-check ### 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 @@ -99,6 +101,7 @@ npm run qc # Alias for quality-check ### `/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) @@ -127,4 +130,30 @@ npm run qc # Alias for quality-check ### Error Handling - Graceful fallbacks for storage API unavailability - Resource URL rotation handling with automatic refresh -- Cross-browser compatibility with feature detection \ No newline at end of file +- 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 \ No newline at end of file diff --git a/package.json b/package.json index e680d0f..c215a13 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "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", @@ -15,7 +16,8 @@ "quality-check": "biome ci", "qc": "npm run quality-check", "prebuild:chrome": "node scripts/prebuild-chrome.js", - "prebuild:firefox": "node scripts/prebuild-firefox.js" + "prebuild:firefox": "node scripts/prebuild-firefox.js", + "prebuild:safari": "node scripts/prebuild-safari.js" }, "dependencies": { "@tailwindcss/vite": "^4.1.7", diff --git a/scripts/prebuild-safari.js b/scripts/prebuild-safari.js new file mode 100644 index 0000000..930e720 --- /dev/null +++ b/scripts/prebuild-safari.js @@ -0,0 +1,359 @@ +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 = ` + + + + MSR Mod Background + + +

MSR Mod Background Page

+

If you can see this page, the background page is loaded!

+
+ + + + + +`; + 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 = \` +
+

MSR Mod Extension Detected

+

Please click the MSR Mod extension icon in your Safari toolbar to open the app.

+ +
+ \`; + } + } 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); +} diff --git a/src/components/PlayListItem.vue b/src/components/PlayListItem.vue index f526b15..6090b0e 100644 --- a/src/components/PlayListItem.vue +++ b/src/components/PlayListItem.vue @@ -9,7 +9,7 @@ const favourites = useFavourites() const hover = ref(false) -const props = defineProps<{ +defineProps<{ item: QueueItem index: number }>() diff --git a/src/utils/browserDetection.ts b/src/utils/browserDetection.ts index 28ba2e9..f1c80ef 100644 --- a/src/utils/browserDetection.ts +++ b/src/utils/browserDetection.ts @@ -28,7 +28,7 @@ export function isSafari(): boolean { * @returns {boolean} 如果是移动版 Safari 返回 true,否则返回 false */ export function isMobileSafari(): boolean { - return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream + return /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as any).MSStream } /** @@ -47,7 +47,7 @@ export function supportsWebAudioVisualization(): boolean { const hasAudioContext = 'AudioContext' in window || 'webkitAudioContext' in window const hasAnalyserNode = hasAudioContext && ( 'AnalyserNode' in window || - (window.AudioContext && 'createAnalyser' in AudioContext.prototype) + ((window as any).AudioContext && 'createAnalyser' in (window as any).AudioContext.prototype) ) return hasAudioContext && hasAnalyserNode