fix thumbnails
This commit is contained in:
@@ -136,12 +136,12 @@ func setStretchBltMode(hdc uintptr, mode int32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func bitBltWithFallback(hdcMem, hdcScreen uintptr, bounds image.Rectangle, w, h int) bool {
|
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 {
|
func stretchBltCapture(hdcMem, hdcScreen uintptr, bounds image.Rectangle, srcW, srcH, dstW, dstH int) bool {
|
||||||
setStretchBltMode(hdcMem, COLORONCOLOR)
|
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
|
var dpiAwareOnce sync.Once
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
monitorsBadge,
|
monitorsBadge,
|
||||||
shortId,
|
shortId,
|
||||||
} from "./viewUtils.js";
|
} from "./viewUtils.js";
|
||||||
|
import { applyImageSrcSmooth } from "./thumbnail-loader.js";
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement("div");
|
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}`;
|
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 = {}) {
|
function renderMerge(data, options = {}) {
|
||||||
setupGridDelegation();
|
setupGridDelegation();
|
||||||
ensureLayoutScaffold();
|
ensureLayoutScaffold();
|
||||||
@@ -463,9 +481,18 @@ export function createRenderer({
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
if (existing.classList.contains("card-uninstalling")) return;
|
if (existing.classList.contains("card-uninstalling")) return;
|
||||||
const digest = cardDigest(client);
|
const digest = cardDigest(client);
|
||||||
if (existing._cardDigest === digest) return;
|
const thumbDigest = cardThumbDigest(client);
|
||||||
|
const structureChanged = existing._cardDigest !== digest;
|
||||||
|
const thumbChanged = existing._thumbDigest !== thumbDigest;
|
||||||
|
if (!structureChanged && !thumbChanged) return;
|
||||||
|
if (structureChanged) {
|
||||||
existing._cardDigest = digest;
|
existing._cardDigest = digest;
|
||||||
|
existing._thumbDigest = thumbDigest;
|
||||||
updateCard(existing, client);
|
updateCard(existing, client);
|
||||||
|
} else {
|
||||||
|
existing._thumbDigest = thumbDigest;
|
||||||
|
softThumbUpdate(existing, client);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
newClients.push(client);
|
newClients.push(client);
|
||||||
@@ -550,6 +577,7 @@ export function createRenderer({
|
|||||||
: buildRowA(client, options);
|
: buildRowA(client, options);
|
||||||
|
|
||||||
node._cardDigest = cardDigest(client);
|
node._cardDigest = cardDigest(client);
|
||||||
|
node._thumbDigest = cardThumbDigest(client);
|
||||||
|
|
||||||
if (options.animate && currentLayout !== "table") {
|
if (options.animate && currentLayout !== "table") {
|
||||||
node.classList.add("card-animate");
|
node.classList.add("card-animate");
|
||||||
|
|||||||
@@ -794,7 +794,7 @@ if (window.CodeMirror && scriptEditor) {
|
|||||||
tabSize: 2,
|
tabSize: 2,
|
||||||
lineWrapping: true,
|
lineWrapping: true,
|
||||||
});
|
});
|
||||||
editorInstance.setSize(null, "100%");
|
editorInstance.setSize(null, "24rem");
|
||||||
window._vbCodeMirror = editorInstance;
|
window._vbCodeMirror = editorInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +803,36 @@ const modeToggleVisual = document.getElementById("mode-toggle-visual");
|
|||||||
const codeEditorSection = document.getElementById("code-editor-section");
|
const codeEditorSection = document.getElementById("code-editor-section");
|
||||||
const visualBuilderSection = document.getElementById("visual-builder-section");
|
const visualBuilderSection = document.getElementById("visual-builder-section");
|
||||||
const rightColumn = codeEditorSection?.parentElement?.querySelector(".lg\\:col-span-1");
|
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;
|
let visualBuilderInited = false;
|
||||||
|
|
||||||
function setMode(mode) {
|
function setMode(mode) {
|
||||||
@@ -825,7 +855,10 @@ function setMode(mode) {
|
|||||||
visualBuilderSection?.classList.add("hidden");
|
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";
|
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";
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,24 @@ function buildThumbnailUrl(clientId, version) {
|
|||||||
return version > 0 ? `${base}?v=${version}` : base;
|
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 {
|
export class ThumbnailLoader {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
this.refreshIntervalMs = Math.max(500, Number(options.refreshIntervalMs) || DEFAULT_REFRESH_MS);
|
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]");
|
const img = element.querySelector("img[data-thumb-img]");
|
||||||
if (!img) return;
|
if (!img) return;
|
||||||
const url = buildThumbnailUrl(rec.clientId, rec.version);
|
const url = buildThumbnailUrl(rec.clientId, rec.version);
|
||||||
if (img.dataset.thumbUrl !== url) {
|
applyImageSrcSmooth(img, url);
|
||||||
img.dataset.thumbUrl = url;
|
|
||||||
img.src = url;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleIntersection(entries) {
|
handleIntersection(entries) {
|
||||||
|
|||||||
@@ -94,8 +94,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Script Editor -->
|
<!-- Script Editor -->
|
||||||
<div id="code-editor-section" class="lg:col-span-2">
|
<div id="code-editor-section" class="lg:col-span-2 min-h-0">
|
||||||
<div class="bg-slate-900/70 rounded-xl border border-slate-800 p-6 h-full flex flex-col">
|
<div class="bg-slate-900/70 rounded-xl border border-slate-800 p-6 h-full flex flex-col overflow-hidden">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-xl font-semibold">Script Editor</h2>
|
<h2 class="text-xl font-semibold">Script Editor</h2>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -119,14 +119,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id="script-editor"
|
id="script-editor"
|
||||||
class="w-full flex-1 min-h-[24rem] px-4 py-3 rounded-lg border border-slate-700 bg-slate-950 text-slate-100 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
class="w-full flex-1 min-h-[24rem] px-4 py-3 rounded-lg border border-slate-700 bg-slate-950 text-slate-100 font-mono text-sm resize-none overflow-auto focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||||
placeholder="Enter your script here..."
|
placeholder="Enter your script here..."
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Saved Scripts + Templates (right column) -->
|
<!-- Saved Scripts + Templates (right column) -->
|
||||||
<div class="lg:col-span-1">
|
<div class="lg:col-span-1 self-start">
|
||||||
<div class="bg-slate-900/70 rounded-xl border border-slate-800 p-6 mb-6">
|
<div class="bg-slate-900/70 rounded-xl border border-slate-800 p-6 mb-6">
|
||||||
<h2 class="text-xl font-semibold mb-4">Saved Scripts</h2>
|
<h2 class="text-xl font-semibold mb-4">Saved Scripts</h2>
|
||||||
<div class="flex gap-2 mb-3">
|
<div class="flex gap-2 mb-3">
|
||||||
|
|||||||
@@ -253,9 +253,10 @@ export async function handleClientRoutes(
|
|||||||
if (!canUserAccessClient(user.userId, user.role, clientId)) {
|
if (!canUserAccessClient(user.userId, user.role, clientId)) {
|
||||||
return new Response("Forbidden: Client access denied", { status: 403 });
|
return new Response("Forbidden: Client access denied", { status: 403 });
|
||||||
}
|
}
|
||||||
const { generateThumbnail, markThumbnailRequested, getThumbnailVersion } = await import("../../thumbnails");
|
const { requestThumbnailRegen, markThumbnailRequested, getThumbnailVersion, waitForThumbnail, clearThumbnailRequest } = await import("../../thumbnails");
|
||||||
markThumbnailRequested(clientId);
|
markThumbnailRequested(clientId);
|
||||||
const target = clientManager.getClient(clientId);
|
const target = clientManager.getClient(clientId);
|
||||||
|
const beforeVersion = getThumbnailVersion(clientId);
|
||||||
if (target?.online) {
|
if (target?.online) {
|
||||||
const commandId = uuidv4();
|
const commandId = uuidv4();
|
||||||
target.ws.send(
|
target.ws.send(
|
||||||
@@ -268,10 +269,16 @@ export async function handleClientRoutes(
|
|||||||
);
|
);
|
||||||
metrics.recordCommand("screenshot");
|
metrics.recordCommand("screenshot");
|
||||||
}
|
}
|
||||||
const success = await generateThumbnail(clientId);
|
const fresh = target?.online ? await waitForThumbnail(clientId, 2500) : false;
|
||||||
|
if (fresh) {
|
||||||
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
|
} else {
|
||||||
|
await requestThumbnailRegen(clientId);
|
||||||
|
}
|
||||||
|
clearThumbnailRequest(clientId);
|
||||||
const version = getThumbnailVersion(clientId);
|
const version = getThumbnailVersion(clientId);
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{ ok: true, updated: success, version },
|
{ ok: true, updated: version > beforeVersion, version },
|
||||||
{ headers: deps.CORS_HEADERS },
|
{ headers: deps.CORS_HEADERS },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,9 @@ export async function generateThumbnail(id: string): Promise<boolean> {
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
evictThumbnailsIfFull();
|
evictThumbnailsIfFull();
|
||||||
|
if (latestFrames.get(id) === frameData) {
|
||||||
latestFrames.delete(id);
|
latestFrames.delete(id);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[thumbnails] Failed to generate thumbnail for client ${id}:`, err);
|
console.error(`[thumbnails] Failed to generate thumbnail for client ${id}:`, err);
|
||||||
@@ -136,10 +138,54 @@ export async function generateThumbnail(id: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const thumbnailGenState = new Map<string, { inFlight: boolean; pending: boolean }>();
|
||||||
|
|
||||||
|
export async function requestThumbnailRegen(id: string): Promise<boolean> {
|
||||||
|
let state = thumbnailGenState.get(id);
|
||||||
|
if (!state) {
|
||||||
|
state = { inFlight: false, pending: false };
|
||||||
|
thumbnailGenState.set(id, state);
|
||||||
|
}
|
||||||
|
if (state.inFlight) {
|
||||||
|
state.pending = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.inFlight = true;
|
||||||
|
let didGenerate = false;
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
state.pending = false;
|
||||||
|
const ok = await generateThumbnail(id);
|
||||||
|
if (ok) didGenerate = true;
|
||||||
|
if (!state.pending) break;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
state.inFlight = false;
|
||||||
|
if (thumbnailGenState.get(id) === state && !state.pending) {
|
||||||
|
thumbnailGenState.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return didGenerate;
|
||||||
|
}
|
||||||
|
|
||||||
export function markThumbnailRequested(id: string) {
|
export function markThumbnailRequested(id: string) {
|
||||||
thumbnailRequests.set(id, Date.now());
|
thumbnailRequests.set(id, Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isThumbnailRequested(id: string, windowMs = 5000): boolean {
|
||||||
|
const ts = thumbnailRequests.get(id);
|
||||||
|
if (!ts) return false;
|
||||||
|
if (Date.now() - ts > windowMs) {
|
||||||
|
thumbnailRequests.delete(id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearThumbnailRequest(id: string) {
|
||||||
|
thumbnailRequests.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
export function consumeThumbnailRequest(id: string, windowMs = 5000): boolean {
|
export function consumeThumbnailRequest(id: string, windowMs = 5000): boolean {
|
||||||
const ts = thumbnailRequests.get(id);
|
const ts = thumbnailRequests.get(id);
|
||||||
if (!ts) return false;
|
if (!ts) return false;
|
||||||
@@ -150,3 +196,40 @@ export function consumeThumbnailRequest(id: string, windowMs = 5000): boolean {
|
|||||||
thumbnailRequests.delete(id);
|
thumbnailRequests.delete(id);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const thumbnailWaiters = new Map<string, Set<() => void>>();
|
||||||
|
|
||||||
|
export function notifyThumbnailGenerated(id: string) {
|
||||||
|
const waiters = thumbnailWaiters.get(id);
|
||||||
|
if (!waiters) return;
|
||||||
|
thumbnailWaiters.delete(id);
|
||||||
|
for (const cb of waiters) {
|
||||||
|
try { cb(); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForThumbnail(id: string, timeoutMs = 2500): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
let callback: () => void;
|
||||||
|
const finish = (fresh: boolean) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
const set = thumbnailWaiters.get(id);
|
||||||
|
if (set) {
|
||||||
|
set.delete(callback);
|
||||||
|
if (set.size === 0) thumbnailWaiters.delete(id);
|
||||||
|
}
|
||||||
|
resolve(fresh);
|
||||||
|
};
|
||||||
|
callback = () => finish(true);
|
||||||
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
||||||
|
let set = thumbnailWaiters.get(id);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
thumbnailWaiters.set(id, set);
|
||||||
|
}
|
||||||
|
set.add(callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ async function getGeoip() {
|
|||||||
}
|
}
|
||||||
import { ClientInfo } from "./types";
|
import { ClientInfo } from "./types";
|
||||||
import {
|
import {
|
||||||
consumeThumbnailRequest,
|
isThumbnailRequested,
|
||||||
hasThumbnail,
|
hasThumbnail,
|
||||||
generateThumbnail,
|
|
||||||
setLatestFrame,
|
setLatestFrame,
|
||||||
|
notifyThumbnailGenerated,
|
||||||
|
requestThumbnailRegen,
|
||||||
} from "./thumbnails";
|
} from "./thumbnails";
|
||||||
import { upsertClientRow, type ClientDbRow } from "./db";
|
import { upsertClientRow, type ClientDbRow } from "./db";
|
||||||
import { metrics } from "./metrics";
|
import { metrics } from "./metrics";
|
||||||
@@ -296,10 +297,12 @@ export function handleFrame(info: ClientInfo, payload: any) {
|
|||||||
|
|
||||||
if (safeFormat) {
|
if (safeFormat) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const thumbnailRequested = consumeThumbnailRequest(info.id);
|
const thumbnailRequested = isThumbnailRequested(info.id);
|
||||||
if (thumbnailRequested || !hasThumbnail(info.id)) {
|
if (thumbnailRequested || !hasThumbnail(info.id)) {
|
||||||
setLatestFrame(info.id, bytes, safeFormat);
|
setLatestFrame(info.id, bytes, safeFormat);
|
||||||
void generateThumbnail(info.id);
|
void requestThumbnailRegen(info.id).then((ok) => {
|
||||||
|
if (ok) notifyThumbnailGenerated(info.id);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
info.lastSeen = now;
|
info.lastSeen = now;
|
||||||
info.online = true;
|
info.online = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user