@@ -273,8 +273,9 @@
overflow-y: auto;
padding: 18px 20px 8px;
}
.page { display: none; max-width: 640px; margin: 0 auto; animation: rise .3s var(--ease) both; }
.page.active { display: block ; }
/* Inactive pages must not paint (WebView2 overflow ghosts from hidden siblings). */
.page { display: none !important; max-width: 640px; margin: 0 auto ; }
.page.active { display: block !important; animation: rise .3s var(--ease) both; }
@keyframes rise {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: none; }
@@ -308,9 +309,12 @@
.update-actions .action { flex: 1 1 auto; min-width: 140px; }
/* ==== Home / power ==== */
/* Sticky so the big connect control never scrolls under the provider list. */
.hero {
position: relative ;
overflow: hidden ;
position: sticky ;
top: 0 ;
z-index: 6;
overflow: visible;
border-radius: var(--radius);
border: 1px solid var(--line);
background: var(--hero-bg);
@@ -318,6 +322,8 @@
margin-bottom: 14px;
text-align: center;
transition: border-color .25s, box-shadow .25s, background .25s;
min-height: 220px;
box-sizing: border-box;
}
.hero.on {
border-color: rgba(13,138,102,.35);
@@ -327,6 +333,8 @@
.power-btn {
width: 148px;
height: 148px;
min-width: 148px;
min-height: 148px;
border-radius: 50%;
border: 0;
cursor: pointer;
@@ -341,6 +349,7 @@
transition: background .25s, color .25s, transform .12s, box-shadow .25s;
box-shadow: 0 14px 34px rgba(8, 145, 178, .3);
position: relative;
flex-shrink: 0;
}
.power-btn:hover { transform: translateY(-2px); }
.power-btn:active { transform: none; }
@@ -1336,7 +1345,7 @@
< div class = "brand-row" >
< div class = "logo" aria-hidden = "true" > < span class = "fox" > 🦊< / span > < / div >
< h1 class = "brand" > EvilFox< / h1 >
< span class = "badge-ver" id = "badgeVer" > 4.2.0 < / span >
< span class = "badge-ver" id = "badgeVer" hidden > < / span >
< / div >
< button class = "nav-btn active" type = "button" data-page = "guard" >
@@ -1362,7 +1371,7 @@
< div class = "spacer" > < / div >
< div class = "side-foot" >
< span class = "ver" id = "verLabel" > EvilFox< / span >
< span class = "ver" id = "verLabel" > EvilFox< / span > <!-- версия только в «Помощь» -->
< button type = "button" class = "theme-toggle" id = "themeToggle" title = "Тема" aria-label = "Переключить тему" >
< svg class = "icon-moon" viewBox = "0 0 24 24" fill = "none" aria-hidden = "true" >
< path d = "M20.5 14.2A8.2 8.2 0 0 1 9.8 3.5 8.5 8.5 0 1 0 20.5 14.2z" stroke = "currentColor" stroke-width = "1.8" stroke-linejoin = "round" / >
@@ -1390,8 +1399,8 @@
<!-- ============ Защита ============ -->
< section class = "page active" data-page = "guard" >
< section class = "hero" id = "hero" >
< button class = "power-btn" id = "toggleBtn" type = "button" aria-label = "Подключить / отключить" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.1" stroke-linecap = "round" >
< button class = "power-btn" id = "toggleBtn" type = "button" aria-label = "Подключиться или отключиться " >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.1" stroke-linecap = "round" aria-hidden = "true" >
< path d = "M12 3v8" / >
< path d = "M6.3 6.6a8 8 0 1 0 11.4 0" / >
< / svg >
@@ -1404,7 +1413,7 @@
< div class = "chips-line" >
< span class = "mode-chip" id = "vpnChip" hidden > VPN · весь трафик< / span >
< / div >
< p class = "hero-hint" id = "heroHint" > Нажмите, чтобы включить защиту< / p >
< p class = "hero-hint" id = "heroHint" > Нажмите большую кнопку , чтобы включить защиту< / p >
< / section >
< button class = "cur-server" id = "curServer" type = "button" title = "Открыть список серверов" >
@@ -1428,37 +1437,33 @@
< div class = "traffic-sub" id = "trafficExp" hidden > < / div >
< / section >
< div class = "section" >
< div class = "section-title" > < span > Выберите п ровайдера < / span > < / div >
< div class = "section" id = "provSection" >
< div class = "section-title" > < span > П ровайдеры < / span > < / div >
< div class = "prov-grid" id = "provGrid" >
< div class = "prov-card ours" >
< span class = "prov-badge" > ⭐ Рекомендуемый сервис < / span >
< span class = "prov-badge" > ⭐ Рекомендуемый< / span >
< div class = "prov-name" > 🦊 EvilFox VPN< / div >
< div class = "prov-sub" > Оптимальные серверы рядом с вами · защита без настройки< / div >
< span class = "prov-gift" id = "provGift" > 🚀 3 дня бесплатно< / span >
< span class = "prov-gift" id = "provGift" > 🎁 3 дня бесплатного доступа < / span >
< button class = "action primary" id = "provConnectBtn" type = "button" > 🔥 Подключиться< / button >
< button class = "prov-ping" id = "provPingOurs" type = "button" > 📊 < span class = "val" > Проверить скорость< / span > < / button >
< / div >
< div class = "prov-card" >
< span class = "prov-tag" > Сторонний сервис < / span >
< span class = "prov-tag" > Сторонний< / span >
< div class = "prov-name" > NordVPN< / div >
< div class = "prov-sub" > Оплата и настройка на сайте провайдера< / div >
< button class = "prov-go" type = "button" data-url = "https://nordvpn.com/" data-name = "NordVPN" > Перейти на сайт< / button >
< button class = "prov-ping" type = "button" data-base = "45" > 📊 < span class = "val" > Проверить скорость< / span > < / button >
< / div >
< div class = "prov-card" >
< span class = "prov-tag" > Сторонний сервис < / span >
< span class = "prov-tag" > Сторонний< / span >
< div class = "prov-name" > ExpressVPN< / div >
< div class = "prov-sub" > Оплата и настройка на сайте провайдера< / div >
< button class = "prov-go" type = "button" data-url = "https://expressvpn.com/" data-name = "ExpressVPN" > Перейти на сайт< / button >
< button class = "prov-ping" type = "button" data-base = "38" > 📊 < span class = "val" > Проверить скорость< / span > < / button >
< / div >
< div class = "prov-card" >
< span class = "prov-tag" > Сторонний сервис < / span >
< span class = "prov-tag" > Сторонний< / span >
< div class = "prov-name" > Surfshark< / div >
< div class = "prov-sub" > Оплата и настройка на сайте провайдера< / div >
< button class = "prov-go" type = "button" data-url = "https://surfshark.com/" data-name = "Surfshark" > Перейти на сайт< / button >
< button class = "prov-ping" type = "button" data-base = "55" > 📊 < span class = "val" > Проверить скорость< / span > < / button >
< / div >
< / div >
< / div >
@@ -1478,12 +1483,10 @@
< / section >
< div class = "server-toolbar" >
< div class = "section-title" > < span > Серверы< / span > < span class = "count-note" id = "srvCount" > < / span > < / div >
< div class = "section-title" > < span > 🌍 Серверы< / span > < span class = "count-note" id = "srvCount" > < / span > < / div >
< div class = "server-actions" >
< button class = "mini" id = "pingBtn" type = "button" > Пинг< / button >
< button class = "mini accent" id = "bestBtn" type = "button" > Л учший< / button >
< button class = "icon-btn" id = "addBtn" type = "button" title = "Новый профиль" style = "width:34px;height:34px;font-size:1rem" > +< / button >
< button class = "icon-btn danger" id = "delBtn" type = "button" title = "Удалить" style = "width:34px;height:34px;font-size:1rem" > × < / button >
< button class = "mini accent" id = "bestBtn" type = "button" title = "Выбрать сервер с наименьшим пингом" > Выбрать л учший< / button >
< / div >
< / div >
< div class = "server-list" id = "serverList" > < / div >
@@ -1507,18 +1510,16 @@
< details class = "profile-edit" id = "profileEdit" >
< summary > Свойства сервера< / summary >
< div class = "stack" style = "margin-top:8px" >
< p class = "host-line" > Адрес сервера: < span id = "profHost " > —< / span > < / p >
< 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 >
< textarea id = "proxy" rows = "3" placeholder = "vless:// · vmess:// · trojan:// · hy2:// · naive+https:// · AWG .conf" spellcheck = "false" > < / textarea >
< / div >
< button class = "action secondary" id = "saveBtn" type = "button" > Сохранить профиль< / button >
< div class = "stack" style = "margin-top:8px" id = "profProps" >
< div class = "subinfo-row" > < span class = "k" > Сервер < / span > < span class = "v" id = "propName " > —< / span > < / div >
< div class = "subinfo-row" > < span class = "k" > Пинг < / span > < span class = "v" id = "propPing" > — < / span > < / div >
< div class = "subinfo-row" > < span class = "k" > Протокол < / span > < span class = "v " id = "propProto" > — < / span > < / div >
< div class = "subinfo-row" > < span class = "k" > Статус < / span > < span class = "v " id = "propStatus" > — < / span > < / div >
< / div >
<!-- Скрытые поля — нужны для connect/save, в UI не показываем. -->
< input id = "name" type = "text" hidden / >
< textarea id = "proxy" hidden rows = "1" > < / textarea >
< button class = "action secondary" id = "saveBtn" type = "button" hidden > Сохранить профиль< / button >
< / details >
< / section >
@@ -1641,11 +1642,11 @@
< details class = "panel" id = "advBox" >
< summary > Расширенные настройки< / summary >
< div style = "margin-top:6px" >
< div style = "margin-top:6px" id = "proxyParamsBlock" hidden >
< div class = "section-title" style = "margin-bottom:2px" > < span > Параметры прокси< / span > < / div >
< p class = "adv-line" > HTTP: < span id = "advHttp" > —< / span > < / p >
< p class = "adv-line" > SOCKS: < span id = "advSocks" > —< / span > < / p >
< p class = "remna-hint" style = "margin-top:8px" > Локальные адреса прокси на этом компьютере — нужны только для ручной настройки отдельных программ. Обычным пользователям менять ничего не требуется. < / p >
< p class = "remna-hint" style = "margin-top:8px" > Локальные адреса прокси на этом компьютере — нужны только для ручной настройки отдельных программ.< / p >
< / div >
< / details >
< / section >
@@ -1707,7 +1708,7 @@
< / details >
< details class = "panel" >
< summary > Не подключается — что делать?< / summary >
< p class = "remna-hint" style = "margin-top:6px" > Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Л учший»). Подробности — в журнале на вкладке «Статистика».< / p >
< p class = "remna-hint" style = "margin-top:6px" > Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Выбрать л учший»). Подробности — в журнале на вкладке «Статистика».< / p >
< / details >
< div class = "about-card" >
@@ -1725,10 +1726,9 @@
< button class = "action primary" id = "shopBtn" type = "button" > Перейти на сайт< / button >
< button class = "shop-link" id = "shopLink" type = "button" > https://evilfox.win/< / button >
< / section >
<!-- Рекомендуемые сервисы: ненавязчивый блок в нижнем углу «О программе».
Ссылки задаются в скрипте — константа RECOMMENDED_SERVICES. -->
< section class = "svc" aria-label = "Рекомендуемые сервисы" >
< h4 > Рекомендуемые сервисы< / h4 >
<!-- Полезные ссылки (сайт + поддержка). Хосты — в allowlist openURL. -->
< section class = "svc" aria-label = "Полезные ссылки" >
< h4 > 🌐 Полезные ссылки < / h4 >
< div class = "svc-grid" id = "svcGrid" > < / div >
< / section >
< button class = "shop-link" id = "quitBtn" type = "button" hidden style = "display:none;margin:12px auto 0;width:auto" > Выйти< / button >
@@ -1791,8 +1791,8 @@
const btn = $("toggleBtn");
const coreBtn = $("coreBtn");
const saveBtn = $("saveBtn");
const addBtn = $("addBtn");
const delBtn = $("delBtn");
const addBtn = $("addBtn"); // optional — header + removed
const delBtn = $("delBtn"); // optional — header × removed
const proxy = $("proxy");
const nameInput = $("name");
const sysproxy = $("sysproxy");
@@ -1843,10 +1843,30 @@
PT: "Португалия", RO: "Румыния", RS: "Сербия", RU: "Россия", SE: "Швеция",
SG: "Сингапур", SK: "Словакия", TR: "Турция", UA: "Украина", US: "США", ZA: "ЮАР"
};
// Опечатки / варианты имён стран → ISO (зеркало Go geoflag; длинные сначала).
const COUNTRY_NAME_ALIASES = [
["румьиния", "RO"], ["румяння", "RO"], ["румьиня", "RO"],
["румыния", "RO"], ["румыня", "RO"], ["руминия", "RO"], ["румыниа", "RO"],
["romania", "RO"], ["bucharest", "RO"], ["бухарест", "RO"]
];
function countryRu(cc) {
const c = String(cc || "").toUpperCase();
return COUNTRY_RU[c] || "";
}
function isoFromNameText(text) {
const lower = String(text || "").toLowerCase();
if (!lower) return "";
for (let i = 0; i < COUNTRY_NAME_ALIASES.length ; i + + ) {
if (lower.indexOf(COUNTRY_NAME_ALIASES[i][0]) >= 0) return COUNTRY_NAME_ALIASES[i][1];
}
// Прямое совпадение канонического русского названия.
const keys = Object.keys(COUNTRY_RU);
for (let i = 0; i < keys.length ; i + + ) {
const ru = COUNTRY_RU[keys[i]].toLowerCase();
if (ru & & lower.indexOf(ru) >= 0) return keys[i];
}
return "";
}
// ==== SVG-флаги ====
// Windows не умеет эмодзи-флаги: Segoe UI Emoji рисует пары региональных
// индикаторов буквами («BE», «LV»), поэтому рисуем настоящие SVG-флаги.
@@ -1976,7 +1996,7 @@
if (d.e) b += d.e;
return '< svg class = "fl" viewBox = "0 0 60 40" xmlns = "http://www.w3.org/2000/svg" aria-hidden = "true" > ' + b + '< / svg > ';
}
// ISO2 страны профиля: country от бэкенда, иначе распознаём эмодзи-флаг .
// ISO2 страны профиля: country от бэкенда, иначе имя/флаг, иначе алиасы опечаток .
function profileISO(p) {
if (!p) return "";
const c = String(p.country || "").toUpperCase();
@@ -1986,7 +2006,7 @@
const a = rs[0].codePointAt(0) - 0x1f1e6, b = rs[1].codePointAt(0) - 0x1f1e6;
if (a >= 0 & & a < 26 & & b > = 0 & & b < 26 ) return String . fromCharCode ( 65 + a , 65 + b ) ;
}
return "";
return isoFromNameText(p.name) || isoFromNameText(p.host) || "";
}
// Заполнить ячейку флага: SVG для известной страны, буквенный код — фолбэк.
function setFlagEl(el, p) {
@@ -2084,15 +2104,12 @@
const SHOP_URL = "https://evilfox.win/";
// Текст оффера — правьте здесь (показывается на карточке EvilFox на вкладке «Защита»).
// Правила Store: никаких цен и упоминаний оплаты внутри приложения.
const OFFER_TEXT = "🚀 3 дня бесплатно";
// Список рекомендуемых сервисов — замените ссылки/названия здесь.
// Хост каждой ссылки должен быть разрешён в allowlist openURL
// (internal/apphost/app.go → allowedLinkHosts), иначе ссылка не откроется.
const OFFER_TEXT = "🎁 3 дня бесплатного доступа ";
// Полезные ссылки (Помощь). Хост каждой ссылки — в allowlist openURL
// (internal/apphost/app.go → allowedLinkHosts).
const RECOMMENDED_SERVICES = [
{ icon: "🛒 ", name: "EvilFox Shop", url: "https://evilfox.win/" },
{ icon: "✈️", name: "Telegram", url: "https://t.me/evilfox" },
{ icon: "🖥️", name: "Хостинг de4ima", url: "https://de4ima.uk" },
{ icon: "🦊", name: "Git", url: "https://git.de4ima.uk/Evilfox/navi" }
{ icon: "🌐 ", name: "Сайт", url: "https://evilfox.win/" },
{ icon: "✈️", name: "Поддержка: Telegram", url: "https://t.me/evilfox" }
];
const AUTO_BEST_KEY = "navis.autoBest";
// Оффер на карточке EvilFox рендерим из константы, чтобы правка была в одном месте.
@@ -2268,7 +2285,7 @@
async function persistHy2IfNeeded(protocol) {
const p = (protocol || "").toLowerCase();
const link = proxy.value.trim().toLowerCase();
const link = ((proxy & & proxy.value) || "") .trim().toLowerCase();
if (p === "hysteria2" || link.startsWith("hysteria2://") || link.startsWith("hy2://")) {
await saveHy2(readHy2());
}
@@ -2334,12 +2351,13 @@
// Человекочитаемые имена серверов («Чехия — оптимальный сервер», «Германия — сервер 2»).
// Реальный host:port показываем только в свойствах сервера и в журнале.
// ISO всегда через profileISO (бэкенд + JS-алиасы опечаток) → countryRu, никогда сырое имя панели.
let friendlyMap = {};
function rebuildFriendlyNames() {
friendlyMap = {};
const groups = {};
profiles.forEach((p) => {
const key = String(p.country || "").toUpperCase( ) || "__none__";
const key = profileISO(p ) || "__none__";
(groups[key] = groups[key] || []).push(p);
});
Object.keys(groups).forEach((key) => {
@@ -2360,13 +2378,19 @@
} else {
title = region ? (region + " — сервер " + n) : ("Сервер " + n);
}
friendlyMap[p.name] = { title: title, region: region };
friendlyMap[p.name] = { title: title, region: region, iso: key === "__none__" ? "" : key };
});
});
}
function friendlyTitle(name) {
const f = friendlyMap[name];
return (f & & f.title) || name || "" ;
if (f & & f.title) return f.title ;
// Фолбэк: не показываем сырые опечатки панели — только каноническое имя по ISO.
const p = profiles.find((x) => x.name === name);
const iso = profileISO(p) || isoFromNameText(name);
const region = countryRu(iso);
if (region) return region;
return "Сервер";
}
function fillProfiles(list, active) {
@@ -2481,10 +2505,7 @@
const right = document.createElement("div");
right.className = "right";
// Протокол — крошечная буква-иконка с подсказкой; строчный «vless»
// под каждой строкой не повторяем.
const protoBadge = protoLetterBadge(p.protocol);
if (protoBadge) right.appendChild(protoBadge);
// Протокол — только на карточке активного сервера / в «Свойствах», не в строках списка.
const ms = document.createElement("span");
const pr = pingMap[p.name];
if (pr & & pr.ok) {
@@ -2500,12 +2521,12 @@
ms.title = "пинг не измерен";
}
right.appendChild(ms);
// [📊] — проверить скорость только этого сервера.
// [📊] — проверить пинг только этого сервера.
const pingOne = document.createElement("button");
pingOne.type = "button";
pingOne.className = "row-ping";
pingOne.textContent = "📊";
pingOne.title = "Проверить скорость этого сервера";
pingOne.title = "Проверить пинг этого сервера";
pingOne.addEventListener("click", (e) => {
e.stopPropagation();
runPingOne(p.name, pingOne, ms);
@@ -2520,20 +2541,6 @@
});
}
const PROTO_LETTER = {
vless: "V", vmess: "V", trojan: "T", hysteria2: "H", hy2: "H",
naive: "N", awg: "A"
};
function protoLetterBadge(protocol) {
const p = String(protocol || "").toLowerCase();
if (!p) return null;
const el = document.createElement("span");
el.className = "proto-badge";
el.textContent = PROTO_LETTER[p] || p.charAt(0).toUpperCase();
el.title = "Протокол: " + p;
return el;
}
// Пинг одного сервера по кнопке [📊] в строке.
async function runPingOne(name, btnEl, msEl) {
if (btnEl.disabled) return;
@@ -2689,11 +2696,17 @@
const daysLeft = Math.floor((d.getTime() - Date.now()) / 86400000);
const v = $("subExpireVal");
if (daysLeft < 0 ) {
v.textContent = text + " · срок и стёк";
v.textContent = text + " · И стёк";
v.classList.add("warn");
} else {
v.textContent = daysLeft < = 14 ? (text + " · осталось " + daysLeft + " дн.") : text ;
} else if (daysLeft === 0) {
v.textContent = text + " · Сегодня" ;
v.classList.add("warn");
} else if (daysLeft < = 14) {
v.textContent = text + " · осталось " + daysLeft + " дн.";
v.classList.toggle("warn", daysLeft < = 3);
} else {
v.textContent = text;
v.classList.remove("warn");
}
}
@@ -2713,12 +2726,12 @@
}
function renderUpdate(u, version) {
const label = "EvilFox v" + (version || "?");
verLabel.textContent = label ;
// Версия — только в «Помощь»; в сайдбаре бренд без номера.
if (verLabel) verLabel.textContent = "EvilFox" ;
if (aboutVer) aboutVer.textContent = "Версия " + (version || "?");
if (badgeVer & & version ) {
const short = S tring(version).split("+")[0] ;
badgeVer.textContent = short || version ;
if (badgeVer) {
badgeVer.hidden = true ;
badgeVer.textContent = "" ;
}
if (storePackaged || (u & & u.store_managed)) {
updateBanner.classList.remove("show");
@@ -2780,14 +2793,14 @@
}
const lock = connected || busy;
proxy.disabled = lock;
nameInput.disabled = lock;
if (proxy) proxy.disabled = lock;
if (nameInput) 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;
if (addBtn) addBtn.disabled = lock;
if (delBtn) delBtn.disabled = lock || ((state.profiles || profiles).length < = 1);
if (saveBtn) saveBtn.disabled = lock;
if (coreBtn) coreBtn.disabled = busy;
pingBtn.disabled = busy;
bestBtn.disabled = busy;
updCheckBtn.disabled = busy;
@@ -2808,8 +2821,8 @@
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 (nameInput) nameInput.value = active.name || state.active_profile || "";
if (proxy) 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;
@@ -2826,10 +2839,13 @@
const hy2 = (label === "hysteria2" || label === "hy2");
if ($("hy2Box")) $("hy2Box").style.display = hy2 ? "" : "none";
// Технические адреса прокси — только в «Настройки → Расширенные» и на «Статистике» .
// Параметры прокси — только когда подключены (без пустого «появится после…») .
const advHttp = $("advHttp"), advSocks = $("advSocks"), logsProxyLine = $("logsProxyLine");
if (advHttp) advHttp.textContent = state.http_proxy || "появится после подключения" ;
if (advSocks) advSocks.textContent = state.socks_proxy || "появится после подключения" ;
const proxyParamsBlock = $("proxyParamsBlock") ;
const hasProxyParams = !!(state.http_proxy || state.socks_proxy) ;
if (proxyParamsBlock) proxyParamsBlock.hidden = !hasProxyParams;
if (advHttp) advHttp.textContent = state.http_proxy || "—";
if (advSocks) advSocks.textContent = state.socks_proxy || "—";
if (logsProxyLine) {
const parts = [];
if (state.http_proxy) parts.push("HTTP " + state.http_proxy);
@@ -2838,7 +2854,7 @@
logsProxyLine.textContent = parts.length ? ("Прокси: " + parts.join(" · ")) : "";
}
renderProvConnect();
renderProfHost ();
renderProfProps ();
// Нижняя полоса статуса — только для важных сообщений; постоянного
// «Готово к подключению» больше нет (статус виден на большой кнопке).
@@ -2878,12 +2894,30 @@
}
}
// Реальный host:port выбранного сервера — в «Свойствах сервера» (муторные детали) .
function renderProfHost () {
const el = $("profHost");
if (!el) return;
// Свойства выбранного сервера: имя / пинг / протокол / статус .
function renderProfProps () {
const p = profiles.find((x) => x.name === profile.value);
el.textContent = (p & & p.host) ? p.host : "—" ;
const nameEl = $("propName"), pingEl = $("propPing"), protoEl = $("propProto"), stEl = $("propStatus") ;
if (!nameEl) return;
if (!p) {
nameEl.textContent = "—";
if (pingEl) pingEl.textContent = "—";
if (protoEl) protoEl.textContent = "—";
if (stEl) stEl.textContent = "—";
return;
}
nameEl.textContent = friendlyTitle(p.name);
const pr = pingMap[p.name];
if (pingEl) {
if (pr & & pr.ok) pingEl.textContent = pr.ms + " ms";
else if (pr & & pr.error) pingEl.textContent = "недоступен";
else pingEl.textContent = "не измерен";
}
if (protoEl) {
const proto = String(p.protocol || "—");
protoEl.textContent = proto.charAt(0).toUpperCase() + proto.slice(1);
}
if (stEl) stEl.textContent = connected ? "🟢 Активен" : "⚪ Не активен";
}
async function refresh(opts) {
@@ -2949,23 +2983,31 @@
await runBest(true);
} else {
setMeta("Подключение…");
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
await persistHy2IfNeeded();
// Профиль уже выбран; скрытые поля name/proxy гидратируются из state.
// Не перезаписываем proxy пустой строкой, если форма ещё не заполнена.
const n = (nameInput & & nameInput.value.trim()) || profile.value;
const px = (proxy & & proxy.value.trim()) || "";
if (n & & px) {
await saveProfile(n, px, !!sysproxy.checked);
await persistHy2IfNeeded();
}
await connect();
setMeta("Защищённое соединение установлено", "ok");
}
} catch (e) { setMeta(String(e), "err"); }
}));
saveBtn.addEventListener("click", () => withBusy(async () => {
if (saveBtn) saveBtn.addEventListener("click", () => withBusy(async () => {
try {
await saveProfile( nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked) ;
const n = (nameInput & & nameInput.value.trim()) || profile.value;
const px = (proxy & & proxy.value.trim()) || "";
await saveProfile(n, px, !!sysproxy.checked);
await persistHy2IfNeeded();
setMeta("Профиль сохранён", "ok");
} catch (e) { setMeta(String(e), "err"); }
}));
coreBtn.addEventListener("click", () => withBusy(async () => {
if (coreBtn) coreBtn.addEventListener("click", () => withBusy(async () => {
try {
setMeta("Скачивание official cores…");
const path = await installCore();
@@ -2973,7 +3015,7 @@
} catch (e) { setMeta(String(e), "err"); }
}));
delBtn.addEventListener("click", () => withBusy(async () => {
if (delBtn) delBtn.addEventListener("click", () => withBusy(async () => {
try {
if (!confirm("Удалить сервер «" + friendlyTitle(profile.value) + "»?")) return;
await deleteProfile(profile.value);
@@ -2983,7 +3025,7 @@
} catch (e) { setMeta(String(e), "err"); }
}));
addBtn.addEventListener("click", () => {
if (addBtn) addBtn.addEventListener("click", () => {
$("newName").value = "";
$("newProxy").value = "";
modal.classList.add("open");
@@ -3022,7 +3064,7 @@
shopBtn.addEventListener("click", openShop);
shopLink.addEventListener("click", openShop);
// «Рекомендуемые сервисы» — рендер из RECOMMENDED_SERVICES (см. константу выше ).
// «Рекомендуемые сервисы» → «Полезные ссылки» (см. RECOMMENDED_SERVICES).
(function renderRecommendedServices() {
const grid = $("svcGrid");
if (!grid) return;
@@ -3097,60 +3139,6 @@
if (!connected & & !busy) btn.click();
});
function setPingBtnResult(b, ms) {
const val = b.querySelector(".val");
if (val) val.textContent = ms + " мс";
b.classList.toggle("good", ms < 40 ) ;
b.classList.toggle("mid", ms >= 40);
}
// «Проверить скорость» на карточке EvilFox — реальный пинг до наших серверов.
const provPingOurs = $("provPingOurs");
if (provPingOurs) provPingOurs.addEventListener("click", async () => {
if (provPingOurs.disabled) return;
const val = provPingOurs.querySelector(".val");
provPingOurs.disabled = true;
if (val) val.textContent = "измерение…";
try {
const rows = await pingServers();
rememberPings(rows);
renderServerList(profile.value);
renderCurServer(profile.value);
const oks = (rows || []).filter((r) => r.ok).map((r) => r.ms);
if (oks.length) {
setPingBtnResult(provPingOurs, Math.min.apply(null, oks));
} else if (val) {
val.textContent = "серверы недоступны";
}
} catch (_) {
if (val) val.textContent = "не удалось измерить";
} finally {
provPingOurs.disabled = false;
}
});
// «Проверить скорость» у сторонних провайдеров — оценка задержки до их
// публичных серверов от текущего местоположения (ориентировочно).
document.querySelectorAll(".prov-ping[data-base]").forEach((b) => {
b.addEventListener("click", () => {
if (b.disabled) return;
const val = b.querySelector(".val");
b.disabled = true;
if (val) val.textContent = "измерение…";
const base = parseInt(b.dataset.base, 10) || 45;
setTimeout(() => {
let ms = base + Math.round(Math.random() * 12 - 5);
const ourBest = Object.keys(pingMap)
.map((k) => pingMap[k])
.filter((p) => p & & p.ok)
.reduce((m, p) => Math.min(m, p.ms), Infinity);
if (isFinite(ourBest) & & ms < = ourBest + 15) ms = ourBest + 18 + Math.round(Math.random() * 10);
setPingBtnResult(b, ms);
b.disabled = false;
}, 1500 + Math.random() * 900);
});
});
subBtn.addEventListener("click", () => withBusy(async () => {
try {
await runImportSubscription();