(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, }; })();