Initial Navis client with NaiveProxy, profiles, ping and git updates
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,625 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Navis</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--bg0: #dff3ea;
|
||||
--bg1: #f3faf7;
|
||||
--ink: #10241c;
|
||||
--muted: #567064;
|
||||
--line: rgba(16, 36, 28, 0.12);
|
||||
--accent: #0c7a55;
|
||||
--accent-2: #10956a;
|
||||
--danger: #b42318;
|
||||
--surface: rgba(255, 255, 255, 0.78);
|
||||
--shadow: 0 22px 60px rgba(16, 36, 28, 0.10);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0; min-height: 100%;
|
||||
font-family: Manrope, sans-serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(900px 520px at 0% 0%, #bfe8d4 0%, transparent 55%),
|
||||
radial-gradient(700px 420px at 100% 10%, #c9dcf0 0%, transparent 50%),
|
||||
linear-gradient(160deg, var(--bg0), var(--bg1));
|
||||
}
|
||||
body { display: grid; place-items: center; padding: 22px 14px; }
|
||||
.shell {
|
||||
width: min(460px, 100%);
|
||||
background: var(--surface);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: 28px;
|
||||
padding: 24px 22px 20px;
|
||||
animation: rise .5s cubic-bezier(.2,.8,.2,1) both;
|
||||
}
|
||||
@keyframes rise {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
.top {
|
||||
display: flex; align-items: center; gap: 14px; margin-bottom: 18px;
|
||||
}
|
||||
.logo {
|
||||
width: 48px; height: 48px; border-radius: 14px;
|
||||
background: linear-gradient(145deg, #0c7a55, #1aa876);
|
||||
display: grid; place-items: center;
|
||||
box-shadow: 0 10px 24px rgba(12, 122, 85, 0.28);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.logo svg { width: 26px; height: 26px; }
|
||||
.brand-wrap { min-width: 0; }
|
||||
.brand {
|
||||
font-family: Syne, sans-serif;
|
||||
font-size: 1.85rem; font-weight: 800;
|
||||
letter-spacing: -0.04em; line-height: 1; margin: 0;
|
||||
}
|
||||
.tagline { margin: 4px 0 0; color: var(--muted); font-size: .86rem; }
|
||||
|
||||
.status {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin-bottom: 16px; font-weight: 600;
|
||||
}
|
||||
.dot {
|
||||
width: 10px; height: 10px; border-radius: 50%; background: #9aa89f;
|
||||
}
|
||||
.dot.on {
|
||||
background: var(--accent);
|
||||
animation: pulse 1.8s ease-out infinite;
|
||||
}
|
||||
@keyframes pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(12,122,85,.35); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(12,122,85,0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(12,122,85,0); }
|
||||
}
|
||||
|
||||
label.field {
|
||||
display: block; font-size: .72rem; font-weight: 700;
|
||||
letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--muted); margin: 0 0 8px;
|
||||
}
|
||||
.profile-bar {
|
||||
display: grid; grid-template-columns: 1fr auto auto; gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
select, input[type="text"] {
|
||||
width: 100%; border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,.92);
|
||||
border-radius: 14px; padding: 12px 14px;
|
||||
font: inherit; color: var(--ink); outline: none;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
}
|
||||
select:focus, input[type="text"]:focus {
|
||||
border-color: rgba(12,122,85,.55);
|
||||
box-shadow: 0 0 0 4px rgba(12,122,85,.12);
|
||||
}
|
||||
.icon-btn {
|
||||
width: 46px; padding: 0; display: grid; place-items: center;
|
||||
border-radius: 14px; border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,.7); color: var(--ink);
|
||||
font-size: 1.2rem; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.icon-btn:hover { background: #fff; }
|
||||
.icon-btn.danger { color: var(--danger); }
|
||||
|
||||
.stack { display: grid; gap: 10px; margin-bottom: 14px; }
|
||||
|
||||
.row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; padding: 12px 14px;
|
||||
border: 1px solid var(--line); border-radius: 14px;
|
||||
background: rgba(255,255,255,.55); margin-bottom: 16px;
|
||||
}
|
||||
.switch { position: relative; width: 44px; height: 26px; flex: 0 0 auto; }
|
||||
.switch input { opacity: 0; width: 0; height: 0; }
|
||||
.slider {
|
||||
position: absolute; inset: 0; background: #c5d2cb;
|
||||
border-radius: 999px; cursor: pointer; transition: background .15s;
|
||||
}
|
||||
.slider::before {
|
||||
content: ""; position: absolute; width: 20px; height: 20px;
|
||||
left: 3px; top: 3px; background: #fff; border-radius: 50%;
|
||||
transition: transform .15s;
|
||||
}
|
||||
.switch input:checked + .slider { background: var(--accent); }
|
||||
.switch input:checked + .slider::before { transform: translateX(18px); }
|
||||
|
||||
.actions { display: grid; gap: 8px; }
|
||||
button.action {
|
||||
appearance: none; border: 0; border-radius: 14px;
|
||||
padding: 13px 16px; font: inherit; font-weight: 700; cursor: pointer;
|
||||
transition: transform .12s, background .15s, opacity .15s;
|
||||
}
|
||||
button.action:active { transform: translateY(1px); }
|
||||
button.action:disabled { opacity: .55; cursor: not-allowed; transform: none; }
|
||||
.primary { background: var(--accent); color: #f4fff9; }
|
||||
.primary:hover:not(:disabled) { background: var(--accent-2); }
|
||||
.primary.danger { background: var(--danger); }
|
||||
.ghost {
|
||||
background: transparent; color: var(--muted);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.ghost:hover:not(:disabled) { background: rgba(255,255,255,.75); color: var(--ink); }
|
||||
|
||||
.meta {
|
||||
margin-top: 14px; font-size: .82rem; color: var(--muted);
|
||||
line-height: 1.45; min-height: 2.6em;
|
||||
}
|
||||
|
||||
.shop {
|
||||
margin-top: 16px;
|
||||
padding: 14px 14px 12px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(12, 122, 85, 0.18);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(12, 122, 85, 0.10), rgba(255,255,255,0.55));
|
||||
}
|
||||
.shop h3 {
|
||||
font-family: Syne, sans-serif;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
.shop p {
|
||||
margin: 0 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: .84rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.shop .action {
|
||||
width: 100%;
|
||||
}
|
||||
.shop-link {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-size: .78rem;
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
.shop-link:hover { text-decoration: underline; }
|
||||
|
||||
.tools {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.ping-list {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
.ping-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(255,255,255,.55);
|
||||
font-size: .84rem;
|
||||
}
|
||||
.ping-item .ms { font-weight: 700; }
|
||||
.ping-item .ms.ok { color: var(--accent); }
|
||||
.ping-item .ms.bad { color: var(--danger); }
|
||||
|
||||
.update-banner {
|
||||
display: none;
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(12,122,85,.25);
|
||||
background: linear-gradient(135deg, rgba(12,122,85,.14), rgba(255,255,255,.7));
|
||||
}
|
||||
.update-banner.show { display: block; }
|
||||
.update-banner strong {
|
||||
font-family: Syne, sans-serif;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.update-banner p {
|
||||
margin: 0 0 10px;
|
||||
color: var(--muted);
|
||||
font-size: .84rem;
|
||||
}
|
||||
.ver {
|
||||
margin-top: 10px;
|
||||
font-size: .75rem;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed; inset: 0; background: rgba(16,36,28,.35);
|
||||
display: none; place-items: center; padding: 18px; z-index: 20;
|
||||
}
|
||||
.modal-backdrop.open { display: grid; }
|
||||
.modal {
|
||||
width: min(400px, 100%); background: #f7fcf9;
|
||||
border-radius: 22px; border: 1px solid var(--line);
|
||||
box-shadow: var(--shadow); padding: 20px;
|
||||
animation: rise .35s ease both;
|
||||
}
|
||||
.modal h2 {
|
||||
font-family: Syne, sans-serif; margin: 0 0 14px;
|
||||
font-size: 1.25rem; letter-spacing: -.02em;
|
||||
}
|
||||
.modal .actions { margin-top: 14px; grid-template-columns: 1fr 1fr; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<div class="update-banner" id="updateBanner">
|
||||
<strong id="updateTitle">Доступно обновление</strong>
|
||||
<p id="updateNotes"></p>
|
||||
<button class="action primary" id="updateBtn" type="button">Обновить сейчас</button>
|
||||
</div>
|
||||
|
||||
<div class="top">
|
||||
<div class="logo" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 3l6.5 17H16l-2.2-5.8H10.2L8 20H5.5L12 3z" fill="#f4fff9"/>
|
||||
<circle cx="12" cy="14.2" r="2.2" fill="#0c7a55"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="brand-wrap">
|
||||
<h1 class="brand">Navis</h1>
|
||||
<p class="tagline">NaiveProxy · несколько серверов</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<span class="dot" id="dot"></span>
|
||||
<span id="statusText">Отключено</span>
|
||||
</div>
|
||||
|
||||
<label class="field" for="profile">Профиль</label>
|
||||
<div class="profile-bar">
|
||||
<select id="profile"></select>
|
||||
<button class="icon-btn" id="addBtn" type="button" title="Новый профиль">+</button>
|
||||
<button class="icon-btn danger" id="delBtn" type="button" title="Удалить профиль">×</button>
|
||||
</div>
|
||||
|
||||
<div class="stack">
|
||||
<div>
|
||||
<label class="field" for="name">Название</label>
|
||||
<input id="name" type="text" placeholder="Например: DE-1 / Home" spellcheck="false" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="proxy">Ссылка сервера</label>
|
||||
<input id="proxy" type="text" placeholder="naive+https://user:pass@host:443#name" spellcheck="false" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<span>Системный прокси Windows</span>
|
||||
<label class="switch">
|
||||
<input id="sysproxy" type="checkbox" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="action primary" id="toggleBtn" type="button">Подключить</button>
|
||||
<button class="action ghost" id="saveBtn" type="button">Сохранить профиль</button>
|
||||
<div class="tools">
|
||||
<button class="action ghost" id="pingBtn" type="button">Пинг серверов</button>
|
||||
<button class="action ghost" id="updCheckBtn" type="button">Проверить обновление</button>
|
||||
</div>
|
||||
<button class="action ghost" id="coreBtn" type="button">Установить / обновить naive core</button>
|
||||
</div>
|
||||
<div class="ping-list" id="pingList"></div>
|
||||
<p class="meta" id="meta">Загрузка…</p>
|
||||
|
||||
<section class="shop" aria-label="Купить подключение">
|
||||
<h3>Безопасное подключение</h3>
|
||||
<p>На любом устройстве — защита данных, стабильность и приватность. Демо-ключ от 30₽.</p>
|
||||
<button class="action primary" id="shopBtn" type="button">Купить на evilfox.win</button>
|
||||
<a class="shop-link" id="shopLink" href="https://evilfox.win/" rel="noopener">https://evilfox.win/</a>
|
||||
</section>
|
||||
<p class="ver" id="verLabel">Navis</p>
|
||||
</main>
|
||||
|
||||
<div class="modal-backdrop" id="modal">
|
||||
<div class="modal">
|
||||
<h2>Новый профиль</h2>
|
||||
<div class="stack">
|
||||
<div>
|
||||
<label class="field" for="newName">Название</label>
|
||||
<input id="newName" type="text" placeholder="Server EU" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="field" for="newProxy">Ссылка</label>
|
||||
<input id="newProxy" type="text" placeholder="naive+https://user:pass@host:443#name" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button class="action ghost" id="cancelNew" type="button">Отмена</button>
|
||||
<button class="action primary" id="createNew" type="button">Создать</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const meta = $("meta");
|
||||
const btn = $("toggleBtn");
|
||||
const coreBtn = $("coreBtn");
|
||||
const saveBtn = $("saveBtn");
|
||||
const addBtn = $("addBtn");
|
||||
const delBtn = $("delBtn");
|
||||
const proxy = $("proxy");
|
||||
const nameInput = $("name");
|
||||
const sysproxy = $("sysproxy");
|
||||
const profile = $("profile");
|
||||
const dot = $("dot");
|
||||
const statusText = $("statusText");
|
||||
const modal = $("modal");
|
||||
const shopBtn = $("shopBtn");
|
||||
const shopLink = $("shopLink");
|
||||
const pingBtn = $("pingBtn");
|
||||
const updCheckBtn = $("updCheckBtn");
|
||||
const updateBanner = $("updateBanner");
|
||||
const updateBtn = $("updateBtn");
|
||||
const pingList = $("pingList");
|
||||
const verLabel = $("verLabel");
|
||||
const SHOP_URL = "https://evilfox.win/";
|
||||
|
||||
let busy = false;
|
||||
let connected = false;
|
||||
let formHydrated = false;
|
||||
let dirty = false;
|
||||
let profiles = [];
|
||||
|
||||
function markDirty() { dirty = true; }
|
||||
[proxy, nameInput, sysproxy].forEach((el) => {
|
||||
el.addEventListener("input", markDirty);
|
||||
el.addEventListener("change", markDirty);
|
||||
el.addEventListener("focus", markDirty);
|
||||
});
|
||||
|
||||
function setMeta(text, kind) {
|
||||
meta.textContent = text || "";
|
||||
meta.style.color = kind === "err" ? "var(--danger)" : kind === "ok" ? "var(--accent)" : "var(--muted)";
|
||||
}
|
||||
|
||||
function fillProfiles(list, active) {
|
||||
profiles = list || [];
|
||||
const cur = profile.value;
|
||||
profile.innerHTML = "";
|
||||
profiles.forEach((p) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = p.name;
|
||||
opt.textContent = p.host ? (p.name + " · " + p.host) : p.name;
|
||||
profile.appendChild(opt);
|
||||
});
|
||||
const want = active || cur || (profiles[0] && profiles[0].name);
|
||||
if (want) profile.value = want;
|
||||
}
|
||||
|
||||
function renderPings(pings) {
|
||||
pingList.innerHTML = "";
|
||||
(pings || []).forEach((p) => {
|
||||
const row = document.createElement("div");
|
||||
row.className = "ping-item";
|
||||
const left = document.createElement("span");
|
||||
left.textContent = p.host ? (p.name + " · " + p.host) : p.name;
|
||||
const right = document.createElement("span");
|
||||
right.className = "ms " + (p.ok ? "ok" : "bad");
|
||||
right.textContent = p.ok ? (p.ms + " ms") : (p.error || "ошибка");
|
||||
row.appendChild(left);
|
||||
row.appendChild(right);
|
||||
pingList.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function renderUpdate(u, version) {
|
||||
verLabel.textContent = "Navis v" + (version || "?");
|
||||
if (!u) {
|
||||
updateBanner.classList.remove("show");
|
||||
return;
|
||||
}
|
||||
if (u.available) {
|
||||
updateBanner.classList.add("show");
|
||||
$("updateTitle").textContent = "Обновление " + u.latest;
|
||||
$("updateNotes").textContent = u.notes || ("Текущая версия " + u.current);
|
||||
} else {
|
||||
updateBanner.classList.remove("show");
|
||||
}
|
||||
}
|
||||
|
||||
function paint(state, opts) {
|
||||
const syncForm = opts && opts.syncForm;
|
||||
connected = !!state.connected;
|
||||
dot.classList.toggle("on", connected);
|
||||
statusText.textContent = connected
|
||||
? ("Подключено" + (state.profile ? " · " + state.profile : ""))
|
||||
: "Отключено";
|
||||
btn.textContent = connected ? "Отключить" : "Подключить";
|
||||
btn.classList.toggle("danger", connected);
|
||||
|
||||
const lock = connected || busy;
|
||||
proxy.disabled = lock;
|
||||
nameInput.disabled = lock;
|
||||
sysproxy.disabled = lock;
|
||||
profile.disabled = lock;
|
||||
addBtn.disabled = lock;
|
||||
delBtn.disabled = lock || ((state.profiles || profiles).length <= 1);
|
||||
saveBtn.disabled = lock;
|
||||
coreBtn.disabled = busy;
|
||||
pingBtn.disabled = busy;
|
||||
updCheckBtn.disabled = busy;
|
||||
updateBtn.disabled = busy;
|
||||
btn.disabled = busy;
|
||||
|
||||
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
||||
renderPings(state.pings || []);
|
||||
renderUpdate(state.update, state.version);
|
||||
|
||||
if (syncForm || (!formHydrated && !dirty)) {
|
||||
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
|
||||
nameInput.value = active.name || state.active_profile || "";
|
||||
proxy.value = typeof state.proxy === "string" ? state.proxy : (active.proxy || "");
|
||||
if (typeof state.system_proxy === "boolean") sysproxy.checked = state.system_proxy;
|
||||
formHydrated = true;
|
||||
if (syncForm) dirty = false;
|
||||
}
|
||||
|
||||
let detail = "";
|
||||
if (connected) {
|
||||
const parts = [];
|
||||
if (state.http_proxy) parts.push("HTTP " + state.http_proxy);
|
||||
if (state.socks_proxy) parts.push("SOCKS " + state.socks_proxy);
|
||||
detail = parts.join(" · ");
|
||||
} else if (state.core_ready === false) {
|
||||
detail = "Сначала установите official naive.exe";
|
||||
} else {
|
||||
detail = "Выберите профиль или создайте новый";
|
||||
}
|
||||
if (!busy) setMeta(detail, state.core_ready === false ? "err" : "");
|
||||
}
|
||||
|
||||
async function refresh(opts) {
|
||||
const state = await getState();
|
||||
paint(state, opts);
|
||||
return state;
|
||||
}
|
||||
|
||||
async function withBusy(fn) {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn].forEach((b) => { if (b) b.disabled = true; });
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
busy = false;
|
||||
try {
|
||||
await refresh({ syncForm: true });
|
||||
} catch (e) {
|
||||
setMeta(String(e), "err");
|
||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn].forEach((b) => { if (b) b.disabled = false; });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profile.addEventListener("change", () => withBusy(async () => {
|
||||
await selectProfile(profile.value);
|
||||
dirty = false;
|
||||
formHydrated = false;
|
||||
}));
|
||||
|
||||
btn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
if (connected) {
|
||||
setMeta("Отключение…");
|
||||
await disconnect();
|
||||
setMeta("Отключено", "ok");
|
||||
} else {
|
||||
setMeta("Подключение…");
|
||||
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
|
||||
await connect();
|
||||
setMeta("Туннель активен", "ok");
|
||||
}
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
saveBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
|
||||
setMeta("Профиль сохранён", "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
coreBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Скачивание official naiveproxy…");
|
||||
const path = await installCore();
|
||||
setMeta("Core установлен: " + path, "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
delBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
if (!confirm("Удалить профиль «" + profile.value + "»?")) return;
|
||||
await deleteProfile(profile.value);
|
||||
formHydrated = false;
|
||||
dirty = false;
|
||||
setMeta("Профиль удалён", "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
$("newName").value = "";
|
||||
$("newProxy").value = "";
|
||||
modal.classList.add("open");
|
||||
$("newName").focus();
|
||||
});
|
||||
$("cancelNew").addEventListener("click", () => modal.classList.remove("open"));
|
||||
$("createNew").addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
const n = $("newName").value.trim();
|
||||
const p = $("newProxy").value.trim();
|
||||
if (!n) throw "Укажите название профиля";
|
||||
await createProfile(n, p, !!sysproxy.checked);
|
||||
modal.classList.remove("open");
|
||||
formHydrated = false;
|
||||
dirty = false;
|
||||
setMeta("Профиль создан", "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
async function openShop(e) {
|
||||
if (e) e.preventDefault();
|
||||
try {
|
||||
await openURL(SHOP_URL);
|
||||
} catch (err) {
|
||||
setMeta(String(err), "err");
|
||||
}
|
||||
}
|
||||
shopBtn.addEventListener("click", openShop);
|
||||
shopLink.addEventListener("click", openShop);
|
||||
|
||||
pingBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Проверка серверов…");
|
||||
const rows = await pingServers();
|
||||
renderPings(rows);
|
||||
const ok = (rows || []).filter((r) => r.ok).length;
|
||||
setMeta("Пинг: " + ok + "/" + (rows || []).length + " доступны", ok ? "ok" : "err");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
updCheckBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Проверка обновлений…");
|
||||
const st = await checkUpdate();
|
||||
renderUpdate(st, st.current);
|
||||
if (st.available) setMeta("Доступна версия " + st.latest, "ok");
|
||||
else if (st.error) setMeta("Обновление: " + st.error, "err");
|
||||
else setMeta("У вас актуальная версия " + st.current, "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
updateBtn.addEventListener("click", () => withBusy(async () => {
|
||||
try {
|
||||
setMeta("Скачивание обновления…");
|
||||
await applyUpdate();
|
||||
setMeta("Обновление установлено, перезапуск…", "ok");
|
||||
} catch (e) { setMeta(String(e), "err"); }
|
||||
}));
|
||||
|
||||
refresh({ syncForm: true }).catch((e) => setMeta(String(e), "err"));
|
||||
setInterval(() => { if (!busy && !modal.classList.contains("open")) refresh().catch(() => {}); }, 2500);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user