Release 4.1.1+1: UI polish по чек-листу ревью — трафик «Доступно X ГБ» + процент использования (баг «Осталось 100 из 100 · 0%»), оффер «3 дня бесплатно, затем от 99 ₽/мес», бейдж «Рекомендуем» на карточке EvilFox + подпись «Сторонний сервис» у конкурентов, пилюля «АКТИВЕН» и протокол мелким текстом в списке серверов, «—» вместо пустого пинга, счётчик серверов только на «Мои серверы», 🦊 вместо буквы N (старый логотип Navis), флаги во всех строках, geoflag-алиасы опечаток («Румьиня» → RO), фикс [hidden] vs display:grid. Windows 4.1.1.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+141
-48
@@ -136,6 +136,8 @@
|
||||
--log-bg: rgba(2, 6, 14, 0.6);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
/* Атрибут hidden должен побеждать классовые display:grid/flex (иначе пустые карточки видны). */
|
||||
[hidden] { display: none !important; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
@@ -180,6 +182,7 @@
|
||||
place-items: center;
|
||||
}
|
||||
.logo svg { width: 26px; height: 26px; }
|
||||
.logo .fox { font-size: 1.35rem; line-height: 1; }
|
||||
.brand {
|
||||
font-family: Outfit, sans-serif;
|
||||
font-size: 1.35rem;
|
||||
@@ -529,6 +532,13 @@
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.section-title .count-note {
|
||||
font-size: .68rem;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
label.field {
|
||||
display: block;
|
||||
@@ -839,22 +849,39 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.server-row .right { display: flex; flex-direction: column; align-items: flex-end; gap: 2px; flex: 0 0 auto; }
|
||||
.server-row .proto {
|
||||
font-size: .62rem;
|
||||
.server-row .name-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
||||
/* Пилюля «АКТИВЕН» вместо строчного текста «активный сервер» */
|
||||
.badge-active {
|
||||
flex: 0 0 auto;
|
||||
font-size: .58rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
color: var(--accent-deep);
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid rgba(8,145,178,.35);
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
padding: 2px 7px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
html[data-theme="dark"] .badge-active { color: var(--accent); }
|
||||
.server-row .ms {
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--muted);
|
||||
}
|
||||
.server-row .ms::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 7px; height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
vertical-align: 1px;
|
||||
background: var(--dot-off);
|
||||
}
|
||||
.server-row .ms.good::before, .server-row .ms.ok::before { background: var(--ms-good); }
|
||||
.server-row .ms.mid::before { background: var(--ms-mid); }
|
||||
.server-row .ms.bad::before { background: var(--danger); }
|
||||
.server-row .ms.ok { color: var(--ok); }
|
||||
.server-row .ms.good { color: var(--ms-good); }
|
||||
.server-row .ms.mid { color: var(--ms-mid); }
|
||||
@@ -1017,6 +1044,7 @@
|
||||
transition: width .4s var(--ease);
|
||||
}
|
||||
.traffic-fill.warn { background: linear-gradient(90deg, #d97706, #f59e0b); }
|
||||
.traffic-sub { font-size: .74rem; color: var(--muted); }
|
||||
|
||||
/* ==== Провайдеры (вкладка «Защита») ==== */
|
||||
.prov-grid {
|
||||
@@ -1050,9 +1078,10 @@
|
||||
html[data-theme="dark"] .prov-card.ours {
|
||||
background: linear-gradient(150deg, rgba(34,211,238,.16), var(--surface-2) 70%);
|
||||
}
|
||||
/* Бейдж «Рекомендуем» лежит НА карточке EvilFox (верхний угол), а не между карточками. */
|
||||
.prov-badge {
|
||||
position: absolute;
|
||||
top: -9px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: .64rem;
|
||||
font-weight: 800;
|
||||
@@ -1064,6 +1093,16 @@
|
||||
padding: 3px 9px;
|
||||
box-shadow: 0 4px 10px rgba(251, 191, 36, .35);
|
||||
}
|
||||
.prov-card.ours .prov-name { padding-right: 96px; }
|
||||
/* Приглушённая подпись «Сторонний сервис» на карточках конкурентов */
|
||||
.prov-tag {
|
||||
font-size: .62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .05em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
opacity: .8;
|
||||
}
|
||||
.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; }
|
||||
@@ -1150,6 +1189,7 @@
|
||||
}
|
||||
.about-card .logo { margin: 0 auto 10px; width: 56px; height: 56px; border-radius: 16px; }
|
||||
.about-card .logo svg { width: 34px; height: 34px; }
|
||||
.about-card .logo .fox { font-size: 1.9rem; }
|
||||
.about-card h2 { font-family: Outfit, sans-serif; margin: 0 0 4px; letter-spacing: -.03em; }
|
||||
.about-card p { margin: 0; color: var(--muted); font-size: .84rem; line-height: 1.45; }
|
||||
|
||||
@@ -1191,14 +1231,9 @@
|
||||
<!-- ===== Sidebar ===== -->
|
||||
<nav class="side">
|
||||
<div class="brand-row">
|
||||
<div class="logo" aria-hidden="true">
|
||||
<svg viewBox="0 0 32 32" fill="none">
|
||||
<path d="M8 26V6h4.2L20 18.4V6H24v20h-4.2L12 13.6V26H8z" fill="#f4fff9"/>
|
||||
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="logo" aria-hidden="true"><span class="fox">🦊</span></div>
|
||||
<h1 class="brand">EvilFox</h1>
|
||||
<span class="badge-ver" id="badgeVer">4.1.0</span>
|
||||
<span class="badge-ver" id="badgeVer">4.1.1</span>
|
||||
</div>
|
||||
|
||||
<button class="nav-btn active" type="button" data-page="guard">
|
||||
@@ -1262,8 +1297,8 @@
|
||||
<span class="dot" id="dot"></span>
|
||||
<span id="statusText">Отключено</span>
|
||||
</div>
|
||||
<!-- Протокол показываем на карточке активного сервера ниже — в статусной строке не дублируем. -->
|
||||
<div class="chips-line">
|
||||
<span class="proto-chip" id="protoChip">протокол</span>
|
||||
<span class="mode-chip" id="vpnChip" hidden>VPN · весь трафик</span>
|
||||
</div>
|
||||
<p class="hero-hint" id="heroHint">Нажмите на кнопку, чтобы включить защиту</p>
|
||||
@@ -1287,6 +1322,7 @@
|
||||
<span class="pct" id="trafficPct"></span>
|
||||
</div>
|
||||
<div class="traffic-bar"><div class="traffic-fill" id="trafficFill"></div></div>
|
||||
<div class="traffic-sub" id="trafficExp" hidden></div>
|
||||
</section>
|
||||
|
||||
<div class="section">
|
||||
@@ -1296,25 +1332,28 @@
|
||||
<span class="prov-badge">⭐ Рекомендуем</span>
|
||||
<div class="prov-name">🦊 EvilFox VPN</div>
|
||||
<div class="prov-sub">Оптимальные серверы рядом с вами · защита без настройки</div>
|
||||
<span class="prov-gift">🎁 Демо-доступ от 30₽</span>
|
||||
<span class="prov-gift" id="provGift">🚀 3 дня бесплатно. Затем — от 99 ₽/мес</span>
|
||||
<button class="action primary" id="provConnectBtn" type="button">🔥 Подключиться</button>
|
||||
<button class="prov-ping" id="provPingOurs" type="button">📊 <span class="val">Проверить скорость</span></button>
|
||||
</div>
|
||||
<div class="prov-card">
|
||||
<span class="prov-tag">Сторонний сервис</span>
|
||||
<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-ping" type="button" data-base="45">📊 <span class="val">Проверить скорость</span></button>
|
||||
</div>
|
||||
<div class="prov-card">
|
||||
<span class="prov-tag">Сторонний сервис</span>
|
||||
<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-ping" type="button" data-base="38">📊 <span class="val">Проверить скорость</span></button>
|
||||
</div>
|
||||
<div class="prov-card">
|
||||
<span class="prov-tag">Сторонний сервис</span>
|
||||
<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-ping" type="button" data-base="55">📊 <span class="val">Проверить скорость</span></button>
|
||||
</div>
|
||||
@@ -1335,7 +1374,7 @@
|
||||
</section>
|
||||
|
||||
<div class="server-toolbar">
|
||||
<div class="section-title"><span>Серверы</span></div>
|
||||
<div class="section-title"><span>Серверы</span> <span class="count-note" id="srvCount"></span></div>
|
||||
<div class="server-actions">
|
||||
<button class="mini" id="pingBtn" type="button">Пинг</button>
|
||||
<button class="mini accent" id="bestBtn" type="button">Лучший</button>
|
||||
@@ -1507,6 +1546,7 @@
|
||||
<span class="pct" id="trafficPct2"></span>
|
||||
</div>
|
||||
<div class="traffic-bar"><div class="traffic-fill" id="trafficFill2"></div></div>
|
||||
<div class="traffic-sub" id="trafficExp2" hidden></div>
|
||||
</section>
|
||||
|
||||
<div class="section-title" style="margin-top:4px"><span>Журнал подключения</span></div>
|
||||
@@ -1536,12 +1576,7 @@
|
||||
</details>
|
||||
|
||||
<div class="about-card">
|
||||
<div class="logo" aria-hidden="true">
|
||||
<svg viewBox="0 0 32 32" fill="none">
|
||||
<path d="M8 26V6h4.2L20 18.4V6H24v20h-4.2L12 13.6V26H8z" fill="#f4fff9"/>
|
||||
<path d="M22.5 8.2l3.2-1.1-1.1 3.2-5.6 5.6 1.6 1.6 5.6-5.6 3.2-1.1-1.1 3.2" fill="#b8f0d8" opacity=".9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="logo" aria-hidden="true"><span class="fox">🦊</span></div>
|
||||
<h2>EvilFox</h2>
|
||||
<p id="aboutVer">версия…</p>
|
||||
<p style="margin-top:6px">VPN/прокси-клиент: Naive · Hysteria 2 · AWG · Xray</p>
|
||||
@@ -1551,7 +1586,7 @@
|
||||
</div>
|
||||
<section class="shop" aria-label="Купить ключ">
|
||||
<h3>Нужен ключ?</h3>
|
||||
<p>Ключи и тарифы на evilfox.win — демо от 30₽. Техподдержка — в Telegram.</p>
|
||||
<p>Ключи и тарифы на evilfox.win — 3 дня бесплатно, затем от 99 ₽/мес. Техподдержка — в Telegram.</p>
|
||||
<button class="action primary" id="shopBtn" type="button">Открыть магазин</button>
|
||||
<button class="shop-link" id="shopLink" type="button">https://evilfox.win/</button>
|
||||
</section>
|
||||
@@ -1676,6 +1711,18 @@
|
||||
const c = String(cc || "").toUpperCase();
|
||||
return COUNTRY_RU[c] || "";
|
||||
}
|
||||
// "RO" → "🇷🇴" — запасной вариант, если бэкенд не прислал флаг.
|
||||
function isoFlag(cc) {
|
||||
const c = String(cc || "").toUpperCase();
|
||||
if (!/^[A-Z]{2}$/.test(c)) return "";
|
||||
return String.fromCodePoint(0x1f1e6 + c.charCodeAt(0) - 65, 0x1f1e6 + c.charCodeAt(1) - 65);
|
||||
}
|
||||
function pluralRu(n, one, few, many) {
|
||||
const m10 = n % 10, m100 = n % 100;
|
||||
if (m10 === 1 && m100 !== 11) return one;
|
||||
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return few;
|
||||
return many;
|
||||
}
|
||||
|
||||
// ==== Navigation ====
|
||||
document.querySelectorAll(".nav-btn").forEach((b) => {
|
||||
@@ -1748,6 +1795,8 @@
|
||||
const protoChip = $("protoChip");
|
||||
const heroHint = $("heroHint");
|
||||
const SHOP_URL = "https://evilfox.win/";
|
||||
// Текст оффера — правьте здесь (показывается на карточке EvilFox на вкладке «Защита»).
|
||||
const OFFER_TEXT = "🚀 3 дня бесплатно. Затем — от 99 ₽/мес";
|
||||
// Список рекомендуемых сервисов — замените ссылки/названия здесь.
|
||||
// Хост каждой ссылки должен быть разрешён в allowlist openURL
|
||||
// (internal/apphost/app.go → allowedLinkHosts), иначе ссылка не откроется.
|
||||
@@ -1758,6 +1807,8 @@
|
||||
{ icon: "🦊", name: "Git", url: "https://git.de4ima.uk/Evilfox/navi" }
|
||||
];
|
||||
const AUTO_BEST_KEY = "navis.autoBest";
|
||||
// Оффер на карточке EvilFox рендерим из константы, чтобы правка была в одном месте.
|
||||
if ($("provGift")) $("provGift").textContent = OFFER_TEXT;
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem(AUTO_BEST_KEY);
|
||||
@@ -1940,7 +1991,7 @@
|
||||
$("curPing").className = "ms";
|
||||
return;
|
||||
}
|
||||
$("curFlag").textContent = p.flag || (p.country || "·");
|
||||
$("curFlag").textContent = p.flag || isoFlag(p.country) || "·";
|
||||
$("curName").textContent = friendlyTitle(p.name);
|
||||
$("curHost").textContent = "нажмите, чтобы выбрать другой сервер";
|
||||
$("curProto").textContent = (p.protocol || "?").toString();
|
||||
@@ -1949,13 +2000,16 @@
|
||||
if (pr && pr.ok) {
|
||||
el.className = "ms " + msClass(pr.ms, true);
|
||||
el.textContent = pr.ms + " ms";
|
||||
el.title = "";
|
||||
} else if (pr && pr.error) {
|
||||
el.className = "ms bad";
|
||||
el.textContent = "—";
|
||||
el.title = pr.error;
|
||||
} else {
|
||||
// Пинг ещё не измерялся — показываем «—», а не пустоту.
|
||||
el.className = "ms";
|
||||
el.textContent = "…";
|
||||
el.textContent = "—";
|
||||
el.title = "пинг не измерен";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1963,6 +2017,13 @@
|
||||
const active = activeName || profile.value;
|
||||
serverList.innerHTML = "";
|
||||
const list = orderedProfiles();
|
||||
// Количество серверов — только здесь, мелкой приглушённой подписью.
|
||||
const srvCount = $("srvCount");
|
||||
if (srvCount) {
|
||||
srvCount.textContent = list.length
|
||||
? "· " + list.length + " " + pluralRu(list.length, "сервер", "сервера", "серверов")
|
||||
: "";
|
||||
}
|
||||
if (!list.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "server-empty";
|
||||
@@ -1971,35 +2032,44 @@
|
||||
return;
|
||||
}
|
||||
list.forEach((p) => {
|
||||
const isActive = p.name === active;
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = "server-row" + (p.name === active ? " active" : "");
|
||||
row.className = "server-row" + (isActive ? " active" : "");
|
||||
row.dataset.name = p.name;
|
||||
|
||||
const left = document.createElement("div");
|
||||
left.className = "left";
|
||||
const flagEmoji = p.flag || isoFlag(p.country);
|
||||
const flag = document.createElement("span");
|
||||
flag.className = "flag" + (p.flag ? "" : " unknown");
|
||||
flag.textContent = p.flag || (p.country || "·");
|
||||
flag.className = "flag" + (flagEmoji ? "" : " unknown");
|
||||
flag.textContent = flagEmoji || "·";
|
||||
flag.title = p.country || "регион неизвестен";
|
||||
const metaCol = document.createElement("div");
|
||||
metaCol.className = "meta-col";
|
||||
const nameRow = document.createElement("div");
|
||||
nameRow.className = "name-row";
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.className = "name";
|
||||
nameEl.textContent = friendlyTitle(p.name);
|
||||
nameRow.appendChild(nameEl);
|
||||
if (isActive) {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "badge-active";
|
||||
badge.textContent = "АКТИВЕН";
|
||||
nameRow.appendChild(badge);
|
||||
}
|
||||
// Протокол в списке — мелкой приглушённой подписью (чип есть на карточке активного сервера).
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "sub";
|
||||
sub.textContent = p.name === active ? "активный сервер" : "защищённое соединение";
|
||||
metaCol.appendChild(nameEl);
|
||||
sub.textContent = (p.protocol || "").toString() || "защищённое соединение";
|
||||
metaCol.appendChild(nameRow);
|
||||
metaCol.appendChild(sub);
|
||||
left.appendChild(flag);
|
||||
left.appendChild(metaCol);
|
||||
|
||||
const right = document.createElement("div");
|
||||
right.className = "right";
|
||||
const proto = document.createElement("span");
|
||||
proto.className = "proto";
|
||||
proto.textContent = (p.protocol || "?").toString();
|
||||
const ms = document.createElement("span");
|
||||
const pr = pingMap[p.name];
|
||||
if (pr && pr.ok) {
|
||||
@@ -2011,9 +2081,9 @@
|
||||
ms.title = pr.error;
|
||||
} else {
|
||||
ms.className = "ms";
|
||||
ms.textContent = "…";
|
||||
ms.textContent = "—";
|
||||
ms.title = "пинг не измерен";
|
||||
}
|
||||
right.appendChild(proto);
|
||||
right.appendChild(ms);
|
||||
|
||||
row.appendChild(left);
|
||||
@@ -2079,22 +2149,27 @@
|
||||
if (vpnChip) vpnChip.hidden = !state.vpn_active;
|
||||
}
|
||||
|
||||
// Прогресс-бар трафика на «Защите» и «Статистике» (📊 Осталось X из Y ГБ).
|
||||
// Прогресс-бар трафика на «Защите» и «Статистике».
|
||||
// Заголовок — «📊 Доступно X ГБ», полоса и подпись — процент ИЗРАСХОДОВАННОГО
|
||||
// («Z% использовано»), чтобы цифры и полоса были в одной системе координат.
|
||||
function renderTraffic(info) {
|
||||
const hasTraffic = !!(info && (info.total || info.download || info.upload));
|
||||
[["trafficCard", "trafficLabel", "trafficPct", "trafficFill"],
|
||||
["trafficCard2", "trafficLabel2", "trafficPct2", "trafficFill2"]].forEach((ids) => {
|
||||
[["trafficCard", "trafficLabel", "trafficPct", "trafficFill", "trafficExp"],
|
||||
["trafficCard2", "trafficLabel2", "trafficPct2", "trafficFill2", "trafficExp2"]].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]);
|
||||
const label = $(ids[1]), pct = $(ids[2]), fill = $(ids[3]), exp = $(ids[4]);
|
||||
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 + "%";
|
||||
// «из Y ГБ» показываем только когда лимит уже начали расходовать.
|
||||
label.textContent = used > 0
|
||||
? "Доступно " + fmtTraffic(left) + " из " + fmtTraffic(info.total)
|
||||
: "Доступно " + fmtTraffic(left);
|
||||
pct.textContent = usedPct + "% использовано";
|
||||
fill.style.width = usedPct + "%";
|
||||
fill.classList.toggle("warn", usedPct >= 85);
|
||||
} else {
|
||||
@@ -2103,6 +2178,21 @@
|
||||
fill.style.width = "100%";
|
||||
fill.classList.remove("warn");
|
||||
}
|
||||
if (exp) {
|
||||
let expText = "";
|
||||
if (info.expire) {
|
||||
const daysLeft = Math.floor((info.expire * 1000 - Date.now()) / 86400000);
|
||||
if (daysLeft === 0) {
|
||||
expText = "🔥 Сгорает сегодня";
|
||||
} else if (daysLeft > 0) {
|
||||
expText = "🔥 Сгорает через " + daysLeft + " " + pluralRu(daysLeft, "день", "дня", "дней");
|
||||
} else {
|
||||
expText = "Срок доступа истёк";
|
||||
}
|
||||
}
|
||||
exp.hidden = !expText;
|
||||
exp.textContent = expText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2240,8 +2330,11 @@
|
||||
|
||||
const active = (state.profiles || []).find((p) => p.name === profile.value) || {};
|
||||
const label = detectProtoLabel(state, active);
|
||||
protoChip.textContent = label;
|
||||
protoChip.classList.toggle("active", !!state.connected || label !== "протокол");
|
||||
// Чип протокола в hero убран (дублировал карточку активного сервера) — guard на случай возврата.
|
||||
if (protoChip) {
|
||||
protoChip.textContent = label;
|
||||
protoChip.classList.toggle("active", !!state.connected || label !== "протокол");
|
||||
}
|
||||
const hy2 = (label === "hysteria2" || label === "hy2");
|
||||
if ($("hy2Box")) $("hy2Box").style.display = hy2 ? "" : "none";
|
||||
|
||||
@@ -2269,8 +2362,8 @@
|
||||
detail = "Сначала установите cores в Настройках («Сервис»)";
|
||||
heroHint.textContent = "Нужны cores для выбранного протокола";
|
||||
} else {
|
||||
const n = (state.profiles || []).length;
|
||||
detail = n > 1 ? ("Серверов: " + n + " · «Мои серверы» → Лучший") : "Готово к подключению";
|
||||
// Количество серверов на главном экране не показываем — оно есть на «Мои серверы».
|
||||
detail = "Готово к подключению";
|
||||
heroHint.textContent = currentMode === "vpn"
|
||||
? "Режим VPN: защита всего трафика, включая игры и приложения"
|
||||
: "Нажмите на кнопку, чтобы включить защиту";
|
||||
|
||||
@@ -140,6 +140,9 @@ var countryNames = []nameCode{
|
||||
{"исландия", "IS"}, {"люксембург", "LU"}, {"англия", "GB"},
|
||||
{"сша", "US"}, {"америка", "US"},
|
||||
|
||||
// Частые опечатки в именах серверов на панелях (наблюдались в проде).
|
||||
{"румьиня", "RO"}, {"румыня", "RO"}, {"руминия", "RO"},
|
||||
|
||||
// City → country hints (EN + RU).
|
||||
{"frankfurt", "DE"}, {"amsterdam", "NL"}, {"london", "GB"},
|
||||
{"paris", "FR"}, {"helsinki", "FI"}, {"stockholm", "SE"},
|
||||
|
||||
@@ -25,6 +25,9 @@ func TestFromText(t *testing.T) {
|
||||
{"🇨🇿 Czech Republic Vless", "CZ"},
|
||||
{"🇳🇴 Norway TR+WS", "NO"},
|
||||
{"🇩🇪 Germany 3 HY", "DE"},
|
||||
// Panel-side typos seen in production.
|
||||
{"Румьиня", "RO"},
|
||||
{"Румыня-2", "RO"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
cc, emoji := FromText(c.in)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
// CurrentVersion is the product/semver used for update eligibility (feed "version").
|
||||
// Keep major.minor.patch only — no build suffix here.
|
||||
const CurrentVersion = "4.1.0"
|
||||
const CurrentVersion = "4.1.1"
|
||||
|
||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||
|
||||
Reference in New Issue
Block a user