diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index eaea6fc..041a02c 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -61,6 +61,48 @@ jobs: name: firefox-addon path: dist/ + build-for-safari: + name: 构建 Safari 扩展程序 + runs-on: macos + env: + VITE_RUN_ID: ${{ gitea.run_number }} + VITE_HASH_ID: ${{ gitea.sha }} + + steps: + - uses: actions/checkout@v3 + name: 检出代码 + + - name: 设置 Node.js + uses: actions/setup-node@v3 + with: + node-version: '22' + + - name: 安装依赖 + run: npm install + + - name: 构建扩展程序 + run: npm run build:safari + + - name: 设置 Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - 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 + + - name: 构建 Safari 扩展 + run: | + cd "safari-extension/MSR Mod" + xcodebuild -project "MSR Mod.xcodeproj" -scheme "MSR Mod (macOS)" -configuration Release -destination "generic/platform=macOS" build + + - name: 上传构建工件 + uses: actions/upload-artifact@v3 + with: + name: safari-extension + path: safari-extension/ + publish-to-chrome-webstore: name: 发布至 Chrome 应用商店 runs-on: ubuntu-latest 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/public/manifest.json b/public/manifest.json index afdbec4..0211d94 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "MSR Mod", - "version": "0.0.6", + "version": "0.0.7", "description": "塞壬唱片(Monster Siren Records)官网的替代前端。", "content_scripts": [ { 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/App.vue b/src/App.vue index 5be151c..927471c 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,8 @@ 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() @@ -21,6 +23,8 @@ watch(() => presentPreferencePanel, (value) => { \ No newline at end of file + diff --git a/src/assets/icons/soundwave.vue b/src/assets/icons/soundwave.vue new file mode 100644 index 0000000..8f2336d --- /dev/null +++ b/src/assets/icons/soundwave.vue @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/src/components/PlayQueueItem.vue b/src/components/PlayQueueItem.vue index c15bf70..eb3d284 100644 --- a/src/components/PlayQueueItem.vue +++ b/src/components/PlayQueueItem.vue @@ -1,10 +1,11 @@ + + diff --git a/src/stores/useUpdatePopup.ts b/src/stores/useUpdatePopup.ts new file mode 100644 index 0000000..f567e65 --- /dev/null +++ b/src/stores/useUpdatePopup.ts @@ -0,0 +1,204 @@ +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((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 => { + 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 => { + 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 + } +}) \ No newline at end of file diff --git a/src/utils/browserDetection.ts b/src/utils/browserDetection.ts new file mode 100644 index 0000000..f1c80ef --- /dev/null +++ b/src/utils/browserDetection.ts @@ -0,0 +1,98 @@ +/** + * 浏览器检测工具 + */ + +/** + * 检测是否为 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() + } +} \ No newline at end of file diff --git a/src/utils/index.ts b/src/utils/index.ts index 5f850e4..67aee1a 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,5 +2,16 @@ import artistsOrganize from "./artistsOrganize" import { audioVisualizer } from "./audioVisualizer" import cicdInfo from "./cicdInfo" import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker" +import { isSafari, isMobileSafari, supportsWebAudioVisualization, getBrowserInfo } from "./browserDetection" -export { artistsOrganize, audioVisualizer, cicdInfo, checkAndRefreshSongResource, checkAndRefreshMultipleSongs } +export { + artistsOrganize, + audioVisualizer, + cicdInfo, + checkAndRefreshSongResource, + checkAndRefreshMultipleSongs, + isSafari, + isMobileSafari, + supportsWebAudioVisualization, + getBrowserInfo +}