let serverUrl = ''; function norm(v) { return (v || '').replace(/\/+$/, ''); } async function getServerUrl() { const r = await browser.storage.sync.get('serverUrl'); serverUrl = norm(r.serverUrl); return serverUrl; } browser.storage.onChanged.addListener(changes => { if (changes.serverUrl) { serverUrl = norm(changes.serverUrl.newValue); } }); browser.runtime.onMessage.addListener((msg, sender) => { switch (msg.kind) { case 'list_users': return listUsers(); case 'save_note': return saveNote(msg); case 'upload_attachment': return uploadAttachment(msg); case 'capture_full_page': return captureFullPage(msg); default: return Promise.reject(new Error('Unknown message kind')); } }); async function request(path, opts) { const base = await getServerUrl(); if (!base) { throw new Error('Server URL not set — open snotes and click "Capture" → "Apply to Extension", or set it in Options'); } opts = opts || {}; opts.credentials = 'include'; let res; try { res = await fetch(base + path, opts); } catch (e) { throw new Error('Cannot reach ' + base + ' (' + e.message + ')'); } if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || 'HTTP ' + res.status); } return res.json(); } async function listUsers() { const users = await request('/api/users', { headers: { 'Accept': 'application/json' } }); return { users: users }; } async function saveNote(msg) { const body = { body: msg.body }; if (msg.username) body.username = msg.username; if (msg.id) { return request('/api/notes/' + msg.id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); } return request('/api/notes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); } async function uploadAttachment(msg) { const base = await getServerUrl(); if (!base) { throw new Error('Server URL not set — open snotes and click "Capture" → "Apply to Extension", or set it in Options'); } const blob = dataUrlToBlob(msg.dataUrl); const form = new FormData(); form.append('file', blob, msg.filename || 'page.png'); if (msg.username) form.append('username', msg.username); const res = await fetch(base + '/api/attachments', { method: 'POST', body: form, credentials: 'include', }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || 'HTTP ' + res.status); } return res.json(); } const MAX_SLICES = 60; function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } function loadImage(dataUrl) { return new Promise(function (resolve, reject) { const img = new Image(); img.onload = function () { resolve(img); }; img.onerror = function () { reject(new Error('Could not decode screenshot slice')); }; img.src = dataUrl; }); } async function captureFullPage(msg) { const tabId = msg.tabId; const windowId = msg.windowId; if (tabId == null) throw new Error('No active tab'); const dims = await browser.tabs.sendMessage(tabId, { kind: 'get_viewport' }, { frameId: 0 }); const vw = dims.viewportWidth || 1; const vh = dims.viewportHeight || 1; const ph = dims.pageHeight || vh; const dpr = dims.devicePixelRatio || 1; const sliceCount = Math.min(MAX_SLICES, Math.ceil(ph / vh)); if (Math.ceil(ph / vh) > MAX_SLICES) { throw new Error('Page too tall to capture (' + Math.ceil(ph / vh) + ' viewports)'); } const parts = []; for (let i = 0; i < sliceCount; i++) { const y = Math.max(0, Math.min(i * vh, ph - vh)); await browser.tabs.sendMessage(tabId, { kind: 'scroll_to', y: y }, { frameId: 0 }); await sleep(250); const dataUrl = await browser.tabs.captureVisibleTab(windowId, { format: 'png' }); parts.push({ dataUrl: dataUrl, y: y }); } await browser.tabs.sendMessage(tabId, { kind: 'scroll_to', y: dims.scrollY || 0 }, { frameId: 0 }).catch(() => {}); const canvas = document.createElement('canvas'); canvas.width = Math.round(vw * dpr); canvas.height = Math.round(ph * dpr); const ctx = canvas.getContext('2d'); for (const p of parts) { const img = await loadImage(p.dataUrl); ctx.drawImage(img, 0, Math.round(p.y * dpr)); } return { dataUrl: canvas.toDataURL('image/png') }; } function dataUrlToBlob(dataUrl) { const [meta, b64] = dataUrl.split(','); const mime = (meta.match(/data:(.*?);/) || [])[1] || 'image/png'; const bin = atob(b64); const bytes = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return new Blob([bytes], { type: mime }); }