import { ButtonItem, DropdownItem, Field, PanelSection, PanelSectionRow, ToggleField, } from "@decky/ui"; import { useEffect, useRef, useState } from "react"; import { connect, disconnect, getState, listNodes, NodeInfo, Phase, setNode, VpnState, } from "./backend"; const PHASE_TEXT: Record = { off: "꺼짐", connecting: "연결 중…", on: "연결됨", error: "오류", missing: "미꾸라지 미설치", }; const STATE_POLL_MS = 2000; const NODES_POLL_MS = 10000; function elapsed(since: number, nowMs: number): string { const minutes = Math.floor((nowMs / 1000 - since) / 60); if (minutes < 1) return "방금"; if (minutes < 60) return `${minutes}분째`; return `${Math.floor(minutes / 60)}시간 ${minutes % 60}분째`; } function rttText(rtt: number | null): string { return rtt === null ? "측정 안 됨" : `${Math.round(rtt)}ms`; } function nodeLine(state: VpnState): string { const node = state.node; if (!node) return "핑 측정 중…"; const body = node.rtt === null ? node.label : `${node.label} (${Math.round(node.rtt)}ms)`; return state.node_mode === "auto" ? `자동 · ${body}` : body; } function StatusValue({ state }: { state: VpnState }) { if (state.phase !== "on") return {PHASE_TEXT[state.phase]}; const jp = state.country === "JP"; const flag = jp ? "🇯🇵 JP" : state.country ?? "?"; const since = state.connected_since ? ` · ${elapsed(state.connected_since, Date.now())}` : ""; return ( {`${PHASE_TEXT.on} · ${flag}${state.ip ? ` · ${state.ip}` : ""}${since}`} ); } export function Panel() { const [state, setState] = useState(null); const [busy, setBusy] = useState(false); const [advanced, setAdvanced] = useState(false); const [nodes, setNodes] = useState([]); const polling = useRef(false); const refresh = async () => { if (polling.current) return; polling.current = true; try { setState(await getState()); } finally { polling.current = false; } }; useEffect(() => { refresh(); const id = setInterval(refresh, STATE_POLL_MS); return () => clearInterval(id); }, []); useEffect(() => { if (!advanced) return; const load = async () => setNodes(await listNodes()); load(); const id = setInterval(load, NODES_POLL_MS); return () => clearInterval(id); }, [advanced]); const run = async (action: () => Promise, pending?: Phase) => { setBusy(true); if (pending && state) setState({ ...state, phase: pending, message: null }); try { setState(await action()); } finally { setBusy(false); } }; if (!state) { return ( 불러오는 중… ); } const active = state.phase === "on" || state.phase === "connecting"; const options = [ { data: "auto", label: "자동 (핑 최저)" }, ...nodes.map((n) => ({ data: String(n.sid), label: `${n.label} · ${rttText(n.rtt)}` })), ]; const selected = state.node_mode === "auto" ? "auto" : String(state.node?.sid ?? "auto"); return ( run(on ? connect : disconnect, on ? "connecting" : undefined)} /> {nodeLine(state)} {state.message && ( )} setAdvanced(!advanced)}> {advanced ? "고급 ▾" : "고급 ▸"} {advanced && ( run(() => setNode(String(opt.data)))} /> )} ); }