Release 4.1.0+1: Store-compliance UI rework — tabs Защита/Мои серверы/Настройки/Статистика/Помощь; provider chooser (EvilFox highlighted + NordVPN/ExpressVPN/Surfshark, clean links); «Активировать ключ» wording; human-readable server names (host:port in server properties only); proxy ports moved to advanced settings/stats; traffic progress bar on main; account info on Мои серверы; connect spinner; ping test buttons. Windows 4.1.0.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+4
-4
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "4.0.1",
|
"version": "4.1.0",
|
||||||
"notes": "4.0.1: убрана привязка аккаунта, конфиги — через вашу подписку; добавлен блок рекомендуемых сервисов",
|
"notes": "4.1.0: новый интерфейс для Microsoft Store — вкладки Защита / Мои серверы / Статистика / Помощь, экран выбора провайдера, активация ключа, человекочитаемые имена серверов",
|
||||||
"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": "c9551072d2b6206cd9323ae4eaf2d4dd20377237e837becc785b69539ec79939",
|
"sha256": "6a6a0e80327e6aa87dd4c75b3fdb0795f0b7faf4d54481a73a99c31358cf492f",
|
||||||
"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": "c9551072d2b6206cd9323ae4eaf2d4dd20377237e837becc785b69539ec79939",
|
"sha256": "6a6a0e80327e6aa87dd4c75b3fdb0795f0b7faf4d54481a73a99c31358cf492f",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+4
-4
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "4.0.1",
|
"version": "4.1.0",
|
||||||
"notes": "4.0.1: убрана привязка аккаунта, конфиги — через вашу подписку; добавлен блок рекомендуемых сервисов",
|
"notes": "4.1.0: новый интерфейс для Microsoft Store — вкладки Защита / Мои серверы / Статистика / Помощь, экран выбора провайдера, активация ключа, человекочитаемые имена серверов",
|
||||||
"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": "c9551072d2b6206cd9323ae4eaf2d4dd20377237e837becc785b69539ec79939",
|
"sha256": "6a6a0e80327e6aa87dd4c75b3fdb0795f0b7faf4d54481a73a99c31358cf492f",
|
||||||
"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": "c9551072d2b6206cd9323ae4eaf2d4dd20377237e837becc785b69539ec79939",
|
"sha256": "6a6a0e80327e6aa87dd4c75b3fdb0795f0b7faf4d54481a73a99c31358cf492f",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -419,12 +419,16 @@ 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 + «Рекомендуемые сервисы» block in index.html).
|
// (shop button, «Рекомендуемые сервисы» and provider cards in index.html).
|
||||||
|
// 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,
|
"de4ima.uk": true,
|
||||||
"git.de4ima.uk": true,
|
"git.de4ima.uk": true,
|
||||||
|
"nordvpn.com": true,
|
||||||
|
"expressvpn.com": true,
|
||||||
|
"surfshark.com": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// LinkAllowed validates that raw is an http(s) URL pointing at one of the
|
// LinkAllowed validates that raw is an http(s) URL pointing at one of the
|
||||||
|
|||||||
+515
-75
@@ -355,6 +355,17 @@
|
|||||||
0% { transform: scale(.96); opacity: .9; }
|
0% { transform: scale(.96); opacity: .9; }
|
||||||
100% { transform: scale(1.12); opacity: 0; }
|
100% { transform: scale(1.12); opacity: 0; }
|
||||||
}
|
}
|
||||||
|
/* Состояние «подключение…» — вращающаяся дуга вокруг кнопки */
|
||||||
|
.power-btn.busy::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
border-top-color: var(--accent);
|
||||||
|
animation: spin 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
.status-line {
|
.status-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -973,6 +984,162 @@
|
|||||||
.svc-link:hover { color: var(--accent-deep); border-color: rgba(13,138,102,.45); }
|
.svc-link:hover { color: var(--accent-deep); border-color: rgba(13,138,102,.45); }
|
||||||
.svc-link .ic { font-size: .85rem; line-height: 1; }
|
.svc-link .ic { font-size: .85rem; line-height: 1; }
|
||||||
|
|
||||||
|
/* ==== Traffic progress (главный экран + Статистика) ==== */
|
||||||
|
.traffic {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--row-bg);
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.traffic-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: .84rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.traffic-row .pct { color: var(--muted); font-weight: 600; font-size: .78rem; }
|
||||||
|
.traffic-bar {
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--meta-bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.traffic-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: 0%;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, var(--accent-deep), var(--accent));
|
||||||
|
transition: width .4s var(--ease);
|
||||||
|
}
|
||||||
|
.traffic-fill.warn { background: linear-gradient(90deg, #d97706, #f59e0b); }
|
||||||
|
|
||||||
|
/* ==== Провайдеры (вкладка «Защита») ==== */
|
||||||
|
.prov-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.prov-card {
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface-2);
|
||||||
|
padding: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.prov-card .prov-name {
|
||||||
|
font-family: Outfit, sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1rem;
|
||||||
|
letter-spacing: -.02em;
|
||||||
|
}
|
||||||
|
.prov-card .prov-sub { font-size: .76rem; color: var(--muted); line-height: 1.35; flex: 1; }
|
||||||
|
.prov-card.ours {
|
||||||
|
border-color: rgba(8,145,178,.5);
|
||||||
|
background: linear-gradient(150deg, rgba(8,145,178,.16), var(--surface-2) 70%);
|
||||||
|
box-shadow: 0 12px 30px rgba(8,145,178,.16);
|
||||||
|
}
|
||||||
|
html[data-theme="dark"] .prov-card.ours {
|
||||||
|
background: linear-gradient(150deg, rgba(34,211,238,.16), var(--surface-2) 70%);
|
||||||
|
}
|
||||||
|
.prov-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -9px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: .64rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: .03em;
|
||||||
|
color: #7c5b00;
|
||||||
|
background: linear-gradient(180deg, #fde68a, #fbbf24);
|
||||||
|
border: 1px solid rgba(124, 91, 0, .25);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 3px 9px;
|
||||||
|
box-shadow: 0 4px 10px rgba(251, 191, 36, .35);
|
||||||
|
}
|
||||||
|
.prov-gift { font-size: .72rem; font-weight: 700; color: var(--accent-deep); }
|
||||||
|
html[data-theme="dark"] .prov-gift { color: var(--accent); }
|
||||||
|
.prov-card .action { padding: 10px 12px; font-size: .84rem; }
|
||||||
|
.prov-go {
|
||||||
|
appearance: none;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: .8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color .15s, border-color .15s;
|
||||||
|
}
|
||||||
|
.prov-go:hover { color: var(--ink); border-color: var(--muted); }
|
||||||
|
.prov-ping {
|
||||||
|
appearance: none;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font: inherit;
|
||||||
|
font-size: .74rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.prov-ping:hover { color: var(--accent-deep); }
|
||||||
|
.prov-ping:disabled { cursor: default; color: var(--muted); }
|
||||||
|
.prov-ping .val { font-variant-numeric: tabular-nums; }
|
||||||
|
.prov-ping.good .val { color: var(--ms-good); }
|
||||||
|
.prov-ping.mid .val { color: var(--ms-mid); }
|
||||||
|
@media (max-width: 560px) { .prov-grid { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
/* ==== Активация ключа ==== */
|
||||||
|
.key-box {
|
||||||
|
border: 1px solid var(--shop-border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--shop-bg);
|
||||||
|
padding: 13px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.key-box h3 { font-family: Outfit, sans-serif; font-size: .95rem; letter-spacing: -.02em; margin: 0; }
|
||||||
|
.key-hint { margin: 0; font-size: .76rem; color: var(--muted); line-height: 1.4; }
|
||||||
|
.key-link {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: .76rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.key-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Технические строки (параметры прокси, адрес сервера) */
|
||||||
|
.adv-line {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: .74rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
.host-line {
|
||||||
|
margin: 6px 0 2px;
|
||||||
|
font-size: .72rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.about-card {
|
.about-card {
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
@@ -1031,28 +1198,28 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="brand">EvilFox</h1>
|
<h1 class="brand">EvilFox</h1>
|
||||||
<span class="badge-ver" id="badgeVer">4.0.0</span>
|
<span class="badge-ver" id="badgeVer">4.1.0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="nav-btn active" type="button" data-page="home">
|
<button class="nav-btn active" type="button" data-page="guard">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11.5 12 4l9 7.5"/><path d="M5.5 10v9.5h13V10"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7.5 3v5.2c0 4.6-3.1 8-7.5 9.8-4.4-1.8-7.5-5.2-7.5-9.8V6z"/><path d="M9 12l2.2 2.2L15.4 10"/></svg>
|
||||||
<span class="nav-label">Главная</span>
|
<span class="nav-label">Защита</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" type="button" data-page="profiles">
|
<button class="nav-btn" type="button" data-page="profiles">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="6.5" rx="2"/><rect x="3.5" y="13.5" width="17" height="6.5" rx="2"/><path d="M7 7.2h.01M7 16.7h.01"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4" width="17" height="6.5" rx="2"/><rect x="3.5" y="13.5" width="17" height="6.5" rx="2"/><path d="M7 7.2h.01M7 16.7h.01"/></svg>
|
||||||
<span class="nav-label">Профили</span>
|
<span class="nav-label">Мои серверы</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" type="button" data-page="settings">
|
<button class="nav-btn" type="button" data-page="settings">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 13.5a7.6 7.6 0 0 0 0-3l2-1.5-2-3.4-2.3 1a7.7 7.7 0 0 0-2.6-1.5L14.2 2h-4l-.3 2.6a7.7 7.7 0 0 0-2.6 1.5l-2.3-1-2 3.4 2 1.5a7.6 7.6 0 0 0 0 3l-2 1.5 2 3.4 2.3-1a7.7 7.7 0 0 0 2.6 1.5l.3 2.6h4l.3-2.6a7.7 7.7 0 0 0 2.6-1.5l2.3 1 2-3.4z"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 13.5a7.6 7.6 0 0 0 0-3l2-1.5-2-3.4-2.3 1a7.7 7.7 0 0 0-2.6-1.5L14.2 2h-4l-.3 2.6a7.7 7.7 0 0 0-2.6 1.5l-2.3-1-2 3.4 2 1.5a7.6 7.6 0 0 0 0 3l-2 1.5 2 3.4 2.3-1a7.7 7.7 0 0 0 2.6 1.5l.3 2.6h4l.3-2.6a7.7 7.7 0 0 0 2.6-1.5l2.3 1 2-3.4z"/></svg>
|
||||||
<span class="nav-label">Настройки</span>
|
<span class="nav-label">Настройки</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" type="button" data-page="logs">
|
<button class="nav-btn" type="button" data-page="logs">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3.5h9l3.5 3.5v13.5h-12.5z"/><path d="M9 10h6M9 13.5h6M9 17h4"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20V10M12 20V4M19 20v-7"/></svg>
|
||||||
<span class="nav-label">Логи</span>
|
<span class="nav-label">Статистика</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-btn" type="button" data-page="about">
|
<button class="nav-btn" type="button" data-page="about">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 10.8v5.4M12 7.6h.01"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.6 9.2a2.5 2.5 0 1 1 3.5 2.3c-.8.4-1.1 1-1.1 1.8"/><path d="M12 16.6h.01"/></svg>
|
||||||
<span class="nav-label">О программе</span>
|
<span class="nav-label">Помощь</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
@@ -1082,8 +1249,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pages">
|
<div class="pages">
|
||||||
<!-- ============ Главная ============ -->
|
<!-- ============ Защита ============ -->
|
||||||
<section class="page active" data-page="home">
|
<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">
|
||||||
@@ -1099,14 +1266,14 @@
|
|||||||
<span class="proto-chip" id="protoChip">протокол</span>
|
<span class="proto-chip" id="protoChip">протокол</span>
|
||||||
<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="Открыть список серверов">
|
||||||
<span class="flag" id="curFlag">·</span>
|
<span class="flag" id="curFlag">·</span>
|
||||||
<span>
|
<span>
|
||||||
<span class="name" id="curName">Нет сервера</span>
|
<span class="name" id="curName">Нет сервера</span>
|
||||||
<span class="sub" id="curHost">добавьте профиль или подписку</span>
|
<span class="sub" id="curHost">активируйте ключ на вкладке «Мои серверы»</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
<span class="proto" id="curProto">—</span>
|
<span class="proto" id="curProto">—</span>
|
||||||
@@ -1114,26 +1281,59 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<section class="subinfo" id="subInfoCard" hidden>
|
<section class="traffic" id="trafficCard" hidden>
|
||||||
<div class="subinfo-title">Подписка</div>
|
<div class="traffic-row">
|
||||||
<div class="subinfo-row" id="subExpireRow" hidden>
|
<span>📊 <span id="trafficLabel">…</span></span>
|
||||||
<span class="k">Окончание подписки</span>
|
<span class="pct" id="trafficPct"></span>
|
||||||
<span class="v" id="subExpireVal"></span>
|
|
||||||
</div>
|
|
||||||
<div class="subinfo-row" id="subTrafficRow" hidden>
|
|
||||||
<span class="k">Трафик</span>
|
|
||||||
<span class="v" id="subTrafficVal"></span>
|
|
||||||
</div>
|
|
||||||
<div class="subinfo-row" id="subDevicesRow" hidden>
|
|
||||||
<span class="k">Устройства</span>
|
|
||||||
<span class="v" id="subDevicesVal"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="traffic-bar"><div class="traffic-fill" id="trafficFill"></div></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title"><span>Выберите провайдера</span></div>
|
||||||
|
<div class="prov-grid" id="provGrid">
|
||||||
|
<div class="prov-card ours">
|
||||||
|
<span class="prov-badge">⭐ Рекомендуем</span>
|
||||||
|
<div class="prov-name">🦊 EvilFox VPN</div>
|
||||||
|
<div class="prov-sub">Оптимальные серверы рядом с вами · защита без настройки</div>
|
||||||
|
<span class="prov-gift">🎁 Демо-доступ от 30₽</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">
|
||||||
|
<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">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ Профили ============ -->
|
<!-- ============ Мои серверы ============ -->
|
||||||
<section class="page" data-page="profiles">
|
<section class="page" data-page="profiles">
|
||||||
<h2 class="page-title">Профили</h2>
|
<h2 class="page-title">Мои серверы</h2>
|
||||||
|
|
||||||
|
<section class="key-box" aria-label="Активация ключа">
|
||||||
|
<h3>Ключ доступа</h3>
|
||||||
|
<input id="subUrl" type="text" placeholder="Вставьте ключ или ссылку доступа" spellcheck="false" />
|
||||||
|
<button class="action primary" id="subBtn" type="button">Активировать ключ</button>
|
||||||
|
<p class="key-hint">Введите ваучер для получения доступа</p>
|
||||||
|
<button class="key-link" id="buyKeyLink" type="button">Где купить ключ?</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="server-toolbar">
|
<div class="server-toolbar">
|
||||||
<div class="section-title"><span>Серверы</span></div>
|
<div class="section-title"><span>Серверы</span></div>
|
||||||
<div class="server-actions">
|
<div class="server-actions">
|
||||||
@@ -1146,9 +1346,26 @@
|
|||||||
<div class="server-list" id="serverList"></div>
|
<div class="server-list" id="serverList"></div>
|
||||||
<select id="profile" hidden></select>
|
<select id="profile" hidden></select>
|
||||||
|
|
||||||
|
<section class="subinfo" id="subInfoCard" hidden>
|
||||||
|
<div class="subinfo-title">Информация об аккаунте</div>
|
||||||
|
<div class="subinfo-row" id="subExpireRow" hidden>
|
||||||
|
<span class="k">Действует до</span>
|
||||||
|
<span class="v" id="subExpireVal"></span>
|
||||||
|
</div>
|
||||||
|
<div class="subinfo-row" id="subTrafficRow" hidden>
|
||||||
|
<span class="k">Трафик</span>
|
||||||
|
<span class="v" id="subTrafficVal"></span>
|
||||||
|
</div>
|
||||||
|
<div class="subinfo-row" id="subDevicesRow" hidden>
|
||||||
|
<span class="k">Устройства</span>
|
||||||
|
<span class="v" id="subDevicesVal"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<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">
|
||||||
|
<p class="host-line">Адрес сервера: <span id="profHost">—</span></p>
|
||||||
<div>
|
<div>
|
||||||
<label class="field" for="name">Название</label>
|
<label class="field" for="name">Название</label>
|
||||||
<input id="name" type="text" placeholder="Например: DE-1 / Home" spellcheck="false" />
|
<input id="name" type="text" placeholder="Например: DE-1 / Home" spellcheck="false" />
|
||||||
@@ -1157,13 +1374,6 @@
|
|||||||
<label class="field" for="proxy">Ссылка / конфиг</label>
|
<label class="field" for="proxy">Ссылка / конфиг</label>
|
||||||
<textarea id="proxy" rows="3" placeholder="vless:// · vmess:// · trojan:// · hy2:// · naive+https:// · AWG .conf" spellcheck="false"></textarea>
|
<textarea id="proxy" rows="3" placeholder="vless:// · vmess:// · trojan:// · hy2:// · naive+https:// · AWG .conf" spellcheck="false"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="field" for="subUrl">URL подписки</label>
|
|
||||||
<div class="sub-bar">
|
|
||||||
<input id="subUrl" type="text" placeholder="https://…/api/sub/… или обычная подписка" spellcheck="false" />
|
|
||||||
<button class="icon-btn wide" id="subBtn" type="button" title="Обновить подписку">Обновить</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="action secondary" id="saveBtn" type="button">Сохранить профиль</button>
|
<button class="action secondary" id="saveBtn" type="button">Сохранить профиль</button>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -1275,21 +1485,56 @@
|
|||||||
</div>
|
</div>
|
||||||
<p class="meta" id="storeUpdateNote" hidden style="margin:8px 0 0;min-height:0">Обновления этой сборки — через Microsoft Store.</p>
|
<p class="meta" id="storeUpdateNote" hidden style="margin:8px 0 0;min-height:0">Обновления этой сборки — через Microsoft Store.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details class="panel" id="advBox">
|
||||||
|
<summary>Расширенные настройки</summary>
|
||||||
|
<div style="margin-top:6px">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ Логи ============ -->
|
<!-- ============ Статистика ============ -->
|
||||||
<section class="page" data-page="logs">
|
<section class="page" data-page="logs">
|
||||||
<h2 class="page-title">Логи</h2>
|
<h2 class="page-title">Статистика</h2>
|
||||||
<pre class="log-view" id="logView">Пусто — подключитесь, чтобы увидеть логи ядра.</pre>
|
|
||||||
|
<section class="traffic" id="trafficCard2" hidden>
|
||||||
|
<div class="traffic-row">
|
||||||
|
<span>📊 <span id="trafficLabel2">…</span></span>
|
||||||
|
<span class="pct" id="trafficPct2"></span>
|
||||||
|
</div>
|
||||||
|
<div class="traffic-bar"><div class="traffic-fill" id="trafficFill2"></div></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="section-title" style="margin-top:4px"><span>Журнал подключения</span></div>
|
||||||
|
<p class="adv-line" id="logsProxyLine" hidden style="margin:0 0 8px"></p>
|
||||||
|
<pre class="log-view" id="logView">Пусто — подключитесь, чтобы увидеть журнал.</pre>
|
||||||
<div class="tools" style="margin-top:0">
|
<div class="tools" style="margin-top:0">
|
||||||
<button class="action secondary" id="logsRefreshBtn" type="button">Обновить</button>
|
<button class="action secondary" id="logsRefreshBtn" type="button">Обновить</button>
|
||||||
<button class="action secondary" id="logsClearBtn" type="button">Очистить экран</button>
|
<button class="action secondary" id="logsClearBtn" type="button">Очистить экран</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ============ О программе ============ -->
|
<!-- ============ Помощь ============ -->
|
||||||
<section class="page" data-page="about">
|
<section class="page" data-page="about">
|
||||||
<h2 class="page-title">О программе</h2>
|
<h2 class="page-title">Помощь</h2>
|
||||||
|
|
||||||
|
<details class="panel">
|
||||||
|
<summary>Как подключиться?</summary>
|
||||||
|
<p class="remna-hint" style="margin-top:6px">Активируйте ключ на вкладке «Мои серверы», затем на вкладке «Защита» нажмите большую круглую кнопку. Приложение само выберет оптимальный сервер.</p>
|
||||||
|
</details>
|
||||||
|
<details class="panel">
|
||||||
|
<summary>Где купить ключ?</summary>
|
||||||
|
<p class="remna-hint" style="margin-top:6px">Ключи и тарифы — на сайте evilfox.win (кнопка ниже). После оплаты вставьте полученный ключ на вкладке «Мои серверы».</p>
|
||||||
|
</details>
|
||||||
|
<details class="panel">
|
||||||
|
<summary>Не подключается — что делать?</summary>
|
||||||
|
<p class="remna-hint" style="margin-top:6px">Проверьте, что ключ активирован и не истёк («Мои серверы» → «Информация об аккаунте»). Попробуйте другой сервер (кнопка «Лучший»). Подробности — в журнале на вкладке «Статистика».</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class="about-card">
|
<div class="about-card">
|
||||||
<div class="logo" aria-hidden="true">
|
<div class="logo" aria-hidden="true">
|
||||||
<svg viewBox="0 0 32 32" fill="none">
|
<svg viewBox="0 0 32 32" fill="none">
|
||||||
@@ -1304,9 +1549,9 @@
|
|||||||
<div class="tools" style="margin-top:0">
|
<div class="tools" style="margin-top:0">
|
||||||
<button class="action secondary" id="aboutUpdBtn" type="button">Проверить обновление</button>
|
<button class="action secondary" id="aboutUpdBtn" type="button">Проверить обновление</button>
|
||||||
</div>
|
</div>
|
||||||
<section class="shop" aria-label="Купить подключение">
|
<section class="shop" aria-label="Купить ключ">
|
||||||
<h3>Нужен доступ?</h3>
|
<h3>Нужен ключ?</h3>
|
||||||
<p>Ключи и тарифы на evilfox.win — демо от 30₽.</p>
|
<p>Ключи и тарифы на evilfox.win — демо от 30₽. Техподдержка — в Telegram.</p>
|
||||||
<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>
|
||||||
@@ -1413,7 +1658,24 @@
|
|||||||
const SKIP_UPDATE_KEY = "navis.skipUpdateVersion";
|
const SKIP_UPDATE_KEY = "navis.skipUpdateVersion";
|
||||||
let storePackaged = false;
|
let storePackaged = false;
|
||||||
let currentMode = "proxy";
|
let currentMode = "proxy";
|
||||||
let currentPage = "home";
|
let currentPage = "guard";
|
||||||
|
|
||||||
|
// Русские названия стран для человекочитаемых имён серверов (ISO alpha-2).
|
||||||
|
const COUNTRY_RU = {
|
||||||
|
AT: "Австрия", AU: "Австралия", AE: "ОАЭ", AR: "Аргентина", BE: "Бельгия",
|
||||||
|
BG: "Болгария", BR: "Бразилия", CA: "Канада", CH: "Швейцария", CZ: "Чехия",
|
||||||
|
DE: "Германия", DK: "Дания", EE: "Эстония", ES: "Испания", FI: "Финляндия",
|
||||||
|
FR: "Франция", GB: "Великобритания", GR: "Греция", HK: "Гонконг", HU: "Венгрия",
|
||||||
|
IE: "Ирландия", IL: "Израиль", IN: "Индия", IT: "Италия", JP: "Япония",
|
||||||
|
KR: "Южная Корея", KZ: "Казахстан", LT: "Литва", LU: "Люксембург", LV: "Латвия",
|
||||||
|
MD: "Молдова", MX: "Мексика", NL: "Нидерланды", NO: "Норвегия", PL: "Польша",
|
||||||
|
PT: "Португалия", RO: "Румыния", RS: "Сербия", RU: "Россия", SE: "Швеция",
|
||||||
|
SG: "Сингапур", SK: "Словакия", TR: "Турция", UA: "Украина", US: "США", ZA: "ЮАР"
|
||||||
|
};
|
||||||
|
function countryRu(cc) {
|
||||||
|
const c = String(cc || "").toUpperCase();
|
||||||
|
return COUNTRY_RU[c] || "";
|
||||||
|
}
|
||||||
|
|
||||||
// ==== Navigation ====
|
// ==== Navigation ====
|
||||||
document.querySelectorAll(".nav-btn").forEach((b) => {
|
document.querySelectorAll(".nav-btn").forEach((b) => {
|
||||||
@@ -1598,10 +1860,49 @@
|
|||||||
function rememberPings(pings) {
|
function rememberPings(pings) {
|
||||||
pingMap = {};
|
pingMap = {};
|
||||||
(pings || []).forEach((p) => { pingMap[p.name] = p; });
|
(pings || []).forEach((p) => { pingMap[p.name] = p; });
|
||||||
|
rebuildFriendlyNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Человекочитаемые имена серверов («Чехия — оптимальный сервер», «Германия — сервер 2»).
|
||||||
|
// Реальный host:port показываем только в свойствах сервера и в журнале.
|
||||||
|
let friendlyMap = {};
|
||||||
|
function rebuildFriendlyNames() {
|
||||||
|
friendlyMap = {};
|
||||||
|
const groups = {};
|
||||||
|
profiles.forEach((p) => {
|
||||||
|
const key = String(p.country || "").toUpperCase() || "__none__";
|
||||||
|
(groups[key] = groups[key] || []).push(p);
|
||||||
|
});
|
||||||
|
Object.keys(groups).forEach((key) => {
|
||||||
|
const list = groups[key].slice().sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||||
|
// «Оптимальный» — сервер с лучшим измеренным пингом в стране, иначе первый по имени.
|
||||||
|
let bestIdx = 0, bestMsVal = Infinity;
|
||||||
|
list.forEach((p, i) => {
|
||||||
|
const pr = pingMap[p.name];
|
||||||
|
if (pr && pr.ok && pr.ms < bestMsVal) { bestMsVal = pr.ms; bestIdx = i; }
|
||||||
|
});
|
||||||
|
const region = key === "__none__" ? "" : countryRu(key);
|
||||||
|
let n = 0;
|
||||||
|
list.forEach((p, i) => {
|
||||||
|
n += 1;
|
||||||
|
let title;
|
||||||
|
if (i === bestIdx) {
|
||||||
|
title = region ? (region + " — оптимальный сервер") : ("Сервер " + n + " — оптимальный");
|
||||||
|
} else {
|
||||||
|
title = region ? (region + " — сервер " + n) : ("Сервер " + n);
|
||||||
|
}
|
||||||
|
friendlyMap[p.name] = { title: title, region: region };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function friendlyTitle(name) {
|
||||||
|
const f = friendlyMap[name];
|
||||||
|
return (f && f.title) || name || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillProfiles(list, active) {
|
function fillProfiles(list, active) {
|
||||||
profiles = list || [];
|
profiles = list || [];
|
||||||
|
rebuildFriendlyNames();
|
||||||
const cur = profile.value;
|
const cur = profile.value;
|
||||||
profile.innerHTML = "";
|
profile.innerHTML = "";
|
||||||
profiles.forEach((p) => {
|
profiles.forEach((p) => {
|
||||||
@@ -1633,15 +1934,15 @@
|
|||||||
if (!p) {
|
if (!p) {
|
||||||
$("curFlag").textContent = "·";
|
$("curFlag").textContent = "·";
|
||||||
$("curName").textContent = "Нет сервера";
|
$("curName").textContent = "Нет сервера";
|
||||||
$("curHost").textContent = "добавьте профиль или подписку";
|
$("curHost").textContent = "активируйте ключ на вкладке «Мои серверы»";
|
||||||
$("curProto").textContent = "—";
|
$("curProto").textContent = "—";
|
||||||
$("curPing").textContent = "…";
|
$("curPing").textContent = "…";
|
||||||
$("curPing").className = "ms";
|
$("curPing").className = "ms";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$("curFlag").textContent = p.flag || (p.country || "·");
|
$("curFlag").textContent = p.flag || (p.country || "·");
|
||||||
$("curName").textContent = p.name;
|
$("curName").textContent = friendlyTitle(p.name);
|
||||||
$("curHost").textContent = p.host || "нет хоста";
|
$("curHost").textContent = "нажмите, чтобы выбрать другой сервер";
|
||||||
$("curProto").textContent = (p.protocol || "?").toString();
|
$("curProto").textContent = (p.protocol || "?").toString();
|
||||||
const pr = pingMap[p.name];
|
const pr = pingMap[p.name];
|
||||||
const el = $("curPing");
|
const el = $("curPing");
|
||||||
@@ -1665,7 +1966,7 @@
|
|||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
const empty = document.createElement("div");
|
const empty = document.createElement("div");
|
||||||
empty.className = "server-empty";
|
empty.className = "server-empty";
|
||||||
empty.textContent = "Нет серверов — подождите автовыдачу или добавьте подписку";
|
empty.textContent = "Нет серверов — активируйте ключ доступа выше";
|
||||||
serverList.appendChild(empty);
|
serverList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1685,10 +1986,10 @@
|
|||||||
metaCol.className = "meta-col";
|
metaCol.className = "meta-col";
|
||||||
const nameEl = document.createElement("div");
|
const nameEl = document.createElement("div");
|
||||||
nameEl.className = "name";
|
nameEl.className = "name";
|
||||||
nameEl.textContent = p.name;
|
nameEl.textContent = friendlyTitle(p.name);
|
||||||
const sub = document.createElement("div");
|
const sub = document.createElement("div");
|
||||||
sub.className = "sub";
|
sub.className = "sub";
|
||||||
sub.textContent = p.host || "нет хоста";
|
sub.textContent = p.name === active ? "активный сервер" : "защищённое соединение";
|
||||||
metaCol.appendChild(nameEl);
|
metaCol.appendChild(nameEl);
|
||||||
metaCol.appendChild(sub);
|
metaCol.appendChild(sub);
|
||||||
left.appendChild(flag);
|
left.appendChild(flag);
|
||||||
@@ -1739,9 +2040,9 @@
|
|||||||
async function onServerConnect(name) {
|
async function onServerConnect(name) {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
await withBusy(async () => {
|
await withBusy(async () => {
|
||||||
setMeta("Подключение к «" + name + "»…");
|
setMeta("Подключение к «" + friendlyTitle(name) + "»…");
|
||||||
await connectProfile(name);
|
await connectProfile(name);
|
||||||
setMeta("Подключено: " + name, "ok");
|
setMeta("Защищённое соединение: " + friendlyTitle(name), "ok");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1778,7 +2079,35 @@
|
|||||||
if (vpnChip) vpnChip.hidden = !state.vpn_active;
|
if (vpnChip) vpnChip.hidden = !state.vpn_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Прогресс-бар трафика на «Защите» и «Статистике» (📊 Осталось X из Y ГБ).
|
||||||
|
function renderTraffic(info) {
|
||||||
|
const hasTraffic = !!(info && (info.total || info.download || info.upload));
|
||||||
|
[["trafficCard", "trafficLabel", "trafficPct", "trafficFill"],
|
||||||
|
["trafficCard2", "trafficLabel2", "trafficPct2", "trafficFill2"]].forEach((ids) => {
|
||||||
|
const card = $(ids[0]);
|
||||||
|
if (!card) return;
|
||||||
|
if (!hasTraffic) { card.hidden = true; return; }
|
||||||
|
card.hidden = false;
|
||||||
|
const used = (info.upload || 0) + (info.download || 0);
|
||||||
|
const label = $(ids[1]), pct = $(ids[2]), fill = $(ids[3]);
|
||||||
|
if (info.total > 0) {
|
||||||
|
const left = Math.max(0, info.total - used);
|
||||||
|
const usedPct = Math.min(100, Math.round(used / info.total * 100));
|
||||||
|
label.textContent = "Осталось " + fmtTraffic(left) + " из " + fmtTraffic(info.total);
|
||||||
|
pct.textContent = usedPct + "%";
|
||||||
|
fill.style.width = usedPct + "%";
|
||||||
|
fill.classList.toggle("warn", usedPct >= 85);
|
||||||
|
} else {
|
||||||
|
label.textContent = "Использовано " + fmtTraffic(used) + " · Безлимит";
|
||||||
|
pct.textContent = "";
|
||||||
|
fill.style.width = "100%";
|
||||||
|
fill.classList.remove("warn");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function renderSubInfo(info) {
|
function renderSubInfo(info) {
|
||||||
|
renderTraffic(info);
|
||||||
const card = $("subInfoCard");
|
const card = $("subInfoCard");
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const expireRow = $("subExpireRow"), trafficRow = $("subTrafficRow"), devicesRow = $("subDevicesRow");
|
const expireRow = $("subExpireRow"), trafficRow = $("subTrafficRow"), devicesRow = $("subDevicesRow");
|
||||||
@@ -1798,7 +2127,7 @@
|
|||||||
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 {
|
||||||
v.textContent = daysLeft <= 14 ? (text + " · осталось " + daysLeft + " дн.") : text;
|
v.textContent = daysLeft <= 14 ? (text + " · осталось " + daysLeft + " дн.") : text;
|
||||||
@@ -1866,9 +2195,7 @@
|
|||||||
dot.classList.toggle("on", connected);
|
dot.classList.toggle("on", connected);
|
||||||
hero.classList.toggle("on", connected);
|
hero.classList.toggle("on", connected);
|
||||||
btn.classList.toggle("on", connected);
|
btn.classList.toggle("on", connected);
|
||||||
statusText.textContent = connected
|
statusText.textContent = connected ? "Защищённое соединение" : "Отключено";
|
||||||
? ("Подключено" + (state.profile ? " · " + state.profile : ""))
|
|
||||||
: "Отключено";
|
|
||||||
btn.title = connected ? "Отключить" : "Подключить";
|
btn.title = connected ? "Отключить" : "Подключить";
|
||||||
|
|
||||||
const lock = connected || busy;
|
const lock = connected || busy;
|
||||||
@@ -1890,6 +2217,7 @@
|
|||||||
if (modeProxyBtn) modeProxyBtn.disabled = busy || connected;
|
if (modeProxyBtn) modeProxyBtn.disabled = busy || connected;
|
||||||
if (modeVpnBtn) modeVpnBtn.disabled = busy || connected;
|
if (modeVpnBtn) modeVpnBtn.disabled = busy || connected;
|
||||||
btn.disabled = busy;
|
btn.disabled = busy;
|
||||||
|
btn.classList.toggle("busy", busy);
|
||||||
|
|
||||||
rememberPings(state.pings || []);
|
rememberPings(state.pings || []);
|
||||||
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
fillProfiles(state.profiles || [], state.active_profile || state.profile);
|
||||||
@@ -1917,23 +2245,35 @@
|
|||||||
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";
|
||||||
|
|
||||||
let detail = "";
|
// Технические адреса прокси — только в «Настройки → Расширенные» и на «Статистике».
|
||||||
if (connected) {
|
const advHttp = $("advHttp"), advSocks = $("advSocks"), logsProxyLine = $("logsProxyLine");
|
||||||
|
if (advHttp) advHttp.textContent = state.http_proxy || "появится после подключения";
|
||||||
|
if (advSocks) advSocks.textContent = state.socks_proxy || "появится после подключения";
|
||||||
|
if (logsProxyLine) {
|
||||||
const parts = [];
|
const parts = [];
|
||||||
if (state.vpn_active) parts.push("VPN: весь трафик через туннель");
|
|
||||||
if (state.http_proxy) parts.push("HTTP " + state.http_proxy);
|
if (state.http_proxy) parts.push("HTTP " + state.http_proxy);
|
||||||
if (state.socks_proxy) parts.push("SOCKS " + state.socks_proxy);
|
if (state.socks_proxy) parts.push("SOCKS " + state.socks_proxy);
|
||||||
detail = parts.join(" · ") || "Туннель активен";
|
logsProxyLine.hidden = !parts.length;
|
||||||
heroHint.textContent = detail;
|
logsProxyLine.textContent = parts.length ? ("Прокси: " + parts.join(" · ")) : "";
|
||||||
|
}
|
||||||
|
renderProvConnect();
|
||||||
|
renderProfHost();
|
||||||
|
|
||||||
|
let detail = "";
|
||||||
|
if (connected) {
|
||||||
|
detail = state.vpn_active
|
||||||
|
? "VPN: весь трафик под защитой · Ваш IP скрыт"
|
||||||
|
: "Защищённое соединение активно · Ваш IP скрыт";
|
||||||
|
heroHint.textContent = "Ваш IP скрыт · Трафик зашифрован";
|
||||||
} else if (state.core_ready === false) {
|
} else if (state.core_ready === false) {
|
||||||
detail = "Сначала установите cores в Настройках («Сервис»)";
|
detail = "Сначала установите cores в Настройках («Сервис»)";
|
||||||
heroHint.textContent = "Нужны cores для выбранного протокола";
|
heroHint.textContent = "Нужны cores для выбранного протокола";
|
||||||
} else {
|
} else {
|
||||||
const n = (state.profiles || []).length;
|
const n = (state.profiles || []).length;
|
||||||
detail = n > 1 ? ("Серверов: " + n + " · Профили → Пинг / Лучший") : "Готово к подключению";
|
detail = n > 1 ? ("Серверов: " + n + " · «Мои серверы» → Лучший") : "Готово к подключению";
|
||||||
heroHint.textContent = currentMode === "vpn"
|
heroHint.textContent = currentMode === "vpn"
|
||||||
? "Режим VPN: весь трафик, включая игры и приложения"
|
? "Режим VPN: защита всего трафика, включая игры и приложения"
|
||||||
: "Режим прокси: браузеры и приложения через прокси";
|
: "Нажмите на кнопку, чтобы включить защиту";
|
||||||
}
|
}
|
||||||
if (!busy && Date.now() > metaHoldUntil) {
|
if (!busy && Date.now() > metaHoldUntil) {
|
||||||
setMeta(detail, state.core_ready === false ? "err" : "");
|
setMeta(detail, state.core_ready === false ? "err" : "");
|
||||||
@@ -1941,6 +2281,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Кнопка «Подключиться» на карточке EvilFox (вкладка «Защита»).
|
||||||
|
function renderProvConnect() {
|
||||||
|
const b = $("provConnectBtn");
|
||||||
|
if (!b) return;
|
||||||
|
if (connected) {
|
||||||
|
b.textContent = "✅ Подключено";
|
||||||
|
b.disabled = true;
|
||||||
|
} else {
|
||||||
|
b.textContent = "🔥 Подключиться";
|
||||||
|
b.disabled = busy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Реальный host:port выбранного сервера — в «Свойствах сервера» (муторные детали).
|
||||||
|
function renderProfHost() {
|
||||||
|
const el = $("profHost");
|
||||||
|
if (!el) return;
|
||||||
|
const p = profiles.find((x) => x.name === profile.value);
|
||||||
|
el.textContent = (p && p.host) ? p.host : "—";
|
||||||
|
}
|
||||||
|
|
||||||
async function refresh(opts) {
|
async function refresh(opts) {
|
||||||
const state = await getState();
|
const state = await getState();
|
||||||
paint(state, opts);
|
paint(state, opts);
|
||||||
@@ -1966,9 +2327,11 @@
|
|||||||
|
|
||||||
function paintButtonsLocked(locked) {
|
function paintButtonsLocked(locked) {
|
||||||
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn,
|
[btn, coreBtn, saveBtn, addBtn, delBtn, profile, pingBtn, bestBtn, updCheckBtn, aboutUpdBtn,
|
||||||
updateBtn, skipUpdateBtn, subBtn, subUrl, modeProxyBtn, modeVpnBtn, logsRefreshBtn].forEach((b) => {
|
updateBtn, skipUpdateBtn, subBtn, subUrl, modeProxyBtn, modeVpnBtn, logsRefreshBtn,
|
||||||
|
$("provConnectBtn")].forEach((b) => {
|
||||||
if (b) b.disabled = locked;
|
if (b) b.disabled = locked;
|
||||||
});
|
});
|
||||||
|
btn.classList.toggle("busy", locked);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runBest(autoConnect) {
|
async function runBest(autoConnect) {
|
||||||
@@ -1978,7 +2341,7 @@
|
|||||||
renderServerList(res.best_name || profile.value);
|
renderServerList(res.best_name || profile.value);
|
||||||
renderCurServer(res.best_name || profile.value);
|
renderCurServer(res.best_name || profile.value);
|
||||||
if (res.best_name) {
|
if (res.best_name) {
|
||||||
setMeta("Лучший: " + res.best_name + " · " + res.best_ms + " ms" + (res.connected ? " · подключено" : ""), "ok");
|
setMeta("Лучший: " + friendlyTitle(res.best_name) + " · " + res.best_ms + " ms" + (res.connected ? " · подключено" : ""), "ok");
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -2000,7 +2363,7 @@
|
|||||||
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
|
await saveProfile(nameInput.value.trim() || profile.value, proxy.value.trim(), !!sysproxy.checked);
|
||||||
await persistHy2IfNeeded();
|
await persistHy2IfNeeded();
|
||||||
await connect();
|
await connect();
|
||||||
setMeta("Туннель активен", "ok");
|
setMeta("Защищённое соединение установлено", "ok");
|
||||||
}
|
}
|
||||||
} catch (e) { setMeta(String(e), "err"); }
|
} catch (e) { setMeta(String(e), "err"); }
|
||||||
}));
|
}));
|
||||||
@@ -2023,7 +2386,7 @@
|
|||||||
|
|
||||||
delBtn.addEventListener("click", () => withBusy(async () => {
|
delBtn.addEventListener("click", () => withBusy(async () => {
|
||||||
try {
|
try {
|
||||||
if (!confirm("Удалить профиль «" + profile.value + "»?")) return;
|
if (!confirm("Удалить сервер «" + friendlyTitle(profile.value) + "»?")) return;
|
||||||
await deleteProfile(profile.value);
|
await deleteProfile(profile.value);
|
||||||
formHydrated = false;
|
formHydrated = false;
|
||||||
dirty = false;
|
dirty = false;
|
||||||
@@ -2101,26 +2464,103 @@
|
|||||||
async function runImportSubscription() {
|
async function runImportSubscription() {
|
||||||
const url = subUrl.value.trim();
|
const url = subUrl.value.trim();
|
||||||
if (!url) {
|
if (!url) {
|
||||||
setMeta("Вставьте URL подписки", "err");
|
setMeta("Вставьте ключ доступа (ваучер)", "err");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setMeta("Загрузка подписки…");
|
setMeta("Активация ключа…");
|
||||||
const r = await importSubscription(url);
|
const r = await importSubscription(url);
|
||||||
formHydrated = false;
|
formHydrated = false;
|
||||||
dirty = false;
|
dirty = false;
|
||||||
setMeta(importSummary(r) + " · измеряю пинг…", "ok");
|
setMeta(importSummary(r) + " · подбираю оптимальный сервер…", "ok");
|
||||||
await runBest(!!autoBest.checked);
|
await runBest(!!autoBest.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
function importSummary(r) {
|
function importSummary(r) {
|
||||||
if (r && typeof r === "object") {
|
if (r && typeof r === "object") {
|
||||||
let s = "Импортировано " + (r.imported || 0) + " серверов";
|
let s = "Ключ активирован · серверов: " + (r.imported || 0);
|
||||||
if (r.skipped > 0) s += " (пропущено " + r.skipped + ")";
|
if (r.skipped > 0) s += " (пропущено " + r.skipped + ")";
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
return "Импортировано: " + r;
|
return "Ключ активирован: " + r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// «Где купить ключ?» — открывает evilfox.win в браузере.
|
||||||
|
const buyKeyLink = $("buyKeyLink");
|
||||||
|
if (buyKeyLink) buyKeyLink.addEventListener("click", openShop);
|
||||||
|
|
||||||
|
// ==== Вкладка «Защита»: карточки провайдеров ====
|
||||||
|
// Ссылки конкурентов — чистые, без реферальных кодов (правила Store).
|
||||||
|
document.querySelectorAll(".prov-go").forEach((b) => {
|
||||||
|
b.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
setMeta("Открываю сайт " + (b.dataset.name || "провайдера") + "…");
|
||||||
|
await openURL(b.dataset.url);
|
||||||
|
setMeta("Открыто в браузере", "ok");
|
||||||
|
} catch (err) {
|
||||||
|
setMeta("Не удалось открыть ссылку: " + String(err), "err");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const provConnectBtn = $("provConnectBtn");
|
||||||
|
if (provConnectBtn) provConnectBtn.addEventListener("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();
|
||||||
@@ -2150,7 +2590,7 @@
|
|||||||
async function refreshLogs() {
|
async function refreshLogs() {
|
||||||
try {
|
try {
|
||||||
const text = await getLogs();
|
const text = await getLogs();
|
||||||
logView.textContent = (text && text.trim()) ? text : "Пусто — подключитесь, чтобы увидеть логи ядра.";
|
logView.textContent = (text && text.trim()) ? text : "Пусто — подключитесь, чтобы увидеть журнал.";
|
||||||
logView.scrollTop = logView.scrollHeight;
|
logView.scrollTop = logView.scrollHeight;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logView.textContent = "Не удалось получить логи: " + String(e);
|
logView.textContent = "Не удалось получить логи: " + String(e);
|
||||||
|
|||||||
@@ -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.0.1"
|
const CurrentVersion = "4.1.0"
|
||||||
|
|
||||||
// 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.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<Identity
|
<Identity
|
||||||
Name="EvilFox.EvilFox"
|
Name="EvilFox.EvilFox"
|
||||||
Publisher="CN=EvilFox"
|
Publisher="CN=EvilFox"
|
||||||
Version="4.0.1.1"
|
Version="4.1.0.1"
|
||||||
ProcessorArchitecture="x64" />
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
|
|||||||
+4
-4
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "4.0.1",
|
"version": "4.1.0",
|
||||||
"notes": "4.0.1: убрана привязка аккаунта, конфиги — через вашу подписку; добавлен блок рекомендуемых сервисов",
|
"notes": "4.1.0: новый интерфейс для Microsoft Store — вкладки Защита / Мои серверы / Статистика / Помощь, экран выбора провайдера, активация ключа, человекочитаемые имена серверов",
|
||||||
"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": "c9551072d2b6206cd9323ae4eaf2d4dd20377237e837becc785b69539ec79939",
|
"sha256": "6a6a0e80327e6aa87dd4c75b3fdb0795f0b7faf4d54481a73a99c31358cf492f",
|
||||||
"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": "c9551072d2b6206cd9323ae4eaf2d4dd20377237e837becc785b69539ec79939",
|
"sha256": "6a6a0e80327e6aa87dd4c75b3fdb0795f0b7faf4d54481a73a99c31358cf492f",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"FixedFileInfo": {
|
"FixedFileInfo": {
|
||||||
"FileVersion": { "Major": 4, "Minor": 0, "Patch": 1, "Build": 1 },
|
"FileVersion": { "Major": 4, "Minor": 1, "Patch": 0, "Build": 1 },
|
||||||
"ProductVersion": { "Major": 4, "Minor": 0, "Patch": 1, "Build": 1 },
|
"ProductVersion": { "Major": 4, "Minor": 1, "Patch": 0, "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.0.1.1",
|
"FileVersion": "4.1.0.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.0.1.1",
|
"ProductVersion": "4.1.0.1",
|
||||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
Reference in New Issue
Block a user