import { invoke } from "@tauri-apps/api/core"; import "./styles.css"; type View = "home" | "settings" | "diagnostics"; type Status = { running: boolean; activeConnections: number; tunnels: number; dataCenter: number | null; route: string; failures: number; uptimeSeconds: number; port: number; logs: LogLine[]; }; type LogLine = { timestamp: string; message: string; error: boolean; }; type Settings = { lanMode: boolean; port: number; workerDomain: string; }; const root = document.querySelector("#app")!; let view: View = "home"; let status: Status = { running: false, activeConnections: 0, tunnels: 0, dataCenter: null, route: "Маршрут ещё не выбран", failures: 0, uptimeSeconds: 0, port: 1080, logs: [], }; let settings: Settings = { lanMode: false, port: 1080, workerDomain: "" }; let busy = false; let toastTimer: number | undefined; const icons = { arrowLeft: ``, settings: ``, activity: ``, plane: ``, }; function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function formatUptime(seconds: number): string { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const rest = seconds % 60; return [hours, minutes, rest].map((value) => value.toString().padStart(2, "0")).join(":"); } function connectionState(): { title: string; subtitle: string; className: string } { if (!status.running) { return { title: "Защита выключена", subtitle: "Нажмите кнопку, чтобы подключить Telegram", className: "offline", }; } if (status.failures > 0 && status.tunnels === 0) { return { title: "Ищем новый маршрут", subtitle: "Переключаемся на резервное подключение", className: "warning", }; } if (status.tunnels > 0) { return { title: "Telegram на связи", subtitle: "Соединение защищено и работает", className: "online", }; } return { title: "Защита включена", subtitle: "Откройте Telegram — маршрут готов", className: "online", }; } function shell(content: string, pageClass = ""): string { return `
${content}
`; } function renderHome(): void { const connection = connectionState(); root.innerHTML = shell(`
TGLock

Свободный Telegram. В один клик.

${icons.plane}

${connection.title}

${connection.subtitle}

${escapeHtml(status.route)} · порт ${status.port}
`, "home-page"); document.querySelector("#power")?.addEventListener("click", toggleProtection); document.querySelector("#settings-nav")?.addEventListener("click", () => navigate("settings")); document.querySelector("#diagnostics-nav")?.addEventListener("click", () => navigate("diagnostics")); } function pageHeader(title: string, subtitle: string): string { return ` `; } function renderSettings(): void { root.innerHTML = shell(`
${pageHeader("Настройки", "Подключение и локальная сеть")}

Необязательно. Используется как резервный маршрут.

${status.running ? `
Выключите защиту, чтобы изменить настройки
` : ""}
`, "subpage-shell"); document.querySelector("#back")?.addEventListener("click", () => navigate("home")); document.querySelector("#settings-form")?.addEventListener("submit", saveSettings); } function renderDiagnostics(): void { const recentLogs = status.logs.slice(-4).reverse(); root.innerHTML = shell(`
${pageHeader("Диагностика", "Состояние маршрута в реальном времени")}
${status.running ? "Сервис работает" : "Сервис остановлен"} ${status.running ? "Локальный прокси принимает подключения" : "Включите защиту на главном экране"}
Соединения ${status.activeConnections}
Туннели ${status.tunnels}
Дата-центр ${status.dataCenter ? `DC${status.dataCenter}` : "—"}
Время работы ${formatUptime(status.uptimeSeconds)}
Последние события LIVE
${recentLogs.length ? recentLogs .map( (line) => `
${escapeHtml(line.message)}
`, ) .join("") : `
События появятся после включения защиты
`}
`, "subpage-shell"); document.querySelector("#back")?.addEventListener("click", () => navigate("home")); } function render(): void { if (view === "settings") renderSettings(); else if (view === "diagnostics") renderDiagnostics(); else renderHome(); } function navigate(next: View): void { view = next; render(); } async function refreshStatus(): Promise { try { status = await invoke("get_status"); if (view !== "settings") render(); } catch (error) { showToast(String(error), true); } } async function toggleProtection(): Promise { if (busy) return; busy = true; renderHome(); try { status = status.running ? await invoke("stop_proxy") : await invoke("start_proxy"); } catch (error) { showToast(String(error), true); } finally { busy = false; await refreshStatus(); } } async function saveSettings(event: Event): Promise { event.preventDefault(); if (busy || status.running) return; const worker = document.querySelector("#worker"); const lanMode = document.querySelector("#lan-mode"); const port = document.querySelector("#port"); if (!worker || !lanMode || !port) return; const parsedPort = Number(port.value); if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) { showToast("Порт должен быть от 1 до 65535", true); return; } busy = true; settings = { workerDomain: worker.value.trim(), lanMode: lanMode.checked, port: parsedPort, }; renderSettings(); try { settings = await invoke("save_settings", { settings }); showToast("Настройки сохранены"); window.setTimeout(() => navigate("home"), 350); } catch (error) { showToast(String(error), true); } finally { busy = false; } } function showToast(message: string, error = false): void { window.clearTimeout(toastTimer); window.requestAnimationFrame(() => { const toast = document.querySelector("#toast"); if (!toast) return; toast.textContent = message.replace(/^["']|["']$/g, ""); toast.className = `toast visible${error ? " error" : ""}`; toastTimer = window.setTimeout(() => toast.classList.remove("visible"), 2800); }); } async function bootstrap(): Promise { try { [status, settings] = await Promise.all([ invoke("get_status"), invoke("get_settings"), ]); } catch { // The browser preview intentionally uses the initial local state. } render(); window.setInterval(refreshStatus, 1000); } void bootstrap();