Release 4.2.1+1: UI polish — опечатки Румынии→RO+флаг, чистая шапка «Серверы» без +/×, «Полезные ссылки» (сайт+Telegram), убран мусор «СПЕЦИАЛЬНЫЕ СЕРВИСИ», sticky кнопка ПОДКЛЮЧИТЬСЯ, свойства сервера заполнены, без T/V в списке, оффер «3 дня бесплатного доступа». Windows 4.2.1.1.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-08-01 23:08:28 +03:00
co-authored by Cursor
parent 2a57987aea
commit 497af0e475
16 changed files with 311 additions and 317 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build // versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
versionCode = 4_020_001 versionCode = 4_021_001
versionName = "4.2.0+1" versionName = "4.2.1+1"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
ndk { ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
+46 -46
View File
@@ -1,48 +1,48 @@
{ {
"version": "4.2.0", "version": "4.2.1",
"notes": "4.2.0: обновлённые формулировки, Kill Switch, статистика, кнопка подключения", "notes": "4.2.1: исправлены опечатки Румынии (флаг RO), чистая шапка списка серверов, полезные ссылки вместо рекомендаций, убраны мусорные подписи на Защите, большая кнопка ПОДКЛЮЧИТЬСЯ всегда видна, свойства сервера заполнены, без T/V бейджей в списке",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "64da79ddf7cd5694bf93c4c110bf0e1281a20651e1e868a960562d5c1a15bd0d", "sha256": "c3885d6d0c5b6e542fe1a172650c2f9480afde2b9016ceb1a5699f851761b837",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "64da79ddf7cd5694bf93c4c110bf0e1281a20651e1e868a960562d5c1a15bd0d", "sha256": "c3885d6d0c5b6e542fe1a172650c2f9480afde2b9016ceb1a5699f851761b837",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
"darwin-arm64": { "darwin-arm64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d", "sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin", "os": "darwin",
"arch": "arm64", "arch": "arm64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip"
}, },
"darwin-amd64": { "darwin-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3", "sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin", "os": "darwin",
"arch": "amd64", "arch": "amd64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip"
}, },
"darwin-universal": { "darwin-universal": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2", "sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin", "os": "darwin",
"arch": "universal", "arch": "universal",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip"
}, },
"android": { "android": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk",
"sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf", "sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf",
"os": "android", "os": "android",
"arch": "universal" "arch": "universal"
} }
} }
} }
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+46 -46
View File
@@ -1,48 +1,48 @@
{ {
"version": "4.2.0", "version": "4.2.1",
"notes": "4.2.0: обновлённые формулировки, Kill Switch, статистика, кнопка подключения", "notes": "4.2.1: исправлены опечатки Румынии (флаг RO), чистая шапка списка серверов, полезные ссылки вместо рекомендаций, убраны мусорные подписи на Защите, большая кнопка ПОДКЛЮЧИТЬСЯ всегда видна, свойства сервера заполнены, без T/V бейджей в списке",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "64da79ddf7cd5694bf93c4c110bf0e1281a20651e1e868a960562d5c1a15bd0d", "sha256": "c3885d6d0c5b6e542fe1a172650c2f9480afde2b9016ceb1a5699f851761b837",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "64da79ddf7cd5694bf93c4c110bf0e1281a20651e1e868a960562d5c1a15bd0d", "sha256": "c3885d6d0c5b6e542fe1a172650c2f9480afde2b9016ceb1a5699f851761b837",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
"darwin-arm64": { "darwin-arm64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d", "sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin", "os": "darwin",
"arch": "arm64", "arch": "arm64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip"
}, },
"darwin-amd64": { "darwin-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3", "sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin", "os": "darwin",
"arch": "amd64", "arch": "amd64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip"
}, },
"darwin-universal": { "darwin-universal": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2", "sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin", "os": "darwin",
"arch": "universal", "arch": "universal",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip"
}, },
"android": { "android": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk",
"sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf", "sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf",
"os": "android", "os": "android",
"arch": "universal" "arch": "universal"
} }
} }
} }
+1 -3
View File
@@ -470,13 +470,11 @@ func (a *App) AutoCheckUpdate() {
} }
// allowedLinkHosts are the only hosts the UI may open in the default browser // allowedLinkHosts are the only hosts the UI may open in the default browser
// (shop button, «Рекомендуемые сервисы» and provider cards in index.html). // (shop / «Полезные ссылки» / provider cards in index.html).
// Competitor links are plain URLs without referral codes (Store policy). // Competitor links are plain URLs without referral codes (Store policy).
var allowedLinkHosts = map[string]bool{ var allowedLinkHosts = map[string]bool{
"evilfox.win": true, "evilfox.win": true,
"t.me": true, "t.me": true,
"de4ima.uk": true,
"git.de4ima.uk": true,
"nordvpn.com": true, "nordvpn.com": true,
"expressvpn.com": true, "expressvpn.com": true,
"surfshark.com": true, "surfshark.com": true,
+153 -165
View File
@@ -273,8 +273,9 @@
overflow-y: auto; overflow-y: auto;
padding: 18px 20px 8px; padding: 18px 20px 8px;
} }
.page { display: none; max-width: 640px; margin: 0 auto; animation: rise .3s var(--ease) both; } /* Inactive pages must not paint (WebView2 overflow ghosts from hidden siblings). */
.page.active { display: block; } .page { display: none !important; max-width: 640px; margin: 0 auto; }
.page.active { display: block !important; animation: rise .3s var(--ease) both; }
@keyframes rise { @keyframes rise {
from { opacity: 0; transform: translateY(10px); } from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: none; } to { opacity: 1; transform: none; }
@@ -308,9 +309,12 @@
.update-actions .action { flex: 1 1 auto; min-width: 140px; } .update-actions .action { flex: 1 1 auto; min-width: 140px; }
/* ==== Home / power ==== */ /* ==== Home / power ==== */
/* Sticky so the big connect control never scrolls under the provider list. */
.hero { .hero {
position: relative; position: sticky;
overflow: hidden; top: 0;
z-index: 6;
overflow: visible;
border-radius: var(--radius); border-radius: var(--radius);
border: 1px solid var(--line); border: 1px solid var(--line);
background: var(--hero-bg); background: var(--hero-bg);
@@ -318,6 +322,8 @@
margin-bottom: 14px; margin-bottom: 14px;
text-align: center; text-align: center;
transition: border-color .25s, box-shadow .25s, background .25s; transition: border-color .25s, box-shadow .25s, background .25s;
min-height: 220px;
box-sizing: border-box;
} }
.hero.on { .hero.on {
border-color: rgba(13,138,102,.35); border-color: rgba(13,138,102,.35);
@@ -327,6 +333,8 @@
.power-btn { .power-btn {
width: 148px; width: 148px;
height: 148px; height: 148px;
min-width: 148px;
min-height: 148px;
border-radius: 50%; border-radius: 50%;
border: 0; border: 0;
cursor: pointer; cursor: pointer;
@@ -341,6 +349,7 @@
transition: background .25s, color .25s, transform .12s, box-shadow .25s; transition: background .25s, color .25s, transform .12s, box-shadow .25s;
box-shadow: 0 14px 34px rgba(8, 145, 178, .3); box-shadow: 0 14px 34px rgba(8, 145, 178, .3);
position: relative; position: relative;
flex-shrink: 0;
} }
.power-btn:hover { transform: translateY(-2px); } .power-btn:hover { transform: translateY(-2px); }
.power-btn:active { transform: none; } .power-btn:active { transform: none; }
@@ -1336,7 +1345,7 @@
<div class="brand-row"> <div class="brand-row">
<div class="logo" aria-hidden="true"><span class="fox">🦊</span></div> <div class="logo" aria-hidden="true"><span class="fox">🦊</span></div>
<h1 class="brand">EvilFox</h1> <h1 class="brand">EvilFox</h1>
<span class="badge-ver" id="badgeVer">4.2.0</span> <span class="badge-ver" id="badgeVer" hidden></span>
</div> </div>
<button class="nav-btn active" type="button" data-page="guard"> <button class="nav-btn active" type="button" data-page="guard">
@@ -1362,7 +1371,7 @@
<div class="spacer"></div> <div class="spacer"></div>
<div class="side-foot"> <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="Переключить тему"> <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"> <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"/> <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="page active" data-page="guard">
<section class="hero" id="hero"> <section class="hero" id="hero">
<button class="power-btn" id="toggleBtn" type="button" aria-label="Подключить / отключить"> <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"> <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="M12 3v8"/>
<path d="M6.3 6.6a8 8 0 1 0 11.4 0"/> <path d="M6.3 6.6a8 8 0 1 0 11.4 0"/>
</svg> </svg>
@@ -1404,7 +1413,7 @@
<div class="chips-line"> <div class="chips-line">
<span class="mode-chip" id="vpnChip" hidden>VPN · весь трафик</span> <span class="mode-chip" id="vpnChip" hidden>VPN · весь трафик</span>
</div> </div>
<p class="hero-hint" id="heroHint">Нажмите, чтобы включить защиту</p> <p class="hero-hint" id="heroHint">Нажмите большую кнопку, чтобы включить защиту</p>
</section> </section>
<button class="cur-server" id="curServer" type="button" title="Открыть список серверов"> <button class="cur-server" id="curServer" type="button" title="Открыть список серверов">
@@ -1428,37 +1437,33 @@
<div class="traffic-sub" id="trafficExp" hidden></div> <div class="traffic-sub" id="trafficExp" hidden></div>
</section> </section>
<div class="section"> <div class="section" id="provSection">
<div class="section-title"><span>Выберите провайдера</span></div> <div class="section-title"><span>Провайдеры</span></div>
<div class="prov-grid" id="provGrid"> <div class="prov-grid" id="provGrid">
<div class="prov-card ours"> <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-name">🦊 EvilFox VPN</div>
<div class="prov-sub">Оптимальные серверы рядом с вами · защита без настройки</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="action primary" id="provConnectBtn" type="button">🔥 Подключиться</button>
<button class="prov-ping" id="provPingOurs" type="button">📊 <span class="val">Проверить скорость</span></button>
</div> </div>
<div class="prov-card"> <div class="prov-card">
<span class="prov-tag">Сторонний сервис</span> <span class="prov-tag">Сторонний</span>
<div class="prov-name">NordVPN</div> <div class="prov-name">NordVPN</div>
<div class="prov-sub">Оплата и настройка на сайте провайдера</div> <div class="prov-sub">Оплата и настройка на сайте провайдера</div>
<button class="prov-go" type="button" data-url="https://nordvpn.com/" data-name="NordVPN">Перейти на сайт</button> <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>
<div class="prov-card"> <div class="prov-card">
<span class="prov-tag">Сторонний сервис</span> <span class="prov-tag">Сторонний</span>
<div class="prov-name">ExpressVPN</div> <div class="prov-name">ExpressVPN</div>
<div class="prov-sub">Оплата и настройка на сайте провайдера</div> <div class="prov-sub">Оплата и настройка на сайте провайдера</div>
<button class="prov-go" type="button" data-url="https://expressvpn.com/" data-name="ExpressVPN">Перейти на сайт</button> <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>
<div class="prov-card"> <div class="prov-card">
<span class="prov-tag">Сторонний сервис</span> <span class="prov-tag">Сторонний</span>
<div class="prov-name">Surfshark</div> <div class="prov-name">Surfshark</div>
<div class="prov-sub">Оплата и настройка на сайте провайдера</div> <div class="prov-sub">Оплата и настройка на сайте провайдера</div>
<button class="prov-go" type="button" data-url="https://surfshark.com/" data-name="Surfshark">Перейти на сайт</button> <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> </div>
</div> </div>
@@ -1478,12 +1483,10 @@
</section> </section>
<div class="server-toolbar"> <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"> <div class="server-actions">
<button class="mini" id="pingBtn" type="button">Пинг</button> <button class="mini" id="pingBtn" type="button">Пинг</button>
<button class="mini accent" id="bestBtn" type="button">Лучший</button> <button class="mini accent" id="bestBtn" type="button" title="Выбрать сервер с наименьшим пингом">Выбрать лучший</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>
</div> </div>
</div> </div>
<div class="server-list" id="serverList"></div> <div class="server-list" id="serverList"></div>
@@ -1507,18 +1510,16 @@
<details class="profile-edit" id="profileEdit"> <details class="profile-edit" id="profileEdit">
<summary>Свойства сервера</summary> <summary>Свойства сервера</summary>
<div class="stack" style="margin-top:8px"> <div class="stack" style="margin-top:8px" id="profProps">
<p class="host-line">Адрес сервера: <span id="profHost"></span></p> <div class="subinfo-row"><span class="k">Сервер</span><span class="v" id="propName"></span></div>
<div> <div class="subinfo-row"><span class="k">Пинг</span><span class="v" id="propPing"></span></div>
<label class="field" for="name">Название</label> <div class="subinfo-row"><span class="k">Протокол</span><span class="v" id="propProto"></span></div>
<input id="name" type="text" placeholder="Например: DE-1 / Home" spellcheck="false" /> <div class="subinfo-row"><span class="k">Статус</span><span class="v" id="propStatus"></span></div>
</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> </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> </details>
</section> </section>
@@ -1641,11 +1642,11 @@
<details class="panel" id="advBox"> <details class="panel" id="advBox">
<summary>Расширенные настройки</summary> <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> <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">HTTP: <span id="advHttp"></span></p>
<p class="adv-line">SOCKS: <span id="advSocks"></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> </div>
</details> </details>
</section> </section>
@@ -1707,7 +1708,7 @@
</details> </details>
<details class="panel"> <details class="panel">
<summary>Не подключается — что делать?</summary> <summary>Не подключается — что делать?</summary>
<p class="remna-hint" style="margin-top:6px">Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Лучший»). Подробности — в журнале на вкладке «Статистика».</p> <p class="remna-hint" style="margin-top:6px">Проверьте, что конфигурация добавлена и срок её действия не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Выбрать лучший»). Подробности — в журнале на вкладке «Статистика».</p>
</details> </details>
<div class="about-card"> <div class="about-card">
@@ -1725,10 +1726,9 @@
<button class="action primary" id="shopBtn" type="button">Перейти на сайт</button> <button class="action primary" id="shopBtn" type="button">Перейти на сайт</button>
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button> <button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
</section> </section>
<!-- Рекомендуемые сервисы: ненавязчивый блок в нижнем углу «О программе». <!-- Полезные ссылки (сайт + поддержка). Хосты — в allowlist openURL. -->
Ссылки задаются в скрипте — константа RECOMMENDED_SERVICES. --> <section class="svc" aria-label="Полезные ссылки">
<section class="svc" aria-label="Рекомендуемые сервисы"> <h4>🌐 Полезные ссылки</h4>
<h4>Рекомендуемые сервисы</h4>
<div class="svc-grid" id="svcGrid"></div> <div class="svc-grid" id="svcGrid"></div>
</section> </section>
<button class="shop-link" id="quitBtn" type="button" hidden style="display:none;margin:12px auto 0;width:auto">Выйти</button> <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 btn = $("toggleBtn");
const coreBtn = $("coreBtn"); const coreBtn = $("coreBtn");
const saveBtn = $("saveBtn"); const saveBtn = $("saveBtn");
const addBtn = $("addBtn"); const addBtn = $("addBtn"); // optional — header + removed
const delBtn = $("delBtn"); const delBtn = $("delBtn"); // optional — header × removed
const proxy = $("proxy"); const proxy = $("proxy");
const nameInput = $("name"); const nameInput = $("name");
const sysproxy = $("sysproxy"); const sysproxy = $("sysproxy");
@@ -1843,10 +1843,30 @@
PT: "Португалия", RO: "Румыния", RS: "Сербия", RU: "Россия", SE: "Швеция", PT: "Португалия", RO: "Румыния", RS: "Сербия", RU: "Россия", SE: "Швеция",
SG: "Сингапур", SK: "Словакия", TR: "Турция", UA: "Украина", US: "США", ZA: "ЮАР" 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) { function countryRu(cc) {
const c = String(cc || "").toUpperCase(); const c = String(cc || "").toUpperCase();
return COUNTRY_RU[c] || ""; 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-флаги ==== // ==== SVG-флаги ====
// Windows не умеет эмодзи-флаги: Segoe UI Emoji рисует пары региональных // Windows не умеет эмодзи-флаги: Segoe UI Emoji рисует пары региональных
// индикаторов буквами («BE», «LV»), поэтому рисуем настоящие SVG-флаги. // индикаторов буквами («BE», «LV»), поэтому рисуем настоящие SVG-флаги.
@@ -1976,7 +1996,7 @@
if (d.e) b += d.e; 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>'; 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) { function profileISO(p) {
if (!p) return ""; if (!p) return "";
const c = String(p.country || "").toUpperCase(); const c = String(p.country || "").toUpperCase();
@@ -1986,7 +2006,7 @@
const a = rs[0].codePointAt(0) - 0x1f1e6, b = rs[1].codePointAt(0) - 0x1f1e6; 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); 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 для известной страны, буквенный код — фолбэк. // Заполнить ячейку флага: SVG для известной страны, буквенный код — фолбэк.
function setFlagEl(el, p) { function setFlagEl(el, p) {
@@ -2084,15 +2104,12 @@
const SHOP_URL = "https://evilfox.win/"; const SHOP_URL = "https://evilfox.win/";
// Текст оффера — правьте здесь (показывается на карточке EvilFox на вкладке «Защита»). // Текст оффера — правьте здесь (показывается на карточке EvilFox на вкладке «Защита»).
// Правила Store: никаких цен и упоминаний оплаты внутри приложения. // Правила Store: никаких цен и упоминаний оплаты внутри приложения.
const OFFER_TEXT = "🚀 3 дня бесплатно"; const OFFER_TEXT = "🎁 3 дня бесплатного доступа";
// Список рекомендуемых сервисов — замените ссылки/названия здесь. // Полезные ссылки (Помощь). Хост каждой ссылки — в allowlist openURL
// Хост каждой ссылки должен быть разрешён в allowlist openURL // (internal/apphost/app.go → allowedLinkHosts).
// (internal/apphost/app.go → allowedLinkHosts), иначе ссылка не откроется.
const RECOMMENDED_SERVICES = [ const RECOMMENDED_SERVICES = [
{ icon: "🛒", name: "EvilFox Shop", url: "https://evilfox.win/" }, { icon: "🌐", name: "Сайт", url: "https://evilfox.win/" },
{ icon: "✈️", name: "Telegram", url: "https://t.me/evilfox" }, { 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" }
]; ];
const AUTO_BEST_KEY = "navis.autoBest"; const AUTO_BEST_KEY = "navis.autoBest";
// Оффер на карточке EvilFox рендерим из константы, чтобы правка была в одном месте. // Оффер на карточке EvilFox рендерим из константы, чтобы правка была в одном месте.
@@ -2268,7 +2285,7 @@
async function persistHy2IfNeeded(protocol) { async function persistHy2IfNeeded(protocol) {
const p = (protocol || "").toLowerCase(); 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://")) { if (p === "hysteria2" || link.startsWith("hysteria2://") || link.startsWith("hy2://")) {
await saveHy2(readHy2()); await saveHy2(readHy2());
} }
@@ -2334,12 +2351,13 @@
// Человекочитаемые имена серверов («Чехия — оптимальный сервер», «Германия — сервер 2»). // Человекочитаемые имена серверов («Чехия — оптимальный сервер», «Германия — сервер 2»).
// Реальный host:port показываем только в свойствах сервера и в журнале. // Реальный host:port показываем только в свойствах сервера и в журнале.
// ISO всегда через profileISO (бэкенд + JS-алиасы опечаток) → countryRu, никогда сырое имя панели.
let friendlyMap = {}; let friendlyMap = {};
function rebuildFriendlyNames() { function rebuildFriendlyNames() {
friendlyMap = {}; friendlyMap = {};
const groups = {}; const groups = {};
profiles.forEach((p) => { profiles.forEach((p) => {
const key = String(p.country || "").toUpperCase() || "__none__"; const key = profileISO(p) || "__none__";
(groups[key] = groups[key] || []).push(p); (groups[key] = groups[key] || []).push(p);
}); });
Object.keys(groups).forEach((key) => { Object.keys(groups).forEach((key) => {
@@ -2360,13 +2378,19 @@
} else { } else {
title = region ? (region + " — сервер " + n) : ("Сервер " + n); 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) { function friendlyTitle(name) {
const f = friendlyMap[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) { function fillProfiles(list, active) {
@@ -2481,10 +2505,7 @@
const right = document.createElement("div"); const right = document.createElement("div");
right.className = "right"; right.className = "right";
// Протокол — крошечная буква-иконка с подсказкой; строчный «vless» // Протокол — только на карточке активного сервера / в «Свойствах», не в строках списка.
// под каждой строкой не повторяем.
const protoBadge = protoLetterBadge(p.protocol);
if (protoBadge) right.appendChild(protoBadge);
const ms = document.createElement("span"); const ms = document.createElement("span");
const pr = pingMap[p.name]; const pr = pingMap[p.name];
if (pr && pr.ok) { if (pr && pr.ok) {
@@ -2500,12 +2521,12 @@
ms.title = "пинг не измерен"; ms.title = "пинг не измерен";
} }
right.appendChild(ms); right.appendChild(ms);
// [📊] — проверить скорость только этого сервера. // [📊] — проверить пинг только этого сервера.
const pingOne = document.createElement("button"); const pingOne = document.createElement("button");
pingOne.type = "button"; pingOne.type = "button";
pingOne.className = "row-ping"; pingOne.className = "row-ping";
pingOne.textContent = "📊"; pingOne.textContent = "📊";
pingOne.title = "Проверить скорость этого сервера"; pingOne.title = "Проверить пинг этого сервера";
pingOne.addEventListener("click", (e) => { pingOne.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
runPingOne(p.name, pingOne, ms); 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) { async function runPingOne(name, btnEl, msEl) {
if (btnEl.disabled) return; if (btnEl.disabled) return;
@@ -2689,11 +2696,17 @@
const daysLeft = Math.floor((d.getTime() - Date.now()) / 86400000); const daysLeft = Math.floor((d.getTime() - Date.now()) / 86400000);
const v = $("subExpireVal"); const v = $("subExpireVal");
if (daysLeft < 0) { if (daysLeft < 0) {
v.textContent = text + " · срок истёк"; v.textContent = text + " · Истёк";
v.classList.add("warn"); v.classList.add("warn");
} else { } else if (daysLeft === 0) {
v.textContent = daysLeft <= 14 ? (text + " · осталось " + daysLeft + " дн.") : text; v.textContent = text + " · Сегодня";
v.classList.add("warn");
} else if (daysLeft <= 14) {
v.textContent = text + " · осталось " + daysLeft + " дн.";
v.classList.toggle("warn", daysLeft <= 3); v.classList.toggle("warn", daysLeft <= 3);
} else {
v.textContent = text;
v.classList.remove("warn");
} }
} }
@@ -2713,12 +2726,12 @@
} }
function renderUpdate(u, version) { function renderUpdate(u, version) {
const label = "EvilFox v" + (version || "?"); // Версия — только в «Помощь»; в сайдбаре бренд без номера.
verLabel.textContent = label; if (verLabel) verLabel.textContent = "EvilFox";
if (aboutVer) aboutVer.textContent = "Версия " + (version || "?"); if (aboutVer) aboutVer.textContent = "Версия " + (version || "?");
if (badgeVer && version) { if (badgeVer) {
const short = String(version).split("+")[0]; badgeVer.hidden = true;
badgeVer.textContent = short || version; badgeVer.textContent = "";
} }
if (storePackaged || (u && u.store_managed)) { if (storePackaged || (u && u.store_managed)) {
updateBanner.classList.remove("show"); updateBanner.classList.remove("show");
@@ -2780,14 +2793,14 @@
} }
const lock = connected || busy; const lock = connected || busy;
proxy.disabled = lock; if (proxy) proxy.disabled = lock;
nameInput.disabled = lock; if (nameInput) nameInput.disabled = lock;
sysproxy.disabled = lock; sysproxy.disabled = lock;
profile.disabled = lock; profile.disabled = lock;
addBtn.disabled = lock; if (addBtn) addBtn.disabled = lock;
delBtn.disabled = lock || ((state.profiles || profiles).length <= 1); if (delBtn) delBtn.disabled = lock || ((state.profiles || profiles).length <= 1);
saveBtn.disabled = lock; if (saveBtn) saveBtn.disabled = lock;
coreBtn.disabled = busy; if (coreBtn) coreBtn.disabled = busy;
pingBtn.disabled = busy; pingBtn.disabled = busy;
bestBtn.disabled = busy; bestBtn.disabled = busy;
updCheckBtn.disabled = busy; updCheckBtn.disabled = busy;
@@ -2808,8 +2821,8 @@
if (syncForm || (!formHydrated && !dirty)) { if (syncForm || (!formHydrated && !dirty)) {
const active = (state.profiles || []).find((p) => p.name === profile.value) || {}; const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
nameInput.value = active.name || state.active_profile || ""; if (nameInput) nameInput.value = active.name || state.active_profile || "";
proxy.value = typeof state.proxy === "string" ? state.proxy : (active.proxy || ""); if (proxy) proxy.value = typeof state.proxy === "string" ? state.proxy : (active.proxy || "");
if (typeof state.system_proxy === "boolean") sysproxy.checked = state.system_proxy; if (typeof state.system_proxy === "boolean") sysproxy.checked = state.system_proxy;
fillHy2(state.hy2); fillHy2(state.hy2);
formHydrated = true; formHydrated = true;
@@ -2826,10 +2839,13 @@
const hy2 = (label === "hysteria2" || label === "hy2"); const hy2 = (label === "hysteria2" || label === "hy2");
if ($("hy2Box")) $("hy2Box").style.display = hy2 ? "" : "none"; if ($("hy2Box")) $("hy2Box").style.display = hy2 ? "" : "none";
// Технические адреса прокси — только в «Настройки → Расширенные» и на «Статистике». // Параметры прокси — только когда подключены (без пустого «появится после…»).
const advHttp = $("advHttp"), advSocks = $("advSocks"), logsProxyLine = $("logsProxyLine"); const advHttp = $("advHttp"), advSocks = $("advSocks"), logsProxyLine = $("logsProxyLine");
if (advHttp) advHttp.textContent = state.http_proxy || "появится после подключения"; const proxyParamsBlock = $("proxyParamsBlock");
if (advSocks) advSocks.textContent = state.socks_proxy || "появится после подключения"; 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) { if (logsProxyLine) {
const parts = []; const parts = [];
if (state.http_proxy) parts.push("HTTP " + state.http_proxy); if (state.http_proxy) parts.push("HTTP " + state.http_proxy);
@@ -2838,7 +2854,7 @@
logsProxyLine.textContent = parts.length ? ("Прокси: " + parts.join(" · ")) : ""; logsProxyLine.textContent = parts.length ? ("Прокси: " + parts.join(" · ")) : "";
} }
renderProvConnect(); renderProvConnect();
renderProfHost(); renderProfProps();
// Нижняя полоса статуса — только для важных сообщений; постоянного // Нижняя полоса статуса — только для важных сообщений; постоянного
// «Готово к подключению» больше нет (статус виден на большой кнопке). // «Готово к подключению» больше нет (статус виден на большой кнопке).
@@ -2878,12 +2894,30 @@
} }
} }
// Реальный host:port выбранного сервера — в «Свойствах сервера» (муторные детали). // Свойства выбранного сервера: имя / пинг / протокол / статус.
function renderProfHost() { function renderProfProps() {
const el = $("profHost");
if (!el) return;
const p = profiles.find((x) => x.name === profile.value); 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) { async function refresh(opts) {
@@ -2949,23 +2983,31 @@
await runBest(true); await runBest(true);
} else { } else {
setMeta("Подключение…"); setMeta("Подключение…");
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked); // Профиль уже выбран; скрытые поля name/proxy гидратируются из state.
await persistHy2IfNeeded(); // Не перезаписываем 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(); await connect();
setMeta("Защищённое соединение установлено", "ok"); setMeta("Защищённое соединение установлено", "ok");
} }
} catch (e) { setMeta(String(e), "err"); } } catch (e) { setMeta(String(e), "err"); }
})); }));
saveBtn.addEventListener("click", () => withBusy(async () => { if (saveBtn) saveBtn.addEventListener("click", () => withBusy(async () => {
try { 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(); await persistHy2IfNeeded();
setMeta("Профиль сохранён", "ok"); setMeta("Профиль сохранён", "ok");
} catch (e) { setMeta(String(e), "err"); } } catch (e) { setMeta(String(e), "err"); }
})); }));
coreBtn.addEventListener("click", () => withBusy(async () => { if (coreBtn) coreBtn.addEventListener("click", () => withBusy(async () => {
try { try {
setMeta("Скачивание official cores…"); setMeta("Скачивание official cores…");
const path = await installCore(); const path = await installCore();
@@ -2973,7 +3015,7 @@
} catch (e) { setMeta(String(e), "err"); } } catch (e) { setMeta(String(e), "err"); }
})); }));
delBtn.addEventListener("click", () => withBusy(async () => { if (delBtn) delBtn.addEventListener("click", () => withBusy(async () => {
try { try {
if (!confirm("Удалить сервер «" + friendlyTitle(profile.value) + "»?")) return; if (!confirm("Удалить сервер «" + friendlyTitle(profile.value) + "»?")) return;
await deleteProfile(profile.value); await deleteProfile(profile.value);
@@ -2983,7 +3025,7 @@
} catch (e) { setMeta(String(e), "err"); } } catch (e) { setMeta(String(e), "err"); }
})); }));
addBtn.addEventListener("click", () => { if (addBtn) addBtn.addEventListener("click", () => {
$("newName").value = ""; $("newName").value = "";
$("newProxy").value = ""; $("newProxy").value = "";
modal.classList.add("open"); modal.classList.add("open");
@@ -3022,7 +3064,7 @@
shopBtn.addEventListener("click", openShop); shopBtn.addEventListener("click", openShop);
shopLink.addEventListener("click", openShop); shopLink.addEventListener("click", openShop);
// «Рекомендуемые сервисы» — рендер из RECOMMENDED_SERVICES (см. константу выше). // «Рекомендуемые сервисы» → «Полезные ссылки» (см. RECOMMENDED_SERVICES).
(function renderRecommendedServices() { (function renderRecommendedServices() {
const grid = $("svcGrid"); const grid = $("svcGrid");
if (!grid) return; if (!grid) return;
@@ -3097,60 +3139,6 @@
if (!connected && !busy) btn.click(); 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 () => { subBtn.addEventListener("click", () => withBusy(async () => {
try { try {
await runImportSubscription(); await runImportSubscription();
+3 -1
View File
@@ -141,7 +141,9 @@ var countryNames = []nameCode{
{"сша", "US"}, {"америка", "US"}, {"сша", "US"}, {"америка", "US"},
// Частые опечатки в именах серверов на панелях (наблюдались в проде). // Частые опечатки в именах серверов на панелях (наблюдались в проде).
{"румьиня", "RO"}, {"румыня", "RO"}, {"руминия", "RO"}, // Longer stems first (Contains): «румьиния» before «румьиня».
{"румьиния", "RO"}, {"румяння", "RO"}, {"румьиня", "RO"},
{"румыня", "RO"}, {"руминия", "RO"}, {"румыниа", "RO"},
// City → country hints (EN + RU). // City → country hints (EN + RU).
{"frankfurt", "DE"}, {"amsterdam", "NL"}, {"london", "GB"}, {"frankfurt", "DE"}, {"amsterdam", "NL"}, {"london", "GB"},
+6
View File
@@ -28,6 +28,12 @@ func TestFromText(t *testing.T) {
// Panel-side typos seen in production. // Panel-side typos seen in production.
{"Румьиня", "RO"}, {"Румьиня", "RO"},
{"Румыня-2", "RO"}, {"Румыня-2", "RO"},
{"Румяння", "RO"},
{"Румяння — оптимальный", "RO"},
{"Румьиния", "RO"},
{"Румьиния Fast", "RO"},
{"румыниа", "RO"},
{"Румыния", "RO"},
} }
for _, c := range cases { for _, c := range cases {
cc, emoji := FromText(c.in) cc, emoji := FromText(c.in)
+1 -1
View File
@@ -18,7 +18,7 @@ import (
// CurrentVersion is the product/semver used for update eligibility (feed "version"). // CurrentVersion is the product/semver used for update eligibility (feed "version").
// Keep major.minor.patch only — no build suffix here. // Keep major.minor.patch only — no build suffix here.
const CurrentVersion = "4.2.0" const CurrentVersion = "4.2.1"
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part, // BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build. // macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
+1 -1
View File
@@ -33,7 +33,7 @@
<Identity <Identity
Name="EvilFox.EvilFox" Name="EvilFox.EvilFox"
Publisher="CN=EvilFox" Publisher="CN=EvilFox"
Version="4.2.0.1" Version="4.2.1.1"
ProcessorArchitecture="x64" /> ProcessorArchitecture="x64" />
<Properties> <Properties>
+2 -2
View File
@@ -15,7 +15,7 @@
param( param(
[string]$ExePath = "", [string]$ExePath = "",
[string]$OutDir = "", [string]$OutDir = "",
[string]$Version = "4.2.0.1", [string]$Version = "4.2.1.1",
[string]$Name = "EvilFox.EvilFox", [string]$Name = "EvilFox.EvilFox",
[string]$Publisher = "CN=EvilFox", [string]$Publisher = "CN=EvilFox",
[string]$DisplayName = "EvilFox", [string]$DisplayName = "EvilFox",
@@ -82,7 +82,7 @@ $manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8
# Prefer exact placeholder swaps so we never touch <?xml version=...?>. # Prefer exact placeholder swaps so we never touch <?xml version=...?>.
$manifestText = $manifestText.Replace('Name="EvilFox.EvilFox"', "Name=`"$Name`"") $manifestText = $manifestText.Replace('Name="EvilFox.EvilFox"', "Name=`"$Name`"")
$manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"") $manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"")
$manifestText = $manifestText.Replace('Version="4.2.0.1"', "Version=`"$Version`"") $manifestText = $manifestText.Replace('Version="4.2.1.1"', "Version=`"$Version`"")
$manifestText = $manifestText.Replace('<DisplayName>EvilFox</DisplayName>', "<DisplayName>$DisplayName</DisplayName>") $manifestText = $manifestText.Replace('<DisplayName>EvilFox</DisplayName>', "<DisplayName>$DisplayName</DisplayName>")
$manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>") $manifestText = $manifestText.Replace('<PublisherDisplayName>EvilFox</PublisherDisplayName>', "<PublisherDisplayName>$PublisherDisplayName</PublisherDisplayName>")
$manifestText = $manifestText.Replace('DisplayName="EvilFox"', "DisplayName=`"$DisplayName`"") $manifestText = $manifestText.Replace('DisplayName="EvilFox"', "DisplayName=`"$DisplayName`"")
+46 -46
View File
@@ -1,48 +1,48 @@
{ {
"version": "4.2.0", "version": "4.2.1",
"notes": "4.2.0: обновлённые формулировки, Kill Switch, статистика, кнопка подключения", "notes": "4.2.1: исправлены опечатки Румынии (флаг RO), чистая шапка списка серверов, полезные ссылки вместо рекомендаций, убраны мусорные подписи на Защите, большая кнопка ПОДКЛЮЧИТЬСЯ всегда видна, свойства сервера заполнены, без T/V бейджей в списке",
"platform": "windows-amd64", "platform": "windows-amd64",
"os": "windows", "os": "windows",
"arch": "amd64", "arch": "amd64",
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "64da79ddf7cd5694bf93c4c110bf0e1281a20651e1e868a960562d5c1a15bd0d", "sha256": "c3885d6d0c5b6e542fe1a172650c2f9480afde2b9016ceb1a5699f851761b837",
"mandatory": false, "mandatory": false,
"platforms": { "platforms": {
"windows-amd64": { "windows-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/windows/EvilFox.exe",
"sha256": "64da79ddf7cd5694bf93c4c110bf0e1281a20651e1e868a960562d5c1a15bd0d", "sha256": "c3885d6d0c5b6e542fe1a172650c2f9480afde2b9016ceb1a5699f851761b837",
"os": "windows", "os": "windows",
"arch": "amd64" "arch": "amd64"
}, },
"darwin-arm64": { "darwin-arm64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis",
"sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d", "sha256": "01a40b1cce4e2fb3a38eb80f0815c295a00af11e4288f3b9e5c0b58ae336f28d",
"os": "darwin", "os": "darwin",
"arch": "arm64", "arch": "arm64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/arm64/Navis.app.zip"
}, },
"darwin-amd64": { "darwin-amd64": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis",
"sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3", "sha256": "19a066cfc7f7c3a291a220482b28e66a459d091c19c5351818c401b982bc65b3",
"os": "darwin", "os": "darwin",
"arch": "amd64", "arch": "amd64",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/amd64/Navis.app.zip"
}, },
"darwin-universal": { "darwin-universal": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis",
"sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2", "sha256": "d53dba8256fd6f7b62d1ce9bbad741af087ae44fec942bf2782b9c8f4e36cef2",
"os": "darwin", "os": "darwin",
"arch": "universal", "arch": "universal",
"dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg", "dmg_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.dmg",
"zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip" "zip_url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/macos/universal/Navis.app.zip"
}, },
"android": { "android": {
"url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk", "url": "https://git.de4ima.uk/Evilfox/navi/raw/branch/Windows/dist/navis-release/android/Navis.apk",
"sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf", "sha256": "4416770bebe576eba42811990c4e4424f6722dea3a8513e6164dd34fc975b3cf",
"os": "android", "os": "android",
"arch": "universal" "arch": "universal"
} }
} }
} }
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"FixedFileInfo": { "FixedFileInfo": {
"FileVersion": { "Major": 4, "Minor": 2, "Patch": 0, "Build": 1 }, "FileVersion": { "Major": 4, "Minor": 2, "Patch": 1, "Build": 1 },
"ProductVersion": { "Major": 4, "Minor": 2, "Patch": 0, "Build": 1 }, "ProductVersion": { "Major": 4, "Minor": 2, "Patch": 1, "Build": 1 },
"FileFlagsMask": "3f", "FileFlagsMask": "3f",
"FileFlags": "00", "FileFlags": "00",
"FileOS": "40004", "FileOS": "40004",
@@ -11,12 +11,12 @@
"StringFileInfo": { "StringFileInfo": {
"CompanyName": "EvilFox", "CompanyName": "EvilFox",
"FileDescription": "EvilFox — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)", "FileDescription": "EvilFox — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
"FileVersion": "4.2.0.1", "FileVersion": "4.2.1.1",
"InternalName": "EvilFox", "InternalName": "EvilFox",
"LegalCopyright": "Copyright (c) EvilFox", "LegalCopyright": "Copyright (c) EvilFox",
"OriginalFilename": "EvilFox.exe", "OriginalFilename": "EvilFox.exe",
"ProductName": "EvilFox", "ProductName": "EvilFox",
"ProductVersion": "4.2.0.1", "ProductVersion": "4.2.1.1",
"Comments": "Open-source VPN/proxy client. https://evilfox.win/" "Comments": "Open-source VPN/proxy client. https://evilfox.win/"
}, },
"VarFileInfo": { "VarFileInfo": {