Release 4.2.4+1: group same-host profiles into one server row with protocol chips (VLESS · Trojan · Hysteria2) instead of «сервер 2/3»; оптимальный among distinct hosts. Windows 4.2.4.1.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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_023_001
|
versionCode = 4_024_001
|
||||||
versionName = "4.2.3+1"
|
versionName = "4.2.4+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")
|
||||||
|
|||||||
Vendored
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "4.2.3",
|
"version": "4.2.4",
|
||||||
"notes": "4.2.3: на вкладке «Защита» только карточка EvilFox — убраны NordVPN/ExpressVPN/Surfshark и их ссылки",
|
"notes": "4.2.4: группировка серверов по хосту — один физический сервер с протоколами VLESS · Trojan · Hysteria2 вместо «сервер 2/3»; «оптимальный» среди разных хостов",
|
||||||
"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": "a39c2b3f26d4342409e28d9106d4ced2e63eec9e4a5968de7c61d16b5f72b639",
|
"sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"android": {
|
"android": {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"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": "a39c2b3f26d4342409e28d9106d4ced2e63eec9e4a5968de7c61d16b5f72b639",
|
"sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046",
|
||||||
"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,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "4.2.3",
|
"version": "4.2.4",
|
||||||
"notes": "4.2.3: на вкладке «Защита» только карточка EvilFox — убраны NordVPN/ExpressVPN/Surfshark и их ссылки",
|
"notes": "4.2.4: группировка серверов по хосту — один физический сервер с протоколами VLESS · Trojan · Hysteria2 вместо «сервер 2/3»; «оптимальный» среди разных хостов",
|
||||||
"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": "a39c2b3f26d4342409e28d9106d4ced2e63eec9e4a5968de7c61d16b5f72b639",
|
"sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"android": {
|
"android": {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"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": "a39c2b3f26d4342409e28d9106d4ced2e63eec9e4a5968de7c61d16b5f72b639",
|
"sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
}
|
}
|
||||||
|
|||||||
+261
-43
@@ -874,6 +874,33 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
.server-row .protos {
|
||||||
|
margin-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.server-row .proto-tag {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: .62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
color: var(--muted);
|
||||||
|
background: var(--meta-bg);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.35;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.server-row .proto-tag:hover { border-color: rgba(8,145,178,.45); color: var(--text); }
|
||||||
|
.server-row .proto-tag.selected {
|
||||||
|
color: var(--accent-deep);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
border-color: rgba(8,145,178,.35);
|
||||||
|
}
|
||||||
.server-row .right { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
|
.server-row .right { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
|
||||||
/* Протокол — крошечная буква-иконка с подсказкой (не повторяем «vless» под каждой строкой). */
|
/* Протокол — крошечная буква-иконка с подсказкой (не повторяем «vless» под каждой строкой). */
|
||||||
.proto-badge {
|
.proto-badge {
|
||||||
@@ -2281,35 +2308,168 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Человекочитаемые имена серверов («Чехия — оптимальный сервер», «Германия — сервер 2»).
|
// Человекочитаемые имена серверов («Чехия — оптимальный сервер», «Германия — сервер 2»).
|
||||||
// Реальный host:port показываем только в свойствах сервера и в журнале.
|
// Профили с одним физическим хостом (IP или hostname) группируются в одну строку;
|
||||||
// ISO всегда через profileISO (бэкенд + JS-алиасы опечаток) → countryRu, никогда сырое имя панели.
|
// протоколы (VLESS / Trojan / Hysteria2…) показываются чипами, а не «сервер 2/3».
|
||||||
|
// Реальный host:port — только в свойствах сервера и в журнале.
|
||||||
|
// ISO всегда через profileISO → countryRu, никогда сырое имя панели.
|
||||||
let friendlyMap = {};
|
let friendlyMap = {};
|
||||||
|
|
||||||
|
function isLiteralIP(host) {
|
||||||
|
const h = String(host || "").replace(/^\[|\]$/g, "");
|
||||||
|
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(h)) return true;
|
||||||
|
if (h.includes(":") && /^[0-9a-fA-F:.]+$/.test(h)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хост без схемы/userinfo/порта — ключ группировки «один физический сервер».
|
||||||
|
function normalizeEndpointHost(raw) {
|
||||||
|
let s = String(raw || "").trim();
|
||||||
|
if (!s) return "";
|
||||||
|
const lower = s.toLowerCase();
|
||||||
|
if (lower.includes("[interface]")) {
|
||||||
|
for (const line of s.split(/\r?\n/)) {
|
||||||
|
const t = line.trim();
|
||||||
|
if (/^endpoint\s*[=:]/i.test(t)) {
|
||||||
|
s = t.replace(/^endpoint\s*[=:]\s*/i, "").trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const scheme = s.indexOf("://");
|
||||||
|
if (scheme >= 0) s = s.slice(scheme + 3);
|
||||||
|
const at = s.lastIndexOf("@");
|
||||||
|
if (at >= 0) s = s.slice(at + 1);
|
||||||
|
const cut = s.search(/[/?#]/);
|
||||||
|
if (cut >= 0) s = s.slice(0, cut);
|
||||||
|
s = s.trim();
|
||||||
|
let host = s;
|
||||||
|
if (s.startsWith("[")) {
|
||||||
|
const end = s.indexOf("]");
|
||||||
|
if (end >= 0) host = s.slice(1, end);
|
||||||
|
} else {
|
||||||
|
const col = s.lastIndexOf(":");
|
||||||
|
// Один «:» → host:port; несколько → скорее IPv6 без скобок.
|
||||||
|
if (col > 0 && s.indexOf(":") === col) host = s.slice(0, col);
|
||||||
|
}
|
||||||
|
return host.replace(/^\[|\]$/g, "").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ключ группы: ip:<addr> если известен IP (пинг или литерал в URI), иначе host:<hostname>.
|
||||||
|
// Порт в ключ не входит — варианты протоколов на разных портах одного хоста = один сервер.
|
||||||
|
function profileEndpointKey(p) {
|
||||||
|
if (!p) return "";
|
||||||
|
const pr = pingMap[p.name];
|
||||||
|
if (pr && pr.host) {
|
||||||
|
const ph = String(pr.host).replace(/^\[|\]$/g, "").toLowerCase();
|
||||||
|
if (isLiteralIP(ph)) return "ip:" + ph;
|
||||||
|
}
|
||||||
|
const raw = (p.host && String(p.host).trim()) || (p.proxy && String(p.proxy).trim()) || "";
|
||||||
|
const host = normalizeEndpointHost(raw);
|
||||||
|
if (!host) return "id:" + String(p.name || "");
|
||||||
|
if (isLiteralIP(host)) return "ip:" + host;
|
||||||
|
return "host:" + host;
|
||||||
|
}
|
||||||
|
|
||||||
|
function protoPretty(proto) {
|
||||||
|
const p = String(proto || "").toLowerCase();
|
||||||
|
const map = {
|
||||||
|
vless: "VLESS", vmess: "VMess", trojan: "Trojan",
|
||||||
|
hysteria2: "Hysteria2", hy2: "Hysteria2",
|
||||||
|
naive: "Naive", awg: "AWG"
|
||||||
|
};
|
||||||
|
if (map[p]) return map[p];
|
||||||
|
return p ? (p.charAt(0).toUpperCase() + p.slice(1)) : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueProtos(members) {
|
||||||
|
const seen = {};
|
||||||
|
const out = [];
|
||||||
|
(members || []).forEach((m) => {
|
||||||
|
const raw = String(m.protocol || "").toLowerCase() || "?";
|
||||||
|
const label = protoPretty(raw);
|
||||||
|
if (seen[label]) return;
|
||||||
|
seen[label] = true;
|
||||||
|
out.push({ raw: raw, label: label, name: m.name });
|
||||||
|
});
|
||||||
|
out.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBestPing(members) {
|
||||||
|
let best = null;
|
||||||
|
(members || []).forEach((m) => {
|
||||||
|
const pr = pingMap[m.name];
|
||||||
|
if (!pr) return;
|
||||||
|
if (pr.ok) {
|
||||||
|
if (!best || !best.ok || pr.ms < best.ms) best = pr;
|
||||||
|
} else if (!best) {
|
||||||
|
best = pr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickGroupProfile(members, preferredName) {
|
||||||
|
const list = members || [];
|
||||||
|
if (!list.length) return null;
|
||||||
|
if (preferredName && list.some((m) => m.name === preferredName)) {
|
||||||
|
return list.find((m) => m.name === preferredName);
|
||||||
|
}
|
||||||
|
let best = list[0], bestMs = Infinity;
|
||||||
|
list.forEach((m) => {
|
||||||
|
const pr = pingMap[m.name];
|
||||||
|
if (pr && pr.ok && pr.ms < bestMs) { bestMs = pr.ms; best = m; }
|
||||||
|
});
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
function rebuildFriendlyNames() {
|
function rebuildFriendlyNames() {
|
||||||
friendlyMap = {};
|
friendlyMap = {};
|
||||||
const groups = {};
|
const byCountry = {};
|
||||||
profiles.forEach((p) => {
|
profiles.forEach((p) => {
|
||||||
const key = profileISO(p) || "__none__";
|
const iso = profileISO(p) || "__none__";
|
||||||
(groups[key] = groups[key] || []).push(p);
|
const ek = profileEndpointKey(p);
|
||||||
|
if (!byCountry[iso]) byCountry[iso] = {};
|
||||||
|
(byCountry[iso][ek] = byCountry[iso][ek] || []).push(p);
|
||||||
});
|
});
|
||||||
Object.keys(groups).forEach((key) => {
|
Object.keys(byCountry).forEach((iso) => {
|
||||||
const list = groups[key].slice().sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
const hostMap = byCountry[iso];
|
||||||
// «Оптимальный» — сервер с лучшим измеренным пингом в стране, иначе первый по имени.
|
const hostGroups = Object.keys(hostMap).map((ek) => {
|
||||||
let bestIdx = 0, bestMsVal = Infinity;
|
const list = hostMap[ek].slice().sort((a, b) => String(a.name).localeCompare(String(b.name)));
|
||||||
list.forEach((p, i) => {
|
let bestMs = Infinity;
|
||||||
|
list.forEach((p) => {
|
||||||
const pr = pingMap[p.name];
|
const pr = pingMap[p.name];
|
||||||
if (pr && pr.ok && pr.ms < bestMsVal) { bestMsVal = pr.ms; bestIdx = i; }
|
if (pr && pr.ok && pr.ms < bestMs) bestMs = pr.ms;
|
||||||
});
|
});
|
||||||
const region = key === "__none__" ? "" : countryRu(key);
|
return { ek: ek, list: list, bestMs: bestMs };
|
||||||
let n = 0;
|
});
|
||||||
list.forEach((p, i) => {
|
// Стабильная нумерация по имени; «оптимальный» — лучший пинг среди РАЗНЫХ хостов.
|
||||||
n += 1;
|
hostGroups.sort((a, b) => String(a.list[0].name).localeCompare(String(b.list[0].name)));
|
||||||
|
let bestIdx = 0, bestMsVal = Infinity;
|
||||||
|
hostGroups.forEach((g, i) => {
|
||||||
|
if (g.bestMs < bestMsVal) { bestMsVal = g.bestMs; bestIdx = i; }
|
||||||
|
});
|
||||||
|
const region = iso === "__none__" ? "" : countryRu(iso);
|
||||||
|
hostGroups.forEach((g, i) => {
|
||||||
|
const n = i + 1;
|
||||||
let title;
|
let title;
|
||||||
if (i === bestIdx) {
|
if (i === bestIdx) {
|
||||||
title = region ? (region + " — оптимальный сервер") : ("Сервер " + n + " — оптимальный");
|
title = region ? (region + " — оптимальный сервер") : ("Сервер " + n + " — оптимальный");
|
||||||
} else {
|
} else {
|
||||||
title = region ? (region + " — сервер " + n) : ("Сервер " + n);
|
title = region ? (region + " — сервер " + n) : ("Сервер " + n);
|
||||||
}
|
}
|
||||||
friendlyMap[p.name] = { title: title, region: region, iso: key === "__none__" ? "" : key };
|
const protos = uniqueProtos(g.list);
|
||||||
|
const groupNames = g.list.map((x) => x.name);
|
||||||
|
g.list.forEach((p) => {
|
||||||
|
friendlyMap[p.name] = {
|
||||||
|
title: title,
|
||||||
|
region: region,
|
||||||
|
iso: iso === "__none__" ? "" : iso,
|
||||||
|
endpointKey: g.ek,
|
||||||
|
protocols: protos,
|
||||||
|
groupNames: groupNames
|
||||||
|
};
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2341,16 +2501,38 @@
|
|||||||
renderCurServer(want);
|
renderCurServer(want);
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderedProfiles() {
|
// Группы «один хост = один сервер» для списка; сортировка по лучшему пингу группы.
|
||||||
const list = profiles.slice();
|
function orderedServerGroups() {
|
||||||
list.sort((a, b) => {
|
const seen = {};
|
||||||
const pa = pingMap[a.name], pb = pingMap[b.name];
|
const groups = [];
|
||||||
|
profiles.forEach((p) => {
|
||||||
|
const f = friendlyMap[p.name];
|
||||||
|
const ek = (f && f.endpointKey) || profileEndpointKey(p);
|
||||||
|
const iso = (f && f.iso) || profileISO(p) || "__none__";
|
||||||
|
const id = iso + "|" + ek;
|
||||||
|
if (seen[id]) return;
|
||||||
|
seen[id] = true;
|
||||||
|
const names = (f && f.groupNames) || [p.name];
|
||||||
|
const members = names.map((n) => profiles.find((x) => x.name === n)).filter(Boolean);
|
||||||
|
if (!members.length) return;
|
||||||
|
groups.push({
|
||||||
|
id: id,
|
||||||
|
endpointKey: ek,
|
||||||
|
iso: iso === "__none__" ? "" : iso,
|
||||||
|
members: members,
|
||||||
|
title: friendlyTitle(members[0].name),
|
||||||
|
protocols: (f && f.protocols) || uniqueProtos(members),
|
||||||
|
ping: groupBestPing(members)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
groups.sort((a, b) => {
|
||||||
|
const pa = a.ping, pb = b.ping;
|
||||||
const oa = pa && pa.ok, ob = pb && pb.ok;
|
const oa = pa && pa.ok, ob = pb && pb.ok;
|
||||||
if (oa !== ob) return oa ? -1 : 1;
|
if (oa !== ob) return oa ? -1 : 1;
|
||||||
if (oa && ob && pa.ms !== pb.ms) return pa.ms - pb.ms;
|
if (oa && ob && pa.ms !== pb.ms) return pa.ms - pb.ms;
|
||||||
return String(a.name).localeCompare(String(b.name));
|
return String(a.title).localeCompare(String(b.title));
|
||||||
});
|
});
|
||||||
return list;
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderCurServer(activeName) {
|
function renderCurServer(activeName) {
|
||||||
@@ -2367,8 +2549,12 @@
|
|||||||
setFlagEl($("curFlag"), p);
|
setFlagEl($("curFlag"), p);
|
||||||
$("curName").textContent = friendlyTitle(p.name);
|
$("curName").textContent = friendlyTitle(p.name);
|
||||||
$("curHost").textContent = "нажмите, чтобы выбрать другой сервер";
|
$("curHost").textContent = "нажмите, чтобы выбрать другой сервер";
|
||||||
$("curProto").textContent = (p.protocol || "?").toString();
|
$("curProto").textContent = protoPretty(p.protocol);
|
||||||
const pr = pingMap[p.name];
|
const f = friendlyMap[p.name];
|
||||||
|
const members = (f && f.groupNames)
|
||||||
|
? f.groupNames.map((n) => profiles.find((x) => x.name === n)).filter(Boolean)
|
||||||
|
: [p];
|
||||||
|
const pr = groupBestPing(members) || pingMap[p.name];
|
||||||
const el = $("curPing");
|
const el = $("curPing");
|
||||||
if (pr && pr.ok) {
|
if (pr && pr.ok) {
|
||||||
el.className = "ms " + msClass(pr.ms, true);
|
el.className = "ms " + msClass(pr.ms, true);
|
||||||
@@ -2389,8 +2575,8 @@
|
|||||||
function renderServerList(activeName) {
|
function renderServerList(activeName) {
|
||||||
const active = activeName || profile.value;
|
const active = activeName || profile.value;
|
||||||
serverList.innerHTML = "";
|
serverList.innerHTML = "";
|
||||||
const list = orderedProfiles();
|
const list = orderedServerGroups();
|
||||||
// Количество серверов — только здесь, мелкой приглушённой подписью.
|
// Количество серверов — только здесь, мелкой приглушённой подписью (группы, не профили).
|
||||||
const srvCount = $("srvCount");
|
const srvCount = $("srvCount");
|
||||||
if (srvCount) {
|
if (srvCount) {
|
||||||
srvCount.textContent = list.length
|
srvCount.textContent = list.length
|
||||||
@@ -2404,25 +2590,26 @@
|
|||||||
serverList.appendChild(empty);
|
serverList.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.forEach((p) => {
|
list.forEach((g) => {
|
||||||
const isActive = p.name === active;
|
const selected = pickGroupProfile(g.members, active);
|
||||||
|
const isActive = g.members.some((m) => m.name === active);
|
||||||
const row = document.createElement("button");
|
const row = document.createElement("button");
|
||||||
row.type = "button";
|
row.type = "button";
|
||||||
row.className = "server-row" + (isActive ? " active" : "");
|
row.className = "server-row" + (isActive ? " active" : "");
|
||||||
row.dataset.name = p.name;
|
row.dataset.name = selected ? selected.name : g.members[0].name;
|
||||||
|
|
||||||
const left = document.createElement("div");
|
const left = document.createElement("div");
|
||||||
left.className = "left";
|
left.className = "left";
|
||||||
const flag = document.createElement("span");
|
const flag = document.createElement("span");
|
||||||
flag.className = "flag";
|
flag.className = "flag";
|
||||||
setFlagEl(flag, p);
|
setFlagEl(flag, selected || g.members[0]);
|
||||||
const metaCol = document.createElement("div");
|
const metaCol = document.createElement("div");
|
||||||
metaCol.className = "meta-col";
|
metaCol.className = "meta-col";
|
||||||
const nameRow = document.createElement("div");
|
const nameRow = document.createElement("div");
|
||||||
nameRow.className = "name-row";
|
nameRow.className = "name-row";
|
||||||
const nameEl = document.createElement("div");
|
const nameEl = document.createElement("div");
|
||||||
nameEl.className = "name";
|
nameEl.className = "name";
|
||||||
nameEl.textContent = friendlyTitle(p.name);
|
nameEl.textContent = g.title;
|
||||||
nameRow.appendChild(nameEl);
|
nameRow.appendChild(nameEl);
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
@@ -2431,14 +2618,43 @@
|
|||||||
nameRow.appendChild(badge);
|
nameRow.appendChild(badge);
|
||||||
}
|
}
|
||||||
metaCol.appendChild(nameRow);
|
metaCol.appendChild(nameRow);
|
||||||
|
if (g.protocols && g.protocols.length) {
|
||||||
|
const protos = document.createElement("div");
|
||||||
|
protos.className = "protos";
|
||||||
|
g.protocols.forEach((pr) => {
|
||||||
|
// span, не button — строка сервера уже <button>, вложенные button недопустимы.
|
||||||
|
const tag = document.createElement("span");
|
||||||
|
tag.className = "proto-tag" + (isActive && active === pr.name ? " selected" : "");
|
||||||
|
tag.textContent = pr.label;
|
||||||
|
tag.title = "Протокол " + pr.label;
|
||||||
|
tag.setAttribute("role", "button");
|
||||||
|
tag.tabIndex = 0;
|
||||||
|
tag.addEventListener("click", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onServerClick(pr.name);
|
||||||
|
});
|
||||||
|
tag.addEventListener("dblclick", (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onServerConnect(pr.name);
|
||||||
|
});
|
||||||
|
tag.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onServerClick(pr.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
protos.appendChild(tag);
|
||||||
|
});
|
||||||
|
metaCol.appendChild(protos);
|
||||||
|
}
|
||||||
left.appendChild(flag);
|
left.appendChild(flag);
|
||||||
left.appendChild(metaCol);
|
left.appendChild(metaCol);
|
||||||
|
|
||||||
const right = document.createElement("div");
|
const right = document.createElement("div");
|
||||||
right.className = "right";
|
right.className = "right";
|
||||||
// Протокол — только на карточке активного сервера / в «Свойствах», не в строках списка.
|
|
||||||
const ms = document.createElement("span");
|
const ms = document.createElement("span");
|
||||||
const pr = pingMap[p.name];
|
const pr = g.ping;
|
||||||
if (pr && pr.ok) {
|
if (pr && pr.ok) {
|
||||||
ms.className = "ms " + msClass(pr.ms, true);
|
ms.className = "ms " + msClass(pr.ms, true);
|
||||||
ms.textContent = pr.ms + " ms";
|
ms.textContent = pr.ms + " ms";
|
||||||
@@ -2452,7 +2668,7 @@
|
|||||||
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";
|
||||||
@@ -2460,27 +2676,30 @@
|
|||||||
pingOne.title = "Проверить пинг этого сервера";
|
pingOne.title = "Проверить пинг этого сервера";
|
||||||
pingOne.addEventListener("click", (e) => {
|
pingOne.addEventListener("click", (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
runPingOne(p.name, pingOne, ms);
|
runPingGroup(g.members, pingOne, ms);
|
||||||
});
|
});
|
||||||
right.appendChild(pingOne);
|
right.appendChild(pingOne);
|
||||||
|
|
||||||
row.appendChild(left);
|
row.appendChild(left);
|
||||||
row.appendChild(right);
|
row.appendChild(right);
|
||||||
row.addEventListener("click", () => onServerClick(p.name));
|
const rowName = selected ? selected.name : g.members[0].name;
|
||||||
row.addEventListener("dblclick", () => onServerConnect(p.name));
|
row.addEventListener("click", () => onServerClick(rowName));
|
||||||
|
row.addEventListener("dblclick", () => onServerConnect(rowName));
|
||||||
serverList.appendChild(row);
|
serverList.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Пинг одного сервера по кнопке [📊] в строке.
|
// Пинг всех профилей группы (один хост, разные протоколы) по кнопке [📊].
|
||||||
async function runPingOne(name, btnEl, msEl) {
|
async function runPingGroup(members, btnEl, msEl) {
|
||||||
if (btnEl.disabled) return;
|
if (btnEl.disabled) return;
|
||||||
btnEl.disabled = true;
|
btnEl.disabled = true;
|
||||||
const prevText = msEl.textContent;
|
const prevText = msEl.textContent;
|
||||||
msEl.textContent = "…";
|
msEl.textContent = "…";
|
||||||
try {
|
try {
|
||||||
const res = await pingServer(name);
|
for (const m of members || []) {
|
||||||
if (res) pingMap[name] = res;
|
const res = await pingServer(m.name);
|
||||||
|
if (res) pingMap[m.name] = res;
|
||||||
|
}
|
||||||
rebuildFriendlyNames();
|
rebuildFriendlyNames();
|
||||||
renderServerList(profile.value);
|
renderServerList(profile.value);
|
||||||
renderCurServer(profile.value);
|
renderCurServer(profile.value);
|
||||||
@@ -2845,8 +3064,7 @@
|
|||||||
else pingEl.textContent = "не измерен";
|
else pingEl.textContent = "не измерен";
|
||||||
}
|
}
|
||||||
if (protoEl) {
|
if (protoEl) {
|
||||||
const proto = String(p.protocol || "—");
|
protoEl.textContent = p.protocol ? protoPretty(p.protocol) : "—";
|
||||||
protoEl.textContent = proto.charAt(0).toUpperCase() + proto.slice(1);
|
|
||||||
}
|
}
|
||||||
if (stEl) stEl.textContent = connected ? "🟢 Активен" : "⚪ Не активен";
|
if (stEl) stEl.textContent = connected ? "🟢 Активен" : "⚪ Не активен";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.3"
|
const CurrentVersion = "4.2.4"
|
||||||
|
|
||||||
// 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.2.3.1"
|
Version="4.2.4.1"
|
||||||
ProcessorArchitecture="x64" />
|
ProcessorArchitecture="x64" />
|
||||||
|
|
||||||
<Properties>
|
<Properties>
|
||||||
|
|||||||
+4
-4
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "4.2.3",
|
"version": "4.2.4",
|
||||||
"notes": "4.2.3: на вкладке «Защита» только карточка EvilFox — убраны NordVPN/ExpressVPN/Surfshark и их ссылки",
|
"notes": "4.2.4: группировка серверов по хосту — один физический сервер с протоколами VLESS · Trojan · Hysteria2 вместо «сервер 2/3»; «оптимальный» среди разных хостов",
|
||||||
"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": "a39c2b3f26d4342409e28d9106d4ced2e63eec9e4a5968de7c61d16b5f72b639",
|
"sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"android": {
|
"android": {
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
},
|
},
|
||||||
"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": "a39c2b3f26d4342409e28d9106d4ced2e63eec9e4a5968de7c61d16b5f72b639",
|
"sha256": "3cebbdf3ea5e4bdac251e9fe18efbde59dd2c425ad57b0909b879a58dbd1a046",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"FixedFileInfo": {
|
"FixedFileInfo": {
|
||||||
"FileVersion": { "Major": 4, "Minor": 2, "Patch": 3, "Build": 1 },
|
"FileVersion": { "Major": 4, "Minor": 2, "Patch": 4, "Build": 1 },
|
||||||
"ProductVersion": { "Major": 4, "Minor": 2, "Patch": 3, "Build": 1 },
|
"ProductVersion": { "Major": 4, "Minor": 2, "Patch": 4, "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.3.1",
|
"FileVersion": "4.2.4.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.3.1",
|
"ProductVersion": "4.2.4.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