@@ -273,8 +273,9 @@
overflow-y : auto ;
padding : 18 px 20 px 8 px ;
}
. page { display : none ; max-width : 640 px ; margin : 0 auto ; animation : rise .3 s var ( - - ease ) both ; }
. page . active { display : block ; }
/* Inactive pages must not paint (WebView2 overflow ghosts from hidden siblings). */
. page { display : none !important ; max-width : 640 px ; margin : 0 auto ; }
. page . active { display : block !important ; animation : rise .3 s var ( - - ease ) both ; }
@ keyframes rise {
from { opacity : 0 ; transform : translateY ( 10 px ) ; }
to { opacity : 1 ; transform : none ; }
@@ -308,9 +309,12 @@
. update-actions . action { flex : 1 1 auto ; min-width : 140 px ; }
/* ==== 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 : 1 px solid var ( - - line ) ;
background : var ( - - hero - bg ) ;
@@ -318,6 +322,8 @@
margin-bottom : 14 px ;
text-align : center ;
transition : border-color .25 s , box-shadow .25 s , background .25 s ;
min-height : 220 px ;
box-sizing : border-box ;
}
. hero . on {
border-color : rgba ( 13 , 138 , 102 , .35 ) ;
@@ -327,6 +333,8 @@
. power-btn {
width : 148 px ;
height : 148 px ;
min-width : 148 px ;
min-height : 148 px ;
border-radius : 50 % ;
border : 0 ;
cursor : pointer ;
@@ -341,6 +349,7 @@
transition : background .25 s , color .25 s , transform .12 s , box-shadow .25 s ;
box-shadow : 0 14 px 34 px rgba ( 8 , 145 , 178 , .3 ) ;
position : relative ;
flex-shrink : 0 ;
}
. power-btn : hover { transform : translateY ( -2 px ) ; }
. 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 = String(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 ( ) ;