// QR Clipboard // // The popup shows a persistent log of every QR code ever decoded. // Clicking "Scan visible tab" captures the viewport, decodes all QR codes, // appends them to the log, and copies the LAST one to the clipboard. // // Decode strategy: jsQR finds ONE code per scan, so we loop — decode, // white-out the detected region, re-scan — until nothing is found. const api = typeof browser !== "undefined" ? browser : chrome; const STORAGE_KEY = "qrLog"; const MAX_ENTRIES = 500; const statusEl = document.getElementById("status"); const logEl = document.getElementById("log"); const scanBtn = document.getElementById("scan"); const clearBtn = document.getElementById("clear"); function setStatus(text, cls) { statusEl.textContent = text || ""; statusEl.className = cls || ""; } // --- storage --------------------------------------------------------------- async function getLog() { const stored = await api.storage.local.get(STORAGE_KEY); return Array.isArray(stored[STORAGE_KEY]) ? stored[STORAGE_KEY] : []; } // entries: newest-first (entries[0] is the last decoded code of the scan). async function appendEntries(entries) { const log = await getLog(); const result = log.slice(); // existing log, newest-first let added = 0; let lastSkipped = false; // was the last decoded code (entries[0]) a duplicate? // Walk oldest→newest so the last decoded code ends up at the front. // Skip any code that matches the current front (the most recent entry), // keeping the existing entry rather than re-adding it. for (let i = entries.length - 1; i >= 0; i--) { const entry = entries[i]; const isDuplicate = result[0] && result[0].text === entry.text; if (i === 0) lastSkipped = !!isDuplicate; if (isDuplicate) continue; result.unshift(entry); added++; } const next = result.slice(0, MAX_ENTRIES); // cap total size await api.storage.local.set({ [STORAGE_KEY]: next }); return { log: next, added, lastSkipped }; } async function clearLog() { await api.storage.local.remove(STORAGE_KEY); } // --- rendering ------------------------------------------------------------- function renderLog(log) { logEl.replaceChildren(); if (!log.length) { const li = document.createElement("li"); li.className = "empty"; li.textContent = "No QR codes logged yet."; logEl.appendChild(li); return; } log.forEach((entry, index) => { const li = document.createElement("li"); const body = document.createElement("div"); body.className = "entry-body"; const text = document.createElement("div"); text.className = "entry-text"; text.textContent = entry.text; // textContent: QR payloads are untrusted body.appendChild(text); const meta = document.createElement("div"); meta.className = "entry-meta"; const when = new Date(entry.time).toLocaleString(); meta.textContent = entry.url ? `${when} · ${entry.url}` : when; body.appendChild(meta); li.appendChild(body); const del = document.createElement("button"); del.className = "entry-del"; del.textContent = "✕"; del.title = "Delete this entry"; del.addEventListener("click", () => deleteEntry(index)); li.appendChild(del); logEl.appendChild(li); }); } // --- decoding -------------------------------------------------------------- function dataUrlToImageData(dataUrl) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { const canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext("2d", { willReadFrequently: true }); ctx.drawImage(img, 0, 0); resolve(ctx.getImageData(0, 0, canvas.width, canvas.height)); }; img.onerror = () => reject(new Error("Failed to load captured image")); img.src = dataUrl; }); } // Paint a code's bounding box white so the next jsQR pass can't see it. function maskRegion(imageData, location) { const xs = [ location.topLeftCorner.x, location.topRightCorner.x, location.bottomLeftCorner.x, location.bottomRightCorner.x, ]; const ys = [ location.topLeftCorner.y, location.topRightCorner.y, location.bottomLeftCorner.y, location.bottomRightCorner.y, ]; const minX = Math.max(0, Math.floor(Math.min(...xs))); const maxX = Math.min(imageData.width, Math.ceil(Math.max(...xs))); const minY = Math.max(0, Math.floor(Math.min(...ys))); const maxY = Math.min(imageData.height, Math.ceil(Math.max(...ys))); const data = imageData.data; for (let y = minY; y < maxY; y++) { for (let x = minX; x < maxX; x++) { const i = (y * imageData.width + x) * 4; data[i] = data[i + 1] = data[i + 2] = 255; data[i + 3] = 255; } } } function decodeAll(imageData) { const found = []; const MAX_PASSES = 20; // safety cap against pathological loops for (let pass = 0; pass < MAX_PASSES; pass++) { const code = jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: "attemptBoth", }); if (!code || !code.location) break; found.push(code.data); maskRegion(imageData, code.location); } return found; } async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch (e) { const ta = document.createElement("textarea"); ta.value = text; document.body.appendChild(ta); ta.select(); const ok = document.execCommand("copy"); ta.remove(); return ok; } } // --- actions --------------------------------------------------------------- async function scan() { scanBtn.disabled = true; setStatus("Scanning visible tab…", "muted"); try { const [tab] = await api.tabs.query({ active: true, currentWindow: true }); const dataUrl = await api.tabs.captureVisibleTab(); const imageData = await dataUrlToImageData(dataUrl); const codes = decodeAll(imageData); if (codes.length === 0) { console.log("[QR Clipboard] No QR codes found."); setStatus("No QR codes found in the visible area.", "muted"); return; } console.log(`[QR Clipboard] Decoded ${codes.length} code(s):`, codes); const now = new Date().toISOString(); const url = tab ? tab.url : ""; // Newest first within this scan too, so the LAST decoded sits on top. const entries = codes .map((text) => ({ text, time: now, url })) .reverse(); const { log, added, lastSkipped } = await appendEntries(entries); renderLog(log); const last = codes[codes.length - 1]; const copied = await copyToClipboard(last); const skipped = codes.length - added; let msg = `Found ${codes.length} code(s)`; if (skipped) msg += `, ${skipped} duplicate(s) skipped`; msg += "."; if (!copied) { msg += " Clipboard write failed."; } else if (!lastSkipped) { msg += " Last one copied to clipboard."; } setStatus(msg, copied ? "ok" : "err"); } catch (e) { setStatus("Could not scan this tab: " + e.message, "err"); } finally { scanBtn.disabled = false; } } async function deleteEntry(index) { const log = await getLog(); log.splice(index, 1); await api.storage.local.set({ [STORAGE_KEY]: log }); renderLog(log); setStatus("Entry deleted.", "muted"); } async function clear() { await clearLog(); renderLog([]); setStatus("Log cleared.", "muted"); } // --- init ------------------------------------------------------------------ scanBtn.addEventListener("click", scan); clearBtn.addEventListener("click", clear); (async () => { renderLog(await getLog()); })();