/* screens.jsx — 穿山甲 VPN mobile · screens + bottom tab (i18n) */
const { useState, useEffect, useRef } = React;
/* ───────── Top brand bar ───────── */
function TopBar({ t, lang, right }) {
return (
);
}
/* ───────── 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')}} />
{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]) => (
))}
)}
);
}
/* ───────── SERVERS screen ───────── */
function ServersScreen({ t, lang, current, onPick, q, setQ }) {
const list = SERVERS.filter(s => (s.name + s.en).toLowerCase().includes(q.toLowerCase()));
return (
{/* 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 (
);
}
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]) => (
))}
{t('weekTraffic')}
{vals.map((v, 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 });