import { formatAgo, formatPing, countryToFlag, osBadge, archBadge, versionBadge, monitorsBadge, shortId, } from "./viewUtils.js"; function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } export function createRenderer({ grid, totalPill, pageLabel, openMenu, openModal, requestPreview, requestThumbnail, userRole, }) { const isViewer = userRole === "viewer"; const MAX_ANIMATED_CARDS = 120; const INSERT_BATCH_SIZE = 40; let renderToken = 0; function renderMerge(data) { totalPill.textContent = `${data.online ?? data.total} online / ${data.total} total`; const totalPages = Math.max(1, Math.ceil(data.total / data.pageSize)); pageLabel.textContent = `Page ${data.page} of ${totalPages}`; prevBtnState(data.page, totalPages); const items = data.items || []; const seen = new Set(); const newClients = []; const renderId = ++renderToken; const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; const animateLimit = Math.min(items.length, MAX_ANIMATED_CARDS); const allowAnimation = !prefersReducedMotion && items.length <= 1000; items.forEach((client) => { seen.add(client.id); const existing = grid.querySelector(`article[data-id="${client.id}"]`); if (existing) { updateCard(existing, client); return; } newClients.push(client); }); Array.from(grid.querySelectorAll("article[data-id]")) .filter((el) => !seen.has(el.dataset.id)) .forEach((el) => el.remove()); if (newClients.length === 0) { moveOfflineToBottom(); return; } let idx = 0; const insertBatch = () => { if (renderId !== renderToken) return; const fragment = document.createDocumentFragment(); for ( let batch = 0; batch < INSERT_BATCH_SIZE && idx < newClients.length; batch++, idx++ ) { const client = newClients[idx]; const shouldAnimate = allowAnimation && idx < animateLimit; const card = buildCard(client, { animate: shouldAnimate, delayIndex: idx, }); fragment.appendChild(card); } grid.appendChild(fragment); if (idx < newClients.length) { requestAnimationFrame(insertBatch); return; } moveOfflineToBottom(); }; insertBatch(); } function moveOfflineToBottom() { const cards = Array.from(grid.querySelectorAll("article[data-id]")); if (cards.length === 0) return; const online = []; const offline = []; cards.forEach((card) => { if (card.dataset.online === "true") { online.push(card); return; } offline.push(card); }); if (offline.length === 0) return; const fragment = document.createDocumentFragment(); online.forEach((card) => fragment.appendChild(card)); offline.forEach((card) => fragment.appendChild(card)); grid.appendChild(fragment); } function prevBtnState(currentPage, totalPages) { const prevBtn = document.getElementById("prev"); const nextBtn = document.getElementById("next"); if (prevBtn) prevBtn.disabled = currentPage <= 1; if (nextBtn) nextBtn.disabled = currentPage >= totalPages; } function buildCard(client, options = {}) { const card = document.createElement("article"); card.dataset.id = client.id; card.dataset.hwid = client.hwid || ""; updateCard(card, client); card.addEventListener("contextmenu", (e) => { e.preventDefault(); if (isViewer) return; const clientId = card.dataset.id; if (!clientId) return; const { clientX, clientY } = e; openMenu(clientId, clientX, clientY); }); if (options.animate) { card.classList.add("card-animate"); card.style.animationDelay = `${(options.delayIndex || 0) * 0.05}s`; card.style.opacity = "0"; card.style.transform = "translateY(10px)"; if (typeof anime !== "undefined") { requestAnimationFrame(() => { anime({ targets: card, opacity: [0, 1], translateY: [10, 0], duration: 400, easing: "easeOutQuad", }); }); } } return card; } function updateCard(card, client) { const oldCheckbox = card.querySelector(".client-checkbox"); const wasChecked = oldCheckbox?.checked || false; card.dataset.online = String(!!client.online); card.dataset.os = String(client.os || "").toLowerCase(); card.dataset.nickname = String(client.nickname || ""); const os = osBadge(client.os || "unknown"); const arch = archBadge(client.arch || ""); const ver = versionBadge(client.version || ""); const mons = monitorsBadge(client.monitors); const deviceId = shortId(client.id); const hwid = shortId(client.hwid || ""); const nickname = String(client.nickname || "").trim(); const displayName = nickname || client.host || deviceId; card.className = `card rounded-xl border border-slate-800 bg-slate-900/70 p-4 shadow-lg ${client.online ? "" : "card-offline"} tone-${os.tone}`; const cardThumb = client.thumbnail ? (() => { const wrapper = document.createElement("div"); wrapper.className = "flex-shrink-0"; const img = document.createElement("img"); img.className = "w-40 h-24 rounded-lg border border-slate-800 object-cover cursor-pointer thumb-img"; img.alt = "preview"; img.src = client.thumbnail; wrapper.appendChild(img); return wrapper; })() : (() => { const wrapper = document.createElement("div"); wrapper.className = "flex-shrink-0"; wrapper.innerHTML = `