/* screens.jsx — 穿山甲 VPN mobile · screens + bottom tab (i18n) */ const { useState, useEffect, useRef } = React; /* ───────── Top brand bar ───────── */ function TopBar({ t, lang, right }) { return (
{t('brand')}
{right}
); } /* ───────── CONNECT screen ───────── */ function ConnectScreen({ t, lang, status, onToggle, server, smart, free, mins, adDone, onAd, goServers, elapsed }) { const map = { off: { fill: 'var(--sand-100)', fg: 'var(--clay-500)', icon: 'power', cap: t('capOff'), ring: 'track' }, connecting: { fill: 'var(--accent)', fg: '#fff', icon: 'loader-circle', cap: t('capConnecting'), ring: 'spin' }, on: { fill: 'var(--success)', fg: '#fff', icon: 'shield-check', cap: t('capOn'), ring: 'full' }, }[status]; const fmt = (s) => `${String(Math.floor(s/3600)).padStart(2,'0')}:${String(Math.floor(s%3600/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`; const shadow = status==='off' ? 'inset 0 0 0 1px var(--sand-200), var(--shadow-md)' : `0 0 0 9px ${status==='on' ? 'var(--success-subtle)' : 'var(--accent-subtle)'}, var(--shadow-lg)`; // in-circle center block const center = status==='off' ? (<>{t('connectNow')}) : status==='connecting' ? (···) : (<>{fmt(elapsed)}{t('secure')}); return (
{status==='on'?t('online'):t('offline')}} />
{map.cap}
{free && (
{t('quotaToday')} {mins} {t('minutes')} {t('quotaFree')}
{adDone ? (
{t('adUnlocked')}
) : ( )}
)} {status === 'on' && (
{[['arrow-down',t('download'),'86.4','Mb/s'],['arrow-up',t('upload'),'12.1','Mb/s'],['zap',t('latency'), String(server.ping),'ms']].map(([ic,l,v,u]) => (
{l}
{v} {u}
))}
)}
); } /* ───────── SERVERS screen ───────── */ function ServersScreen({ t, lang, current, onPick, q, setQ }) { const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase())); return (
{t('chooseNode')}
setQ(e.target.value)} placeholder={t('search')} style={{ border: 'none', background: 'transparent', outline: 'none', flex: 1, fontFamily: 'var(--font-sans)', fontSize: 15, color: 'var(--fg1)' }} />
{/* smart select — recommended card */}
{list.map((s, i) => { const active = s.code === current; return ( ); })}
); } /* ───────── SETTINGS screen ───────── */ function Toggle({ on, onChange }) { return (
); } /* segmented language switch 中 / EN */ function LangSwitch({ lang, setLang }) { return (
{[['zh','中文'],['en','EN']].map(([v,l]) => ( ))}
); } function Row({ icon, title, sub, right, last, onClick }) { return (
{icon &&
}
{title}
{sub &&
{sub}
}
{right}
); } function SettingsScreen({ t, lang, setLang, dark, setDark, free, setFree, onRedeem, onUpgrade, onContact, onSignOut, onReplayAuth, onReplayOnboarding }) { const [autostart, setAuto] = useState(true); const [kill, setKill] = useState(true); const [twoFA, setTwoFA] = useState(true); const [devices, setDevices] = useState([ { id: 1, icon: 'laptop', name: 'MacBook Pro', os: 'macOS 26', active: t('thisDevice'), me: true }, { id: 2, icon: 'smartphone', name: 'iPhone 17', os: 'iOS 26', active: '2 min', me: false }, { id: 3, icon: 'monitor-smartphone', name: 'iPad Air', os: 'iPadOS 26', active: lang === 'zh' ? '3 小时前' : '3h ago', me: false }, ]); const card = { background: 'var(--surface)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)', border: '1px solid var(--border)', overflow: 'hidden', marginBottom: 18 }; return (
{t('meTitle')}
{/* plan banner */}
me@pangolin.vpn
{free ? t('freePlanName') : t('proMember')}
{free ? t('quotaToday') : t('expires')}
{free ? t('quotaFree') : '2026-12-31 · 42.6 GB'}
{/* account info */}
{t('accInfoTitle')}
{t('accChange')}} /> {t('accChange')}} /> setTwoFA(!twoFA)} />} last />
{/* devices */}
{t('myDevices')}
{t('devicesSub')}
{devices.map((d, i) => (
{d.name}{d.me && {d.active}}
{d.os}{!d.me && ` · ${d.active}`}
{!d.me && }
))}
{/* redeem + contact */}
} onClick={onRedeem} /> } onClick={() => window.open('../usercenter/index.html', '_blank')} /> } last onClick={onContact} />
{/* app settings */}
setAuto(!autostart)} />} /> setKill(!kill)} />} last />
} /> setDark(!dark)} />} /> WireGuard} /> } last />
} onClick={onReplayAuth} /> } onClick={onReplayOnboarding} /> setFree(!free)} />} last />
穿山甲 · v2.4.0
); } /* ───────── STATS screen ───────── */ function StatsScreen({ t, lang }) { const vals = [2.1, 3.4, 1.8, 4.6, 5.2, 6.1, 3.0]; const max = 6.1; const labels = t('days7'); const card = { background: 'var(--surface)', border: '1px solid var(--border)', borderRadius: 'var(--radius-lg)', boxShadow: 'var(--shadow-sm)' }; return (
{t('statsTitle')}
{[[t('trafficMonth'), '42.6', 'GB'], [t('avgPing'), '29', 'ms'], [t('durMonth'), '86.4', 'h']].map(([l, v, u]) => (
{l}
{v} {u}
))}
{t('weekTraffic')}
{vals.map((v, i) => (
{v}
{labels[i]}
))}
); } /* ───────── Bottom tab ───────── */ function BottomTab({ t, tab, setTab }) { const items = [['connect','power',t('tabConnect')],['servers','globe',t('tabServers')],['stats','chart',t('tabStats')],['me','user',t('tabMe')]]; return (
{items.map(([id, ic, label]) => { const a = tab === id; return ( ); })}
); } Object.assign(window, { ConnectScreen, ServersScreen, StatsScreen, SettingsScreen, BottomTab, LangSwitch });