Release 3.8.2.6: perceived-speed UX — search, tray hide, core warm, SSE.
Server search/hotkeys; Windows close-to-tray; background EnsureCore; SSE status with rare poll fallback. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -548,6 +548,28 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.server-toolbar .section-title { margin: 0; }
|
||||
.server-search-wrap {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.server-search-wrap input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(8, 90, 68, 0.18);
|
||||
background: var(--field-bg, rgba(255,255,255,.55));
|
||||
color: inherit;
|
||||
border-radius: 12px;
|
||||
padding: 9px 12px;
|
||||
font: inherit;
|
||||
font-size: .88rem;
|
||||
outline: none;
|
||||
}
|
||||
.server-search-wrap input:focus {
|
||||
border-color: rgba(37, 88, 75, 0.45);
|
||||
}
|
||||
[data-theme="dark"] .server-search-wrap input {
|
||||
background: rgba(0,0,0,.22);
|
||||
border-color: rgba(180, 220, 200, 0.14);
|
||||
}
|
||||
.server-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
+71
-3
@@ -51,6 +51,7 @@
|
||||
const bestBtn = $("bestBtn");
|
||||
const autoBest = $("autoBest");
|
||||
const serverList = $("serverList");
|
||||
const serverSearch = $("serverSearch");
|
||||
const updCheckBtn = $("updCheckBtn");
|
||||
const updateBanner = $("updateBanner");
|
||||
const updateBtn = $("updateBtn");
|
||||
@@ -251,17 +252,35 @@
|
||||
return list;
|
||||
}
|
||||
|
||||
function filteredProfiles() {
|
||||
const q = (serverSearch && serverSearch.value || "").trim().toLowerCase();
|
||||
const list = orderedProfiles();
|
||||
if (!q) return list;
|
||||
return list.filter((p) => {
|
||||
const hay = [p.name, p.host, p.protocol].map((x) => String(x || "").toLowerCase()).join(" ");
|
||||
return hay.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
function renderServerList(activeName) {
|
||||
const active = activeName || profile.value;
|
||||
serverList.innerHTML = "";
|
||||
const list = orderedProfiles();
|
||||
if (!list.length) {
|
||||
const all = orderedProfiles();
|
||||
const list = filteredProfiles();
|
||||
if (!all.length) {
|
||||
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 = "Ничего не найдено";
|
||||
serverList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
list.forEach((p) => {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
@@ -707,11 +726,60 @@
|
||||
setMeta(String(e), "err");
|
||||
}
|
||||
})();
|
||||
if (serverSearch) {
|
||||
serverSearch.addEventListener("input", () => renderServerList(profile.value));
|
||||
}
|
||||
|
||||
function typingInField(el) {
|
||||
if (!el) return false;
|
||||
const tag = (el.tagName || "").toLowerCase();
|
||||
return tag === "input" || tag === "textarea" || tag === "select" || el.isContentEditable;
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && modal.classList.contains("open")) {
|
||||
modal.classList.remove("open");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (typingInField(document.activeElement)) return;
|
||||
if ((e.key === "/" || (e.key === "f" && (e.ctrlKey || e.metaKey))) && serverSearch) {
|
||||
e.preventDefault();
|
||||
serverSearch.focus();
|
||||
serverSearch.select();
|
||||
return;
|
||||
}
|
||||
if ((e.key === "Enter" || e.key === " ") && !busy) {
|
||||
e.preventDefault();
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
|
||||
// Safety-net poll; primary updates come from SSE /api/events when available.
|
||||
setInterval(() => {
|
||||
if (document.hidden || busy || modal.classList.contains("open")) return;
|
||||
refresh().catch(() => {});
|
||||
}, 4000);
|
||||
}, 12000);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden && !busy) refresh({ force: true }).catch(() => {});
|
||||
});
|
||||
|
||||
(function startEventStream() {
|
||||
if (!window.__navisHttp || typeof EventSource === "undefined") return;
|
||||
const tok = window.__NAVIS_TOKEN__ || "";
|
||||
const url = "/api/events" + (tok ? ("?t=" + encodeURIComponent(tok)) : "");
|
||||
let es;
|
||||
const connectES = () => {
|
||||
try { if (es) es.close(); } catch (_) {}
|
||||
es = new EventSource(url);
|
||||
es.onmessage = () => {
|
||||
if (!busy) refresh({ force: true }).catch(() => {});
|
||||
};
|
||||
es.onerror = () => {
|
||||
try { es.close(); } catch (_) {}
|
||||
setTimeout(connectES, 2500);
|
||||
};
|
||||
};
|
||||
connectES();
|
||||
})();
|
||||
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
<button class="icon-btn danger" id="delBtn" type="button" title="Удалить" style="width:34px;height:34px;font-size:1rem">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-search-wrap">
|
||||
<input id="serverSearch" type="search" placeholder="Поиск по имени, хосту, протоколу…" spellcheck="false" autocomplete="off" />
|
||||
</div>
|
||||
<label class="auto-best">
|
||||
<input id="autoBest" type="checkbox" />
|
||||
Авто: пинг + подключение к лучшему
|
||||
|
||||
Reference in New Issue
Block a user