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, pingClient, userRole, }) { const isViewer = userRole === "viewer"; const MAX_ANIMATED_CARDS = 120; const INSERT_BATCH_SIZE = 40; const TOUCH_LONG_PRESS_MS = 520; const TOUCH_MOVE_CANCEL_PX = 10; let renderToken = 0; function renderMerge(data, options = {}) { const { reorder = false } = options; 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 hadCards = grid.querySelectorAll("article[data-id]").length > 0; 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 (reorder) { items.forEach((client) => { const card = grid.querySelector(`article[data-id="${client.id}"]`); if (card) grid.appendChild(card); }); } if (newClients.length === 0) { return; } const allowAnimation = !hadCards && !prefersReducedMotion && items.length <= 1000; const animateLimit = Math.min(newClients.length, MAX_ANIMATED_CARDS); 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; } }; insertBatch(); } 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 || ""; let longPressTimer = null; card._longPressTriggered = false; let pointerStartX = 0; let pointerStartY = 0; const clearLongPress = () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }; 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); }); card.addEventListener("pointerdown", (e) => { if (isViewer || e.pointerType !== "touch") return; if (e.target.closest("button") || e.target.closest(".client-checkbox")) return; const clientId = card.dataset.id; if (!clientId) return; card._longPressTriggered = false; pointerStartX = e.clientX; pointerStartY = e.clientY; clearLongPress(); longPressTimer = setTimeout(() => { card._longPressTriggered = true; openMenu(clientId, e.clientX, e.clientY); }, TOUCH_LONG_PRESS_MS); }); card.addEventListener("pointermove", (e) => { if (!longPressTimer || e.pointerType !== "touch") return; const movedX = Math.abs(e.clientX - pointerStartX); const movedY = Math.abs(e.clientY - pointerStartY); if (movedX > TOUCH_MOVE_CANCEL_PX || movedY > TOUCH_MOVE_CANCEL_PX) { clearLongPress(); } }); card.addEventListener("pointerup", clearLongPress); card.addEventListener("pointercancel", clearLongPress); card.addEventListener("pointerleave", clearLongPress); 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; const wasTagNoteExpanded = card.dataset.tagNoteExpanded === "true"; card.dataset.online = String(!!client.online); card.dataset.os = String(client.os || "").toLowerCase(); card.dataset.nickname = String(client.nickname || ""); card.dataset.customTag = String(client.customTag || ""); card.dataset.bookmarked = String(!!client.bookmarked); card._customTagNote = String(client.customTagNote || ""); 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 customTag = String(client.customTag || "").trim(); const customTagNote = String(client.customTagNote || ""); const displayName = nickname || client.host || deviceId; const hasTagNote = customTag.length > 0 && customTagNote.length > 0; const isTagNoteExpanded = hasTagNote && wasTagNoteExpanded; card.dataset.tagNoteExpanded = isTagNoteExpanded ? "true" : "false"; card.className = `card rounded-xl border ${client.bookmarked ? "border-yellow-600/60" : "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 object-contain 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 = `