Release 3.8.2.11: idle rev-cache, port-safe Connect, single-instance tray UX.
Faster unchanged polls; orphan-core cleanup on listen ports; clearer busy-port errors; cores only from binDir; incremental server list; CSP without Google Fonts; single-instance activates existing window; close-to-tray without kill-others or balloon. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -42,6 +42,8 @@
|
||||
--ms-mid: #b8860b;
|
||||
--focus-ring: rgba(13,138,102,.12);
|
||||
--theme-btn-bg: rgba(255,255,255,.7);
|
||||
--font-ui: "Segoe UI Variable Text", "Segoe UI", "SF Pro Text", "Helvetica Neue", sans-serif;
|
||||
--font-display: "Segoe UI Variable Display", "Segoe UI", "SF Pro Display", "Helvetica Neue", sans-serif;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
--ink: #e6f4ee;
|
||||
@@ -90,7 +92,7 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
font-family: Figtree, sans-serif;
|
||||
font-family: var(--font-ui);
|
||||
color: var(--ink);
|
||||
background: var(--page-bg);
|
||||
transition: color .2s var(--ease), background .25s var(--ease);
|
||||
@@ -130,7 +132,7 @@
|
||||
}
|
||||
.update-banner.show { display: block; }
|
||||
.update-banner strong {
|
||||
font-family: Sora, sans-serif;
|
||||
font-family: var(--font-display);
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: .95rem;
|
||||
@@ -199,7 +201,7 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.brand {
|
||||
font-family: Sora, sans-serif;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.7rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.045em;
|
||||
@@ -319,7 +321,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin: 0 0 8px;
|
||||
font-family: Sora, sans-serif;
|
||||
font-family: var(--font-display);
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .04em;
|
||||
@@ -528,7 +530,7 @@
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
font-size: 1.02rem;
|
||||
font-family: Sora, sans-serif;
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
|
||||
.actions { display: grid; gap: 8px; }
|
||||
@@ -749,7 +751,7 @@
|
||||
background: var(--shop-bg);
|
||||
}
|
||||
.shop h3 {
|
||||
font-family: Sora, sans-serif;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 5px;
|
||||
@@ -806,7 +808,7 @@
|
||||
color: var(--ink);
|
||||
}
|
||||
.modal h2 {
|
||||
font-family: Sora, sans-serif;
|
||||
font-family: var(--font-display);
|
||||
margin: 0 0 12px;
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: -.02em;
|
||||
|
||||
+75
-50
@@ -262,69 +262,94 @@
|
||||
});
|
||||
}
|
||||
|
||||
function pingLabel(pr) {
|
||||
if (pr && pr.ok) {
|
||||
return {
|
||||
cls: "ms " + msClass(pr.ms, true),
|
||||
text: (pr.soft ? "~" : "") + pr.ms + " ms",
|
||||
title: pr.soft ? "soft-up (UDP без ответа) — не выбирается как «Лучший»" : ""
|
||||
};
|
||||
}
|
||||
if (pr && pr.error) {
|
||||
return { cls: "ms bad", text: "—", title: pr.error };
|
||||
}
|
||||
return { cls: "ms", text: "…", title: "" };
|
||||
}
|
||||
|
||||
function ensureServerRow(p) {
|
||||
let row = serverList.querySelector('.server-row[data-name="' + CSS.escape(p.name) + '"]');
|
||||
if (row) return row;
|
||||
row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = "server-row";
|
||||
row.dataset.name = p.name;
|
||||
const left = document.createElement("div");
|
||||
left.className = "left";
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.className = "name";
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "sub";
|
||||
left.appendChild(nameEl);
|
||||
left.appendChild(sub);
|
||||
const right = document.createElement("div");
|
||||
right.className = "right";
|
||||
const proto = document.createElement("span");
|
||||
proto.className = "proto";
|
||||
const ms = document.createElement("span");
|
||||
ms.className = "ms";
|
||||
right.appendChild(proto);
|
||||
right.appendChild(ms);
|
||||
row.appendChild(left);
|
||||
row.appendChild(right);
|
||||
row.addEventListener("click", () => onServerClick(p.name));
|
||||
row.addEventListener("dblclick", () => onServerConnect(p.name));
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderServerList(activeName) {
|
||||
const active = activeName || profile.value;
|
||||
serverList.innerHTML = "";
|
||||
const all = orderedProfiles();
|
||||
const list = filteredProfiles();
|
||||
if (!all.length) {
|
||||
const keep = new Set();
|
||||
|
||||
if (!all.length || !list.length) {
|
||||
serverList.innerHTML = "";
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "server-empty";
|
||||
empty.textContent = "Нет серверов — добавьте профиль или подписку";
|
||||
serverList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
if (!list.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "server-empty";
|
||||
empty.textContent = "Ничего не найдено";
|
||||
empty.textContent = !all.length
|
||||
? "Нет серверов — добавьте профиль или подписку"
|
||||
: "Ничего не найдено";
|
||||
serverList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop empty-state nodes from prior renders.
|
||||
serverList.querySelectorAll(".server-empty").forEach((el) => el.remove());
|
||||
|
||||
list.forEach((p) => {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
keep.add(p.name);
|
||||
const row = ensureServerRow(p);
|
||||
row.className = "server-row" + (p.name === active ? " active" : "");
|
||||
row.dataset.name = p.name;
|
||||
|
||||
const left = document.createElement("div");
|
||||
left.className = "left";
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.className = "name";
|
||||
nameEl.textContent = p.name;
|
||||
const sub = document.createElement("div");
|
||||
sub.className = "sub";
|
||||
sub.textContent = p.host || "нет хоста";
|
||||
left.appendChild(nameEl);
|
||||
left.appendChild(sub);
|
||||
|
||||
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) {
|
||||
ms.className = "ms " + msClass(pr.ms, true);
|
||||
ms.textContent = (pr.soft ? "~" : "") + pr.ms + " ms";
|
||||
if (pr.soft) ms.title = "soft-up (UDP без ответа) — не выбирается как «Лучший»";
|
||||
} else if (pr && pr.error) {
|
||||
ms.className = "ms bad";
|
||||
ms.textContent = "—";
|
||||
ms.title = pr.error;
|
||||
} else {
|
||||
ms.className = "ms";
|
||||
ms.textContent = "…";
|
||||
const nameEl = row.querySelector(".name");
|
||||
const sub = row.querySelector(".sub");
|
||||
const proto = row.querySelector(".proto");
|
||||
const ms = row.querySelector(".ms");
|
||||
if (nameEl && nameEl.textContent !== p.name) nameEl.textContent = p.name;
|
||||
const hostText = p.host || "нет хоста";
|
||||
if (sub && sub.textContent !== hostText) sub.textContent = hostText;
|
||||
const protoText = (p.protocol || "?").toString();
|
||||
if (proto && proto.textContent !== protoText) proto.textContent = protoText;
|
||||
const pl = pingLabel(pingMap[p.name]);
|
||||
if (ms) {
|
||||
if (ms.className !== pl.cls) ms.className = pl.cls;
|
||||
if (ms.textContent !== pl.text) ms.textContent = pl.text;
|
||||
if ((ms.title || "") !== (pl.title || "")) ms.title = pl.title || "";
|
||||
}
|
||||
right.appendChild(proto);
|
||||
right.appendChild(ms);
|
||||
serverList.appendChild(row); // re-order if needed
|
||||
});
|
||||
|
||||
row.appendChild(left);
|
||||
row.appendChild(right);
|
||||
row.addEventListener("click", () => onServerClick(p.name));
|
||||
row.addEventListener("dblclick", () => onServerConnect(p.name));
|
||||
serverList.appendChild(row);
|
||||
serverList.querySelectorAll(".server-row").forEach((row) => {
|
||||
if (!keep.has(row.dataset.name)) row.remove();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'none';" />
|
||||
<title>Navis</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@500;600;700;800&family=Figtree:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="app.css" />
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user