cydd / static /js /utils /mobile-platform.js
chaofenghui's picture
Upload folder using huggingface_hub
dd92d97 verified
Raw
History Blame Contribute Delete
14.9 kB
(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,
};
})();