Spaces:
Sleeping
Sleeping
| (function () { | |
| const isCapacitorApp = !!(window.Capacitor && window.Capacitor.isNativePlatform && window.Capacitor.isNativePlatform()); | |
| const platform = isCapacitorApp && window.Capacitor?.getPlatform ? window.Capacitor.getPlatform() : 'web'; | |
| // Server endpoints | |
| const MASTER_URL = 'http://122.51.80.140:5000'; | |
| const SLAVE_URL = 'https://chaofenghui-short-track.hf.space'; | |
| const PING_TIMEOUT = 3000; | |
| const PREFERENCE_CACHE_KEY = 'server_preference_v1'; | |
| const PREFERENCE_CACHE_TTL = 60 * 60 * 1000; // 1 hour | |
| const DEFAULT_SYNC_INTERVAL = 7 * 24 * 60 * 60 * 1000; | |
| const MATCH_DAY_SYNC_INTERVAL = 5 * 60 * 1000; | |
| // Active API bases — set asynchronously on startup | |
| // For Capacitor, always use MASTER_URL to avoid SameSite cookie issues. | |
| // Fetch interceptor (installed on 'load') routes writes to master anyway, | |
| // so using MASTER_URL for reads is safe and simpler. | |
| let _readApiBase = isCapacitorApp ? MASTER_URL : ''; | |
| let _readApiBaseReady = !isCapacitorApp; // web不需要异步选择 | |
| function getApiBase() { | |
| return _readApiBase; | |
| } | |
| function isReadApiBaseReady() { | |
| return _readApiBaseReady; | |
| } | |
| function isHfSpace() { | |
| try { return window.location.hostname.includes('hf.space'); } catch (_) { return false; } | |
| } | |
| function getWriteApiBase() { | |
| if (isCapacitorApp) return MASTER_URL; | |
| if (isHfSpace()) return MASTER_URL; | |
| return ''; | |
| } | |
| function getCachedServerPreference() { | |
| try { | |
| const raw = localStorage.getItem(PREFERENCE_CACHE_KEY); | |
| if (!raw) return null; | |
| const entry = JSON.parse(raw); | |
| if (Date.now() - entry.timestamp < PREFERENCE_CACHE_TTL) { | |
| return entry.server; | |
| } | |
| } catch (_) {} | |
| return null; | |
| } | |
| function cacheServerPreference(serverUrl) { | |
| try { | |
| localStorage.setItem(PREFERENCE_CACHE_KEY, JSON.stringify({ | |
| server: serverUrl, | |
| timestamp: Date.now() | |
| })); | |
| } catch (_) {} | |
| } | |
| async function selectFastestServer() { | |
| if (!isCapacitorApp) return ''; | |
| // Return cached preference if still valid | |
| const cached = getCachedServerPreference(); | |
| if (cached) return cached; | |
| const candidates = [ | |
| { url: MASTER_URL, label: 'master' }, | |
| { url: SLAVE_URL, label: 'slave' } | |
| ]; | |
| const results = await Promise.allSettled( | |
| candidates.map(async (c) => { | |
| const start = performance.now(); | |
| const controller = new AbortController(); | |
| const timeoutId = setTimeout(() => controller.abort(), PING_TIMEOUT); | |
| try { | |
| const resp = await fetch(c.url + '/api/ping', { | |
| method: 'GET', | |
| signal: controller.signal | |
| }); | |
| clearTimeout(timeoutId); | |
| const latency = performance.now() - start; | |
| if (resp.ok) { | |
| return { url: c.url, latency, label: c.label }; | |
| } | |
| throw new Error('non-ok: ' + resp.status); | |
| } catch (e) { | |
| clearTimeout(timeoutId); | |
| throw e; | |
| } | |
| }) | |
| ); | |
| let best = null; | |
| for (const r of results) { | |
| if (r.status === 'fulfilled') { | |
| if (!best || r.value.latency < best.latency) { | |
| best = r.value; | |
| } | |
| } | |
| } | |
| const selected = best ? best.url : MASTER_URL; | |
| cacheServerPreference(selected); | |
| console.log('[MobilePlatform] Selected server:', best ? best.label : 'fallback-master', | |
| best ? Math.round(best.latency) + 'ms' : ''); | |
| return selected; | |
| } | |
| async function getReadApiBase() { | |
| if (!isCapacitorApp) return ''; | |
| if (!_readApiBaseReady) { | |
| // Always use MASTER_URL for Capacitor to avoid SameSite cookie issues. | |
| // selectFastestServer() is kept for future re-enablement. | |
| _readApiBase = MASTER_URL; | |
| _readApiBaseReady = true; | |
| } | |
| return _readApiBase; | |
| } | |
| function getServerPreference() { | |
| return { | |
| readBase: _readApiBase, | |
| writeBase: getWriteApiBase(), | |
| ready: _readApiBaseReady, | |
| isCapacitorApp: isCapacitorApp, | |
| isHfSpace: isHfSpace() | |
| }; | |
| } | |
| function getPlatform() { | |
| return platform; | |
| } | |
| function isNativeApp() { | |
| return isCapacitorApp; | |
| } | |
| function isIOS() { | |
| return platform === 'ios'; | |
| } | |
| function isAndroid() { | |
| return platform === 'android'; | |
| } | |
| // ========== Fetch Interceptor (DISABLED) ========== | |
| // Removed: the interceptor saved window.fetch before CapacitorHttp patched it, | |
| // causing all requests to bypass native HTTP and lose cookies (SameSite=Lax). | |
| // Instead, Capacitor always uses MASTER_URL for both reads and writes. | |
| // To re-enable fastest-server selection, the interceptor must be installed | |
| // after the 'load' event when CapacitorHttp has already patched fetch. | |
| // ========== PWA / Native ========== | |
| async function openExternalUrl(url) { | |
| if (isCapacitorApp && window.Capacitor?.Plugins?.Browser) { | |
| try { | |
| await window.Capacitor.Plugins.Browser.open({ url }); | |
| return; | |
| } catch (e) {} | |
| } | |
| window.open(url, '_blank', 'noopener'); | |
| } | |
| function configureStatusBar(theme) { | |
| if (!isCapacitorApp || !window.Capacitor?.Plugins?.StatusBar) return; | |
| try { | |
| var st = window.Capacitor.Plugins.StatusBar; | |
| // Overlay mode: WebView draws behind status bar, env(safe-area-inset-top) works | |
| st.setOverlaysWebView({ overlay: true }).catch(function() {}); | |
| st.setBackgroundColor({ | |
| color: theme === 'dark' ? '#0B1426' : '#0F4C81' | |
| }); | |
| } catch (e) {} | |
| } | |
| function registerBackHandler(handler) { | |
| if (!isCapacitorApp || !window.Capacitor?.Plugins?.App || typeof handler !== 'function') return; | |
| window.Capacitor.Plugins.App.addListener('backButton', handler); | |
| } | |
| function applyServiceWorkerPolicy() { | |
| if (!('serviceWorker' in navigator)) return; | |
| if (!isCapacitorApp) { | |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { | |
| // When a new SW takes over, reload to get latest code | |
| var refreshing = false; | |
| navigator.serviceWorker.addEventListener('controllerchange', function() { | |
| if (refreshing) return; | |
| refreshing = true; | |
| window.location.reload(); | |
| }); | |
| // If a new SW is already waiting, signal it to activate | |
| if (reg.waiting) { | |
| reg.waiting.postMessage('skipWaiting'); | |
| } | |
| reg.addEventListener('updatefound', function() { | |
| var newWorker = reg.installing; | |
| if (!newWorker) return; | |
| newWorker.addEventListener('statechange', function() { | |
| if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { | |
| // New SW installed and waiting — tell it to activate immediately | |
| newWorker.postMessage('skipWaiting'); | |
| } | |
| }); | |
| }); | |
| }).catch(function(err) { console.log('SW registration failed:', err); }); | |
| return; | |
| } | |
| navigator.serviceWorker.getRegistrations().then(function(regs) { return regs.forEach(function(r) { return r.unregister(); }); }); | |
| } | |
| function buildAbsoluteUrl(path) { | |
| if (!path) return ''; | |
| if (/^https?:\/\//.test(path)) return path; | |
| if (!path.startsWith('/')) return `${_readApiBase}/${path}`; | |
| return `${_readApiBase}${path}`; | |
| } | |
| function isVisibleForSync() { | |
| return !document.hidden; | |
| } | |
| function isReadyForSync(currentUser, isOnline) { | |
| return !!currentUser && isOnline(); | |
| } | |
| function isMatchDay(currentMatches) { | |
| if (!Array.isArray(currentMatches) || currentMatches.length === 0) return false; | |
| const today = new Date().toISOString().slice(0, 10).replace(/-/g, '/'); | |
| return currentMatches.some(m => { | |
| const dr = m.date_range || ''; | |
| const parts = dr.split('—'); | |
| if (parts.length === 2) { | |
| const start = parts[0].trim().replace(/\//g, '-'); | |
| const end = parts[1].trim().replace(/\//g, '-'); | |
| return today >= start && today <= end; | |
| } | |
| return dr.includes(today.replace(/-/g, '/')); | |
| }); | |
| } | |
| function getSyncInterval(currentMatches) { | |
| return isMatchDay(currentMatches) ? MATCH_DAY_SYNC_INTERVAL : DEFAULT_SYNC_INTERVAL; | |
| } | |
| function shouldSyncNow(lastSyncTime, currentMatches) { | |
| return Date.now() - lastSyncTime >= getSyncInterval(currentMatches); | |
| } | |
| function shouldUseEmbeddedData() { | |
| return isCapacitorApp; | |
| } | |
| function shouldStartNativeSync() { | |
| return isCapacitorApp; | |
| } | |
| function getRuntimeVersion() { | |
| return window.__APP_VERSION__ || localStorage.getItem('app_version') || 'Web版'; | |
| } | |
| function getSettingsVersionLabel() { | |
| const ver = getRuntimeVersion(); | |
| return isCapacitorApp ? `v${ver}` : 'Web版'; | |
| } | |
| function getMobileUpdateInfo(versionData) { | |
| const platforms = versionData?.platforms || {}; | |
| if (isAndroid()) { | |
| return platforms.android || { available: false, primary_action: null }; | |
| } | |
| if (isIOS()) { | |
| return platforms.ios || { available: false, primary_action: null }; | |
| } | |
| return platforms.android || platforms.web || { available: false, primary_action: null }; | |
| } | |
| function shouldShowNativeUpdate(updateVersion, effectiveLocalVersion, updateInfo, isNewerVersion) { | |
| return isCapacitorApp && !!updateInfo?.primary_action && isNewerVersion(updateVersion, effectiveLocalVersion); | |
| } | |
| function shouldPersistInstalledVersion() { | |
| return isCapacitorApp && !!window.__APP_VERSION__; | |
| } | |
| function shouldCheckNativeUpdate() { | |
| return isCapacitorApp; | |
| } | |
| function getWebUpdateCheckMessage() { | |
| return '网页版无需检查更新'; | |
| } | |
| function shouldResyncAfterCacheClear(currentUser) { | |
| return isCapacitorApp && !!currentUser; | |
| } | |
| function shouldCacheApiResponses() { | |
| return isCapacitorApp; | |
| } | |
| function shouldUseOfflineFallback() { | |
| return isCapacitorApp; | |
| } | |
| function getUpdatePrimaryLabel() { | |
| return isIOS() ? '查看分发方式' : '立即更新'; | |
| } | |
| function getUpdateDescription() { | |
| return isIOS() ? 'iOS 新版本已准备,请按分发方式更新' : '有新版本可用,建议更新以获得最新数据'; | |
| } | |
| function getInstallTitle() { | |
| return isIOS() ? '📱 iOS 安装' : '📱 Android 安装'; | |
| } | |
| function createInstallAction(action, version) { | |
| if (isIOS()) { | |
| return { | |
| href: 'javascript:void(0)', | |
| download: false, | |
| label: action?.label || '请通过 TestFlight / Xcode 分发', | |
| meta: action?.min_os || 'iOS', | |
| onClick(event) { | |
| event?.preventDefault?.(); | |
| const message = action?.label || '当前 iOS 版本请通过 TestFlight / Xcode 分发'; | |
| window.showToast?.(message, 'success'); | |
| return false; | |
| } | |
| }; | |
| } | |
| if (action?.url) { | |
| const absoluteUrl = buildAbsoluteUrl(action.url); | |
| return { | |
| href: action.url, | |
| download: true, | |
| label: action.label || '立即下载 Android 安装包', | |
| meta: action?.min_os || 'Android', | |
| onClick(event) { | |
| if (!isCapacitorApp) return true; | |
| event?.preventDefault?.(); | |
| openExternalUrl(absoluteUrl); | |
| return false; | |
| } | |
| }; | |
| } | |
| return { | |
| href: 'javascript:void(0)', | |
| download: false, | |
| label: '安装包暂未上传', | |
| meta: action?.min_os || 'Android', | |
| onClick(event) { | |
| event?.preventDefault?.(); | |
| window.showToast?.('安装包暂未上传', 'error'); | |
| return false; | |
| } | |
| }; | |
| } | |
| function createUpdateToastAction(action, version) { | |
| if (isIOS()) { | |
| return { | |
| label: getUpdatePrimaryLabel(), | |
| run(toastEl) { | |
| window.showToast?.(action?.label || '请通过 TestFlight / Xcode 分发'); | |
| toastEl?.remove(); | |
| } | |
| }; | |
| } | |
| return { | |
| label: getUpdatePrimaryLabel(), | |
| run(toastEl) { | |
| localStorage.setItem('installed_version', version); | |
| toastEl?.remove(); | |
| if (action?.url) { | |
| openExternalUrl(buildAbsoluteUrl(action.url)); | |
| } else { | |
| window.showToast?.('安装包暂未上传', 'error'); | |
| } | |
| } | |
| }; | |
| } | |
| window.MobilePlatform = { | |
| isCapacitorApp, | |
| isNativeApp, | |
| isIOS, | |
| isAndroid, | |
| getPlatform, | |
| getApiBase, | |
| getReadApiBase, | |
| getWriteApiBase, | |
| isReadApiBaseReady, | |
| selectFastestServer, | |
| getServerPreference, | |
| isHfSpace, | |
| openExternalUrl, | |
| configureStatusBar, | |
| registerBackHandler, | |
| applyServiceWorkerPolicy, | |
| buildAbsoluteUrl, | |
| isVisibleForSync, | |
| isReadyForSync, | |
| isMatchDay, | |
| getSyncInterval, | |
| shouldSyncNow, | |
| shouldUseEmbeddedData, | |
| shouldStartNativeSync, | |
| getRuntimeVersion, | |
| getSettingsVersionLabel, | |
| getMobileUpdateInfo, | |
| shouldShowNativeUpdate, | |
| shouldPersistInstalledVersion, | |
| shouldCheckNativeUpdate, | |
| getWebUpdateCheckMessage, | |
| shouldResyncAfterCacheClear, | |
| shouldCacheApiResponses, | |
| shouldUseOfflineFallback, | |
| getUpdatePrimaryLabel, | |
| getUpdateDescription, | |
| getInstallTitle, | |
| createInstallAction, | |
| createUpdateToastAction, | |
| }; | |
| })(); | |