// drover-hero-live.jsx — Big toggle / VPN-style variant. // Centerpiece: a giant connect button. Form lives in an expandable section below. // Compact form (host:port) shown collapsed in active state; full form in idle. const HERO_THEME = { d: { bg: 'radial-gradient(ellipse at top, #142340 0%, #0e1426 60%, #080d1c 100%)', chrome: 'transparent', panel: '#15192a', panelAlt: '#1a1f33', border: 'rgba(255,255,255,.08)', borderHard:'rgba(255,255,255,.18)', text: '#ffffff', textDim: '#cbd1d9', dim: '#7a8499', dimmer: '#5a6178', accent: '#5dd4b3', accentDeep:'#2da085', accentDarker:'#1a6e5b', danger: '#ff6b6b', warn: '#f3c764', pass: '#5dd4b3', skip: '#7a8499', }, l: { bg: 'linear-gradient(180deg, #f3f7fb 0%, #e8edf4 100%)', chrome: 'transparent', panel: '#ffffff', panelAlt: '#f5f7fb', border: 'rgba(0,0,0,.07)', borderHard:'rgba(0,0,0,.15)', text: '#0e1830', textDim: '#3a4356', dim: '#6a7383', dimmer: '#9aa3b2', accent: '#1aa787', accentDeep:'#0f8c70', accentDarker:'#0a6655', danger: '#c0463f', warn: '#a8731e', pass: '#1aa787', skip: '#7a8499', }, }; function HeroWindow({ mode = 'dark', initial }) { const themeKey = mode === 'dark' ? 'd' : 'l'; const t = HERO_THEME[themeKey]; const D = window.useDrover(initial); const palette = { pending: t.dimmer, running: t.accent, passed: t.pass, failed: t.danger, skipped: t.skip }; const fontUI = "'Inter','Segoe UI',system-ui,sans-serif"; const fontMono = "'JetBrains Mono','SF Mono',ui-monospace,Consolas,monospace"; const [showForm, setShowForm] = React.useState(false); const [showDetails, setShowDetails] = React.useState(false); const phase = D.phase; const summary = D.lastSummary; const allFailed = summary && summary.failed === D.tests.length; const failed = summary?.failed ?? 0; const isActive = phase === 'active'; const warning = isActive && failed > 0; // big-button click handler. Two-step: if idle/checked, go. const handleBigClick = () => { if (phase === 'active') return D.stopProxy(); if (phase === 'checking') return; if (phase === 'idle') { // run check, then auto-start if it passed (async () => { await D.runCheck(); })(); return; } if (phase === 'checked' && !allFailed) D.startProxy(); if (phase === 'checked' && allFailed) D.runCheck(); }; // After runCheck completes from idle, auto-start if all passed. const prevPhase = React.useRef(phase); React.useEffect(() => { if (prevPhase.current === 'checking' && phase === 'checked') { const sum = D.lastSummary; if (sum && sum.failed === 0) { // small delay so user perceives the "checked" state briefly setTimeout(() => D.startProxy(), 600); } } prevPhase.current = phase; }, [phase]); // Big-button label / colors let buttonState; if (phase === 'active') buttonState = warning ? 'active-warn' : 'active'; else if (phase === 'checking') buttonState = 'checking'; else if (phase === 'checked' && allFailed) buttonState = 'failed'; else buttonState = 'idle'; const ringColor = ({ idle: t.accent, checking: t.accent, active: t.accent, 'active-warn': t.warn, failed: t.danger, })[buttonState]; const headline = ({ idle: 'Tap to connect', checking: 'Verifying…', active: 'Discord is protected', 'active-warn': 'Connected · UDP fallback', failed: 'Connection failed', })[buttonState]; const subline = ({ idle: `via ${D.form.host}`, checking: `running 7 checks…`, active: `via ${D.form.host} · ${window.fmtUptime(D.stats.uptimeS)}`, 'active-warn': `voice/screen disabled · ${window.fmtUptime(D.stats.uptimeS)}`, failed: `${failed} of ${D.tests.length} checks failed`, })[buttonState]; return (
{/* Title bar */}
Drover-Go 0.4.2 {phase === 'active' && } {phase === 'idle' && 'idle'} {phase === 'checking' && 'checking…'} {phase === 'checked' && (allFailed ? 'failed' : 'ready')} {phase === 'active' && (warning ? 'connected · warn' : 'connected')}
{/* Big button + headlines */}
{headline}
{subline}
{/* Stats — only when active */} {isActive && (
)} {/* Diagnostic mini-list during check or after */} {(phase === 'checking' || (phase === 'checked' && !isActive)) && (
{D.tests.map((test) => { const r = D.results[test.id]; const state = r?.result || (D.running === test.id ? 'running' : 'pending'); return (
{test.label} {r?.metric || (state === 'running' ? '…' : '')} {r?.result === 'failed' && ( )}
{r?.result === 'failed' && r.expanded && (
{r.error}
{r.hint}
)}
); })}
)}
{/* Bottom: form (idle) or footer chips (active) */}
{showForm && (
D.update({ host: v })} onEnter={D.runCheck} placeholder="95.165.72.59 или example.com" label="Host" style={{ flex: 1 }} /> D.update({ port: v.replace(/\D/g,'') })} onEnter={D.runCheck} placeholder="12334" label="Port" style={{ width: 90 }} />
D.update({ login: v })} onEnter={D.runCheck} placeholder="user" label="Login" style={{ flex: 1 }} disabled={!D.form.auth} /> D.update({ password: v })} onEnter={D.runCheck} placeholder="••••••" label="Password" style={{ flex: 1 }} disabled={!D.form.auth} />
)}
{/* Logs strip */} {showDetails && (
el && (el.scrollTop = el.scrollHeight)}> {D.logs.map((l, i) => (
{window.fmtTime(l.t)}{' '} [{l.level}]{' '} {l.msg}
))}
)}
); } // ─── Big toggle button ────────────────────────────────────────────────── function BigToggle({ t, state, onClick, progress = 0 }) { // size = 168 px circle; outer pulsing rings const isActive = state === 'active' || state === 'active-warn'; const isChecking = state === 'checking'; const isFailed = state === 'failed'; const isWarn = state === 'active-warn'; const accent = isWarn ? t.warn : isFailed ? t.danger : t.accent; const accentDeep = isWarn ? t.warn : isFailed ? t.danger : t.accentDeep; const accentDarker = isWarn ? t.warn : isFailed ? t.danger : t.accentDarker; const ring = 168; const stroke = 5; const r = (ring - stroke) / 2; const c = 2 * Math.PI * r; return ( ); } function HeroStat({ icon, v, u, c, mono, t }) { return (
{icon}
{v}
{u}
); } function HeroTitleBtn({ children, t, hoverBg }) { const [hover, setHover] = React.useState(false); return (
setHover(true)} onMouseLeave={() => setHover(false)} style={{ width: 26, height: 26, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', borderRadius: 5, background: hover ? (hoverBg || 'rgba(127,127,127,.16)') : 'transparent', }}>{children}
); } function HeroInput({ t, fontMono, value, onChange, onEnter, placeholder, label, style, type, disabled, id }) { const [focus, setFocus] = React.useState(false); return ( ); } function fmtCompact(n) { if (n < 1024) return n.toFixed(0); if (n < 1024 * 1024) return (n / 1024).toFixed(0); return (n / 1024 / 1024).toFixed(1); } function fmtUnit(n) { if (n < 1024) return 'B/s'; if (n < 1024 * 1024) return 'KB/s'; return 'MB/s'; } window.HeroWindow = HeroWindow;