646 lines
24 KiB
JavaScript
646 lines
24 KiB
JavaScript
// HTTP bridge for macOS GUI (Windows WebView2 already binds these as natives).
|
||
(function () {
|
||
const methods = [
|
||
"getState","getEditState","connect","disconnect","connectProfile","saveProfile","createProfile",
|
||
"selectProfile","deleteProfile","installCore","openURL","pingServers","pingBest",
|
||
"checkUpdate","applyUpdate","saveHy2","importSubscription","quit"
|
||
];
|
||
if (typeof window.getState === "function") return;
|
||
window.__navisHttp = true;
|
||
async function call(name, args) {
|
||
const headers = { "Content-Type": "application/json" };
|
||
if (window.__NAVIS_TOKEN__) headers["X-Navis-Token"] = window.__NAVIS_TOKEN__;
|
||
const r = await fetch("/api/" + name, {
|
||
method: "POST",
|
||
headers: headers,
|
||
body: JSON.stringify({ args: args || [] })
|
||
});
|
||
if (r.status === 401) throw "unauthorized";
|
||
const j = await r.json();
|
||
if (j && j.error) throw j.error;
|
||
return j ? j.result : null;
|
||
}
|
||
methods.forEach((name) => {
|
||
window[name] = function () {
|
||
return call(name, Array.prototype.slice.call(arguments));
|
||
};
|
||
});
|
||
})();
|
||
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 bestBtn = $("bestBtn");
|
||
const autoBest = $("autoBest");
|
||
const serverList = $("serverList");
|
||
const updCheckBtn = $("updCheckBtn");
|
||
const updateBanner = $("updateBanner");
|
||
const updateBtn = $("updateBtn");
|
||
const skipUpdateBtn = $("skipUpdateBtn");
|
||
const verLabel = $("verLabel");
|
||
const SKIP_UPDATE_KEY = "navis.skipUpdateVersion";
|
||
function skippedVersion() {
|
||
try { return localStorage.getItem(SKIP_UPDATE_KEY) || ""; } catch (_) { return ""; }
|
||
}
|
||
function setSkippedVersion(v) {
|
||
try {
|
||
if (v) localStorage.setItem(SKIP_UPDATE_KEY, String(v));
|
||
else localStorage.removeItem(SKIP_UPDATE_KEY);
|
||
} catch (_) {}
|
||
}
|
||
const quitBtn = $("quitBtn");
|
||
if (window.__navisHttp && quitBtn) {
|
||
quitBtn.hidden = false;
|
||
quitBtn.style.display = "block";
|
||
quitBtn.addEventListener("click", () => {
|
||
withBusy(async () => {
|
||
setMeta("Выход…");
|
||
try { await quit(); } catch (_) {}
|
||
});
|
||
});
|
||
}
|
||
const subUrl = $("subUrl");
|
||
const subBtn = $("subBtn");
|
||
const hero = $("hero");
|
||
const protoChip = $("protoChip");
|
||
const heroHint = $("heroHint");
|
||
const SHOP_URL = "https://evilfox.win/";
|
||
const AUTO_BEST_KEY = "navis.autoBest";
|
||
const THEME_KEY = "navis.theme";
|
||
|
||
(function initThemeToggle() {
|
||
const btn = $("themeBtn");
|
||
if (!btn) return;
|
||
const apply = (theme) => {
|
||
document.documentElement.setAttribute("data-theme", theme);
|
||
try { localStorage.setItem(THEME_KEY, theme); } catch (_) {}
|
||
btn.title = theme === "dark" ? "Светлая тема" : "Тёмная тема";
|
||
btn.setAttribute("aria-label", btn.title);
|
||
};
|
||
btn.addEventListener("click", () => {
|
||
const cur = document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
||
apply(cur === "dark" ? "light" : "dark");
|
||
});
|
||
const cur = document.documentElement.getAttribute("data-theme") === "dark" ? "dark" : "light";
|
||
btn.title = cur === "dark" ? "Светлая тема" : "Тёмная тема";
|
||
btn.setAttribute("aria-label", btn.title);
|
||
})();
|
||
|
||
try {
|
||
const saved = localStorage.getItem(AUTO_BEST_KEY);
|
||
autoBest.checked = saved === "1";
|
||
} catch (_) {}
|
||
autoBest.addEventListener("change", () => {
|
||
try { localStorage.setItem(AUTO_BEST_KEY, autoBest.checked ? "1" : "0"); } catch (_) {}
|
||
});
|
||
|
||
function readHy2() {
|
||
return {
|
||
congestion: $("hy2Congestion").value,
|
||
bbr_profile: $("hy2Bbr").value,
|
||
bandwidth_up: $("hy2Up").value.trim(),
|
||
bandwidth_down: $("hy2Down").value.trim(),
|
||
obfs: $("hy2Obfs").value,
|
||
obfs_password: $("hy2ObfsPass").value.trim(),
|
||
sni: $("hy2Sni").value.trim(),
|
||
insecure: !!$("hy2Insecure").checked,
|
||
pin_sha256: $("hy2Pin").value.trim(),
|
||
fast_open: !!$("hy2Fast").checked,
|
||
lazy: !!$("hy2Lazy").checked,
|
||
hop_interval: $("hy2Hop").value.trim()
|
||
};
|
||
}
|
||
|
||
function fillHy2(h) {
|
||
if (!h) return;
|
||
if (h.congestion) $("hy2Congestion").value = h.congestion;
|
||
if (h.bbr_profile) $("hy2Bbr").value = h.bbr_profile;
|
||
$("hy2Up").value = h.bandwidth_up || "";
|
||
$("hy2Down").value = h.bandwidth_down || "";
|
||
$("hy2Obfs").value = h.obfs || "";
|
||
$("hy2ObfsPass").value = h.obfs_password || "";
|
||
$("hy2Sni").value = h.sni || "";
|
||
$("hy2Pin").value = h.pin_sha256 || "";
|
||
$("hy2Hop").value = h.hop_interval || "";
|
||
$("hy2Insecure").checked = !!h.insecure;
|
||
$("hy2Fast").checked = !!h.fast_open;
|
||
$("hy2Lazy").checked = !!h.lazy;
|
||
}
|
||
|
||
async function persistHy2IfNeeded(protocol) {
|
||
const p = (protocol || "").toLowerCase();
|
||
const link = proxy.value.trim().toLowerCase();
|
||
if (p === "hysteria2" || link.startsWith("hysteria2://") || link.startsWith("hy2://")) {
|
||
await saveHy2(readHy2());
|
||
}
|
||
}
|
||
|
||
let busy = false;
|
||
let connected = false;
|
||
let formHydrated = false;
|
||
let dirty = false;
|
||
let profiles = [];
|
||
let pingMap = {};
|
||
let metaHoldUntil = 0;
|
||
|
||
function markDirty() { dirty = true; }
|
||
[
|
||
proxy, nameInput, sysproxy, subUrl,
|
||
$("hy2Congestion"), $("hy2Bbr"), $("hy2Up"), $("hy2Down"),
|
||
$("hy2Obfs"), $("hy2ObfsPass"), $("hy2Sni"), $("hy2Pin"), $("hy2Hop"),
|
||
$("hy2Insecure"), $("hy2Fast"), $("hy2Lazy")
|
||
].forEach((el) => {
|
||
if (!el) return;
|
||
el.addEventListener("input", markDirty);
|
||
el.addEventListener("change", markDirty);
|
||
});
|
||
|
||
function setMeta(text, kind) {
|
||
meta.textContent = text || "";
|
||
meta.classList.remove("ok", "err");
|
||
if (kind === "ok") meta.classList.add("ok");
|
||
if (kind === "err") meta.classList.add("err");
|
||
if (kind === "ok" || kind === "err") metaHoldUntil = Date.now() + 8000;
|
||
}
|
||
|
||
function detectProtoLabel(state, active) {
|
||
const p = (state.protocol || active.protocol || "").toLowerCase();
|
||
const link = (typeof state.proxy === "string" ? state.proxy : (active.proxy || "")).trim().toLowerCase();
|
||
if (p) return p;
|
||
if (link.startsWith("vless://")) return "vless";
|
||
if (link.startsWith("vmess://")) return "vmess";
|
||
if (link.startsWith("trojan://")) return "trojan";
|
||
if (link.startsWith("hysteria2://") || link.startsWith("hy2://")) return "hysteria2";
|
||
if (link.startsWith("awg://") || link.includes("[interface]")) return "awg";
|
||
if (link.startsWith("naive") || link.startsWith("https://") || link.startsWith("quic://")) return "naive";
|
||
return "протокол";
|
||
}
|
||
|
||
function msClass(ms, ok) {
|
||
if (!ok) return "bad";
|
||
if (ms < 80) return "good";
|
||
if (ms < 180) return "ok";
|
||
if (ms < 350) return "mid";
|
||
return "bad";
|
||
}
|
||
|
||
function rememberPings(pings) {
|
||
pingMap = {};
|
||
(pings || []).forEach((p) => { pingMap[p.name] = p; });
|
||
}
|
||
|
||
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.name;
|
||
profile.appendChild(opt);
|
||
});
|
||
const want = active || cur || (profiles[0] && profiles[0].name);
|
||
if (want) profile.value = want;
|
||
renderServerList(want);
|
||
}
|
||
|
||
function orderedProfiles() {
|
||
const list = profiles.slice();
|
||
list.sort((a, b) => {
|
||
const pa = pingMap[a.name], pb = pingMap[b.name];
|
||
const oa = pa && pa.ok, ob = pb && pb.ok;
|
||
if (oa !== ob) return oa ? -1 : 1;
|
||
if (oa && ob && pa.ms !== pb.ms) return pa.ms - pb.ms;
|
||
return String(a.name).localeCompare(String(b.name));
|
||
});
|
||
return list;
|
||
}
|
||
|
||
function renderServerList(activeName) {
|
||
const active = activeName || profile.value;
|
||
serverList.innerHTML = "";
|
||
const list = orderedProfiles();
|
||
if (!list.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "server-empty";
|
||
empty.textContent = "Нет серверов — добавьте профиль или подписку";
|
||
serverList.appendChild(empty);
|
||
return;
|
||
}
|
||
list.forEach((p) => {
|
||
const row = document.createElement("button");
|
||
row.type = "button";
|
||
row.className = "server-row" + (p.name === active ? " active" : "");
|
||
row.dataset.name = p.name;
|
||
|
||
const left = document.createElement("div");
|
||
left.className = "left";
|
||
const nameEl = document.createElement("div");
|
||
nameEl.className = "name";
|
||
nameEl.textContent = p.name;
|
||
const sub = document.createElement("div");
|
||
sub.className = "sub";
|
||
sub.textContent = p.host || "нет хоста";
|
||
left.appendChild(nameEl);
|
||
left.appendChild(sub);
|
||
|
||
const right = document.createElement("div");
|
||
right.className = "right";
|
||
const proto = document.createElement("span");
|
||
proto.className = "proto";
|
||
proto.textContent = (p.protocol || "?").toString();
|
||
const ms = document.createElement("span");
|
||
const pr = pingMap[p.name];
|
||
if (pr && pr.ok) {
|
||
ms.className = "ms " + msClass(pr.ms, true);
|
||
ms.textContent = pr.ms + " ms";
|
||
} else if (pr && pr.error) {
|
||
ms.className = "ms bad";
|
||
ms.textContent = "—";
|
||
ms.title = pr.error;
|
||
} else {
|
||
ms.className = "ms";
|
||
ms.textContent = "…";
|
||
}
|
||
right.appendChild(proto);
|
||
right.appendChild(ms);
|
||
|
||
row.appendChild(left);
|
||
row.appendChild(right);
|
||
row.addEventListener("click", () => onServerClick(p.name));
|
||
row.addEventListener("dblclick", () => onServerConnect(p.name));
|
||
serverList.appendChild(row);
|
||
});
|
||
}
|
||
|
||
async function onServerClick(name) {
|
||
if (busy) return;
|
||
if (connected) {
|
||
if (name !== profile.value) await onServerConnect(name);
|
||
return;
|
||
}
|
||
await withBusy(async () => {
|
||
await selectProfile(name);
|
||
dirty = false;
|
||
formHydrated = false;
|
||
});
|
||
}
|
||
|
||
async function onServerConnect(name) {
|
||
if (busy) return;
|
||
await withBusy(async () => {
|
||
setMeta("Подключение к «" + name + "»…");
|
||
await connectProfile(name);
|
||
setMeta("Подключено: " + name, "ok");
|
||
});
|
||
}
|
||
|
||
function renderUpdate(u, version) {
|
||
verLabel.textContent = "Navis v" + (version || "?");
|
||
const badge = $("badgeVer");
|
||
if (badge && version) badge.textContent = version;
|
||
if (!u) {
|
||
updateBanner.classList.remove("show");
|
||
return;
|
||
}
|
||
const latest = (u.latest || "").toString();
|
||
const skip = skippedVersion();
|
||
if (u.available && latest && skip === latest) {
|
||
updateBanner.classList.remove("show");
|
||
return;
|
||
}
|
||
if (u.available) {
|
||
updateBanner.classList.add("show");
|
||
$("updateTitle").textContent = "Доступно обновление " + latest;
|
||
$("updateNotes").textContent = u.notes || ("У вас " + (u.current || version || "?") + ". Обновление только по кнопке.");
|
||
updateBanner.dataset.latest = latest;
|
||
} else {
|
||
updateBanner.classList.remove("show");
|
||
}
|
||
}
|
||
|
||
function paint(state, opts) {
|
||
const syncForm = opts && opts.syncForm;
|
||
connected = !!state.connected;
|
||
dot.classList.toggle("on", connected);
|
||
hero.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;
|
||
bestBtn.disabled = busy;
|
||
updCheckBtn.disabled = busy;
|
||
updateBtn.disabled = busy;
|
||
subBtn.disabled = busy;
|
||
subUrl.disabled = busy;
|
||
btn.disabled = busy;
|
||
|
||
rememberPings(state.pings || []);
|
||
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
||
renderUpdate(state.update, state.version);
|
||
if (typeof state.subscription_url === "string" && !dirty) {
|
||
subUrl.value = state.subscription_url;
|
||
}
|
||
|
||
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;
|
||
fillHy2(state.hy2);
|
||
formHydrated = true;
|
||
if (syncForm) dirty = false;
|
||
}
|
||
|
||
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
|
||
const label = detectProtoLabel(state, active);
|
||
protoChip.textContent = label;
|
||
protoChip.classList.toggle("active", !!state.connected || label !== "протокол");
|
||
const hy2 = (label === "hysteria2" || label === "hy2");
|
||
if ($("hy2Box")) $("hy2Box").style.display = hy2 ? "" : "none";
|
||
|
||
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(" · ") || "Туннель активен";
|
||
heroHint.textContent = detail;
|
||
} else if (state.core_ready === false) {
|
||
detail = "Сначала установите cores в разделе «Сервис»";
|
||
heroHint.textContent = "Нужны cores для выбранного протокола";
|
||
} else {
|
||
const n = (state.profiles || []).length;
|
||
detail = n > 1 ? ("Серверов: " + n + " · нажмите Пинг или Лучший") : "Готово к подключению";
|
||
heroHint.textContent = n > 1 ? "Клик — выбрать, двойной клик — подключить" : "Выберите сервер и нажмите Подключить";
|
||
}
|
||
if (!busy && Date.now() > metaHoldUntil) {
|
||
setMeta(detail, state.core_ready === false ? "err" : "");
|
||
metaHoldUntil = 0;
|
||
}
|
||
}
|
||
|
||
async function refresh(opts) {
|
||
const needSecrets = !!(opts && opts.syncForm) || (!formHydrated && !dirty);
|
||
let state;
|
||
if (needSecrets && typeof getEditState === "function") {
|
||
state = await getEditState();
|
||
} else {
|
||
state = await getState();
|
||
}
|
||
paint(state, opts);
|
||
return state;
|
||
}
|
||
|
||
function fmtErr(e) {
|
||
if (e == null) return "ошибка";
|
||
if (typeof e === "string") return e;
|
||
if (typeof e === "object" && e.message) return String(e.message);
|
||
return String(e);
|
||
}
|
||
|
||
async function withBusy(fn) {
|
||
if (busy) return;
|
||
busy = true;
|
||
paintButtonsLocked(true);
|
||
try {
|
||
await fn();
|
||
} catch (e) {
|
||
setMeta(fmtErr(e), "err");
|
||
} finally {
|
||
busy = false;
|
||
try {
|
||
await refresh({ syncForm: true });
|
||
} catch (e) {
|
||
setMeta(fmtErr(e), "err");
|
||
paintButtonsLocked(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
function paintButtonsLocked(locked) {
|
||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, updateBtn, skipUpdateBtn, subBtn, subUrl].forEach((b) => {
|
||
if (b) b.disabled = locked;
|
||
});
|
||
}
|
||
|
||
async function runBest(autoConnect) {
|
||
setMeta(autoConnect ? "Пинг и автоподключение…" : "Пинг и выбор лучшего…");
|
||
const res = await pingBest(!!autoConnect);
|
||
rememberPings(res.pings || []);
|
||
renderServerList(res.best_name || profile.value);
|
||
if (res.best_name) {
|
||
setMeta("Лучший: " + res.best_name + " · " + res.best_ms + " ms" + (res.connected ? " · подключено" : ""), "ok");
|
||
}
|
||
return res;
|
||
}
|
||
|
||
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 persistHy2IfNeeded();
|
||
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);
|
||
await persistHy2IfNeeded();
|
||
setMeta("Профиль сохранён", "ok");
|
||
} catch (e) { setMeta(String(e), "err"); }
|
||
}));
|
||
|
||
coreBtn.addEventListener("click", () => withBusy(async () => {
|
||
try {
|
||
setMeta("Скачивание official cores…");
|
||
const path = await installCore();
|
||
setMeta("Cores готовы: " + 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 = "";
|
||
const me = $("modalErr");
|
||
if (me) { me.style.display = "none"; me.textContent = ""; me.className = "meta"; }
|
||
modal.classList.add("open");
|
||
$("newName").focus();
|
||
});
|
||
$("cancelNew").addEventListener("click", () => modal.classList.remove("open"));
|
||
modal.addEventListener("click", (e) => {
|
||
if (e.target === modal) modal.classList.remove("open");
|
||
});
|
||
function sanitizeShare(s) {
|
||
return String(s || "")
|
||
.replace(/[\u200B-\u200D\u2060\uFEFF\u2066-\u2069\u200E\u200F\u202A-\u202E]/g, "")
|
||
.trim();
|
||
}
|
||
$("createNew").addEventListener("click", () => withBusy(async () => {
|
||
const me = $("modalErr");
|
||
const showModalErr = (msg) => {
|
||
if (!me) { setMeta(String(msg), "err"); return; }
|
||
me.style.display = "block";
|
||
me.className = "meta err";
|
||
me.textContent = String(msg);
|
||
};
|
||
try {
|
||
const n = $("newName").value.trim();
|
||
const p = sanitizeShare($("newProxy").value);
|
||
if (!n) throw "Укажите название профиля";
|
||
if (!p) throw "Вставьте ссылку или конфиг Amnezia/AWG";
|
||
$("newProxy").value = p;
|
||
await createProfile(n, p, !!sysproxy.checked);
|
||
modal.classList.remove("open");
|
||
if (me) { me.style.display = "none"; me.textContent = ""; }
|
||
formHydrated = false;
|
||
dirty = false;
|
||
setMeta("Профиль создан", "ok");
|
||
} catch (e) { showModalErr(e); }
|
||
}));
|
||
|
||
async function openShop(e) {
|
||
if (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
}
|
||
try {
|
||
setMeta("Открываю evilfox.win…");
|
||
await openURL(SHOP_URL);
|
||
setMeta("Открыто в браузере", "ok");
|
||
} catch (err) {
|
||
setMeta("Не удалось открыть ссылку: " + String(err), "err");
|
||
}
|
||
}
|
||
shopBtn.addEventListener("click", openShop);
|
||
shopLink.addEventListener("click", openShop);
|
||
|
||
async function runImportSubscription() {
|
||
const url = subUrl.value.trim();
|
||
if (!url) {
|
||
setMeta("Вставьте URL подписки", "err");
|
||
return;
|
||
}
|
||
setMeta("Загрузка подписки…");
|
||
const n = await importSubscription(url);
|
||
formHydrated = false;
|
||
dirty = false;
|
||
setMeta("Импортировано: " + n + " · измеряю пинг…", "ok");
|
||
await runBest(!!autoBest.checked);
|
||
}
|
||
|
||
subBtn.addEventListener("click", () => withBusy(async () => {
|
||
try {
|
||
await runImportSubscription();
|
||
} catch (e) { setMeta(String(e), "err"); }
|
||
}));
|
||
subUrl.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
subBtn.click();
|
||
}
|
||
});
|
||
|
||
pingBtn.addEventListener("click", () => withBusy(async () => {
|
||
try {
|
||
setMeta("Пинг серверов…");
|
||
const rows = await pingServers();
|
||
rememberPings(rows);
|
||
renderServerList(profile.value);
|
||
const ok = (rows || []).filter((r) => r.ok).length;
|
||
setMeta("Пинг: " + ok + "/" + (rows || []).length + " доступны", ok ? "ok" : "err");
|
||
} catch (e) { setMeta(String(e), "err"); }
|
||
}));
|
||
|
||
bestBtn.addEventListener("click", () => withBusy(async () => {
|
||
try {
|
||
await runBest(!!autoBest.checked);
|
||
} 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("Скачивание и установка обновления…");
|
||
const msg = await applyUpdate();
|
||
setMeta(String(msg || "Перезапуск…"), "ok");
|
||
} catch (e) { setMeta(String(e), "err"); }
|
||
}));
|
||
|
||
skipUpdateBtn.addEventListener("click", () => {
|
||
const latest = updateBanner.dataset.latest || "";
|
||
if (latest) setSkippedVersion(latest);
|
||
updateBanner.classList.remove("show");
|
||
setMeta("Версия " + (latest || "?") + " пропущена. Следующую предложим.", "ok");
|
||
});
|
||
|
||
(async () => {
|
||
try {
|
||
await refresh({ syncForm: true });
|
||
// Auto-best connect is opt-in via checkbox only — never on first paint.
|
||
} catch (e) {
|
||
setMeta(String(e), "err");
|
||
}
|
||
})();
|
||
setInterval(() => { if (!busy && !modal.classList.contains("open")) refresh().catch(() => {}); }, 2500);
|
||
|