Files
navi/internal/appui/index.html
T

855 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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; }
details.hy2 {
margin-bottom: 14px;
border: 1px solid var(--line);
border-radius: 16px;
padding: 10px 12px;
background: rgba(255,255,255,.55);
}
details.hy2 summary {
cursor: pointer;
font-weight: 700;
font-size: .84rem;
color: var(--accent);
list-style: none;
}
details.hy2 .grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 10px;
}
details.hy2 label.field { margin-top: 6px; }
.check {
display: flex; align-items: center; gap: 8px;
font-size: .88rem; margin-top: 8px;
}
.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: 10px; }
button.action {
appearance: none; border: 0; border-radius: 16px;
padding: 14px 16px; font: inherit; font-weight: 700; cursor: pointer;
transition: transform .12s, background .15s, opacity .15s, box-shadow .15s, border-color .15s, color .15s;
box-shadow: 0 8px 18px rgba(16, 36, 28, 0.08);
letter-spacing: -0.01em;
}
button.action:active { transform: translateY(1px); box-shadow: 0 3px 10px rgba(16, 36, 28, 0.08); }
button.action:disabled { opacity: .55; cursor: not-allowed; transform: none; box-shadow: none; }
.primary {
background: linear-gradient(145deg, #0c7a55, #12966a);
color: #f4fff9;
box-shadow: 0 12px 28px rgba(12, 122, 85, 0.28);
}
.primary:hover:not(:disabled) {
background: linear-gradient(145deg, #0e8a60, #14a575);
box-shadow: 0 14px 30px rgba(12, 122, 85, 0.34);
}
.primary.danger {
background: linear-gradient(145deg, #b42318, #d6453a);
box-shadow: 0 12px 28px rgba(180, 35, 24, 0.25);
}
.primary.danger:hover:not(:disabled) {
background: linear-gradient(145deg, #c52c20, #e05548);
}
.secondary {
background: linear-gradient(180deg, #ffffff, #eef7f2);
color: var(--ink);
border: 1px solid rgba(16, 36, 28, 0.14);
}
.secondary:hover:not(:disabled) {
background: #fff;
border-color: rgba(12, 122, 85, 0.35);
color: var(--accent);
box-shadow: 0 10px 22px rgba(16, 36, 28, 0.12);
}
.ghost {
background: linear-gradient(180deg, #ffffff, #eef7f2);
color: var(--ink);
border: 1px solid rgba(16, 36, 28, 0.14);
}
.ghost:hover:not(:disabled) {
background: #fff;
border-color: rgba(12, 122, 85, 0.35);
color: var(--accent);
box-shadow: 0 10px 22px rgba(16, 36, 28, 0.12);
}
.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;
width: 100%;
margin-top: 8px;
padding: 0;
border: 0;
background: transparent;
text-align: center;
font: inherit;
font-size: .78rem;
color: var(--accent);
text-decoration: none;
font-weight: 600;
cursor: pointer;
box-shadow: none;
}
.shop-link:hover { text-decoration: underline; }
.tools {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 0;
}
.tools .action {
min-height: 48px;
font-size: .92rem;
}
.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 · Hysteria 2 · несколько серверов</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://… или hysteria2://pass@host:443/" spellcheck="false" />
</div>
<div>
<label class="field" for="subUrl">URL подписки</label>
<div class="profile-bar">
<input id="subUrl" type="text" placeholder="https://…/sub" spellcheck="false" />
<button class="icon-btn" id="subBtn" type="button" title="Обновить подписку" style="width:auto;padding:0 14px;font-size:.8rem"></button>
</div>
</div>
</div>
<details class="hy2" id="hy2Box">
<summary>Hysteria 2 · BBR / Salamander / маскировка</summary>
<div class="grid2">
<div>
<label class="field" for="hy2Congestion">Congestion</label>
<select id="hy2Congestion">
<option value="bbr">BBR</option>
<option value="reno">Reno</option>
</select>
</div>
<div>
<label class="field" for="hy2Bbr">BBR profile</label>
<select id="hy2Bbr">
<option value="standard">standard</option>
<option value="conservative">conservative</option>
<option value="aggressive">aggressive</option>
</select>
</div>
<div>
<label class="field" for="hy2Up">Brutal up</label>
<input id="hy2Up" type="text" placeholder="пусто = BBR" />
</div>
<div>
<label class="field" for="hy2Down">Brutal down</label>
<input id="hy2Down" type="text" placeholder="пусто = BBR" />
</div>
<div>
<label class="field" for="hy2Obfs">Obfs</label>
<select id="hy2Obfs">
<option value="">нет</option>
<option value="salamander">salamander</option>
<option value="gecko">gecko</option>
</select>
</div>
<div>
<label class="field" for="hy2ObfsPass">Obfs password</label>
<input id="hy2ObfsPass" type="text" placeholder="пароль salamander/gecko" />
</div>
<div>
<label class="field" for="hy2Sni">SNI (маскарад HTTPS)</label>
<input id="hy2Sni" type="text" placeholder="cdn.example.com" />
</div>
<div>
<label class="field" for="hy2Pin">pinSHA256</label>
<input id="hy2Pin" type="text" placeholder="опционально" />
</div>
<div>
<label class="field" for="hy2Hop">Hop interval</label>
<input id="hy2Hop" type="text" placeholder="30s" />
</div>
</div>
<label class="check"><input id="hy2Insecure" type="checkbox" /> TLS insecure</label>
<label class="check"><input id="hy2Fast" type="checkbox" /> Fast Open</label>
<label class="check"><input id="hy2Lazy" type="checkbox" /> Lazy connect</label>
</details>
<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 secondary" id="saveBtn" type="button">Сохранить профиль</button>
<div class="tools">
<button class="action secondary" id="pingBtn" type="button">Пинг серверов</button>
<button class="action secondary" id="updCheckBtn" type="button">Проверить обновление</button>
</div>
<button class="action secondary" id="coreBtn" type="button">Установить cores (naive + hysteria2)</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>
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
</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="hysteria2://pass@host:443/ или naive+https://…" />
</div>
</div>
<div class="actions">
<button class="action secondary" 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 subUrl = $("subUrl");
const subBtn = $("subBtn");
const SHOP_URL = "https://evilfox.win/";
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 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.style.color = kind === "err" ? "var(--danger)" : kind === "ok" ? "var(--accent)" : "var(--muted)";
if (kind === "ok" || kind === "err") metaHoldUntil = Date.now() + 8000;
}
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.protocol ? ("[" + p.protocol + "] ") : "") + (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;
subBtn.disabled = busy;
subUrl.disabled = busy;
btn.disabled = busy;
fillProfiles(state.profiles || [], state.active_profile || state.profile);
renderPings(state.pings || []);
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;
}
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 = "Сначала установите cores (naive / hysteria2)";
} else {
detail = "Вставьте ссылку naive или hysteria2://";
}
if (!busy && Date.now() > metaHoldUntil) {
setMeta(detail, state.core_ready === false ? "err" : "");
metaHoldUntil = 0;
}
}
async function refresh(opts) {
const state = await getState();
paint(state, opts);
return state;
}
async function withBusy(fn) {
if (busy) return;
busy = true;
paintButtonsLocked(true);
try {
await fn();
} finally {
busy = false;
try {
await refresh({ syncForm: true });
} catch (e) {
setMeta(String(e), "err");
paintButtonsLocked(false);
}
}
}
function paintButtonsLocked(locked) {
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, updCheckBtn, updateBtn, subBtn, subUrl].forEach((b) => {
if (b) b.disabled = locked;
});
}
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 = "";
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();
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");
}
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();
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("Скачивание обновления…");
const msg = await applyUpdate();
setMeta(String(msg || "Готово"), "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>