ui update removing topbar, responsiveness work, console behavior, custom titlebar

This commit is contained in:
litoral05
2026-06-02 16:32:43 +01:00
parent bd8eef7f04
commit d6daac97c7
17 changed files with 1066 additions and 832 deletions
+128 -84
View File
@@ -32,6 +32,7 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
const wsRef = useRef<WebSocket | null>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
const rgbaRef = useRef<Uint8ClampedArray | null>(null);
const framebufferRef = useRef({
width: 0,
height: 0,
@@ -45,28 +46,19 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
const [frameSize, setFrameSize] = useState({ width: 0, height: 0 });
const [lastFrameAt, setLastFrameAt] = useState<string | null>(null);
const closeSocket = useCallback(() => {
const ws = wsRef.current;
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "disconnect" }));
ws.close();
} else if (ws) {
ws.close();
}
wsRef.current = null;
}, []);
const clearFrame = useCallback(() => {
const canvas = canvasRef.current;
const ctx = ctxRef.current;
if (canvas && ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
canvas.width = 0;
canvas.height = 0;
}
ctxRef.current = null;
rgbaRef.current = null;
framebufferRef.current = {
width: 0,
height: 0,
@@ -76,16 +68,44 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
setLastFrameAt(null);
}, []);
const closeSocket = useCallback(() => {
const ws = wsRef.current;
wsRef.current = null;
if (!ws) return;
try {
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "disconnect" }));
ws.close();
return;
}
if (ws.readyState === WebSocket.CONNECTING) {
ws.close();
}
} catch {
// Ignore cleanup errors.
}
}, []);
const drawFrame = useCallback((buffer: ArrayBuffer) => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas) {
return;
}
if (buffer.byteLength <= 8) return;
const view = new DataView(buffer);
const width = view.getInt32(0);
const height = view.getInt32(4);
if (!width || !height || width <= 0 || height <= 0) return;
const pixels = new Uint8ClampedArray(buffer, 8);
framebufferRef.current = {
@@ -101,10 +121,7 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
}
const ctx = ctxRef.current;
if (!ctx) {
return;
}
if (!ctx) return;
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
@@ -125,6 +142,7 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
rgba[index + 2] = pixels[index];
rgba[index + 3] = 255;
}
const imageData = ctx.createImageData(width, height);
imageData.data.set(rgba);
@@ -144,89 +162,119 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
return;
}
closeSocket();
clearFrame();
setError(null);
setState("CONNECTING_WS");
closeSocket();
window.setTimeout(() => {
const socket = new WebSocket(websocketUrl);
const socket = new WebSocket(websocketUrl);
wsRef.current = socket;
socket.binaryType = "arraybuffer";
wsRef.current = socket;
socket.binaryType = "arraybuffer";
socket.onopen = () => {
setState("CONNECTING_VNC");
socket.onopen = () => {
if (wsRef.current !== socket) return;
socket.send(
JSON.stringify({
type: "connect",
host: nextHost,
port: nextPort,
password: nextPassword,
}),
);
};
setState("CONNECTING_VNC");
socket.onmessage = (event) => {
if (typeof event.data === "string") {
const message = JSON.parse(event.data) as {
type?: string;
state?: string;
message?: string;
};
socket.send(
JSON.stringify({
type: "connect",
host: nextHost,
port: nextPort,
password: nextPassword,
}),
);
};
if (message.type === "state") {
if (message.state === "CONNECTING") {
setState("CONNECTING_VNC");
socket.onmessage = (event) => {
if (wsRef.current !== socket) return;
if (typeof event.data === "string") {
let message: {
type?: string;
state?: string;
message?: string;
};
try {
message = JSON.parse(event.data);
} catch {
return;
}
if (message.state === "CONNECTED") {
setState("CONNECTED");
return;
if (message.type === "state") {
if (message.state === "CONNECTING") {
setState("CONNECTING_VNC");
return;
}
if (message.state === "CONNECTED") {
setState("CONNECTED");
return;
}
if (message.state === "FIRST_FRAME") {
setState("FIRST_FRAME");
return;
}
if (message.state === "DISCONNECTED") {
clearFrame();
setState("DISCONNECTED");
return;
}
}
if (message.state === "FIRST_FRAME") {
setState("FIRST_FRAME");
return;
if (message.type === "error") {
clearFrame();
setError(message.message ?? "Erro VNC desconhecido.");
setState("ERROR");
}
if (message.state === "DISCONNECTED") {
setState("DISCONNECTED");
return;
}
return;
}
if (message.type === "error") {
setError(message.message ?? "Erro VNC desconhecido.");
setState("ERROR");
}
drawFrame(event.data);
};
return;
}
socket.onerror = () => {
if (wsRef.current !== socket) return;
drawFrame(event.data);
};
clearFrame();
setError("Erro na ligação WebSocket da consola VNC.");
setState("ERROR");
};
socket.onerror = () => {
setError("Erro na ligação WebSocket da consola VNC.");
setState("ERROR");
};
socket.onclose = () => {
if (wsRef.current !== socket) return;
socket.onclose = () => {
wsRef.current = null;
clearFrame();
wsRef.current = null;
clearFrame();
setState((current) =>
current === "ERROR" ? "ERROR" : "DISCONNECTED",
);
};
setState((current) =>
current === "ERROR" ? "ERROR" : "DISCONNECTED",
);
};
}, 100);
},
[closeSocket, drawFrame, host, password, port, websocketUrl],
[
clearFrame,
closeSocket,
drawFrame,
host,
password,
port,
websocketUrl,
],
);
const disconnect = useCallback(() => {
closeSocket();
clearFrame();
setError(null);
setState("DISCONNECTED");
}, [clearFrame, closeSocket]);
@@ -235,13 +283,8 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
const ws = wsRef.current;
const framebuffer = framebufferRef.current;
if (!canvas || !ws || ws.readyState !== WebSocket.OPEN) {
return;
}
if (!framebuffer.width || !framebuffer.height) {
return;
}
if (!canvas || !ws || ws.readyState !== WebSocket.OPEN) return;
if (!framebuffer.width || !framebuffer.height) return;
const rect = canvas.getBoundingClientRect();
@@ -278,8 +321,9 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
useEffect(() => {
return () => {
closeSocket();
clearFrame();
};
}, [closeSocket]);
}, [clearFrame, closeSocket]);
return {
canvasRef,
@@ -299,4 +343,4 @@ export function useVncConsole(options: UseVncConsoleOptions = {}) {
connected: state === "CONNECTED" || state === "FIRST_FRAME",
connecting: state === "CONNECTING_WS" || state === "CONNECTING_VNC",
};
}
}
+1 -1
View File
@@ -109,7 +109,7 @@ export function ConsolePage({ theme }: ConsolePageProps) {
type="button"
disabled={vnc.connecting}
onClick={() => (vnc.connected ? vnc.disconnect() : vnc.connect())}
className="mt-5 flex h-11 w-full items-center justify-center gap-2 rounded-[5px] bg-[#4FD1C5] text-sm font-black text-[#031014] transition hover:bg-[#5FE1D5] disabled:cursor-not-allowed disabled:opacity-50"
className="mt-5 flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-[5px] bg-[#4FD1C5] text-sm font-black text-[#031014] transition hover:bg-[#5FE1D5] disabled:cursor-not-allowed disabled:opacity-50"
>
<Plug className="h-4 w-4" />
{vnc.connected ? "Desligar sessão" : vnc.connecting ? "A ligar..." : "Iniciar sessão"}