From 720ffca099229ef7b04cb91c889a922d0a156892 Mon Sep 17 00:00:00 2001 From: schmeeve Date: Thu, 24 Sep 2026 08:28:43 -0700 Subject: [PATCH] various fixes --- backend/notes.py | 19 +++++++++++ browser-extension/background.js | 56 +++++++++++++++++++++++++++++++++ browser-extension/content.js | 24 +++++++++++++- browser-extension/popup.html | 2 +- browser-extension/popup.js | 13 ++++++-- static/app.css | 10 +++++- static/app.js | 1 + 7 files changed, 119 insertions(+), 6 deletions(-) diff --git a/backend/notes.py b/backend/notes.py index 2763cf2..eab55aa 100644 --- a/backend/notes.py +++ b/backend/notes.py @@ -48,6 +48,24 @@ def note_attachments(body: str) -> tuple[int, str | None]: return count, preview +def note_snippet(body: str, limit: int = 240) -> str: + lines = body.splitlines() + start = 0 + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("# "): + start = i + 1 + continue + start = i + break + excerpt = "\n".join(lines[start:]).strip() + if len(excerpt) > limit: + excerpt = excerpt[:limit].rstrip() + "…" + return excerpt + + def note_path(username: str, note_id: str) -> str: return f"{username}/{note_id}.md" @@ -293,6 +311,7 @@ async def _attach_meta(notes: list[dict]) -> None: count, preview = 0, None note["attachments"] = count note["image"] = preview + note["snippet"] = note_snippet(body) note.pop("path", None) diff --git a/browser-extension/background.js b/browser-extension/background.js index 7a76082..e5554bd 100644 --- a/browser-extension/background.js +++ b/browser-extension/background.js @@ -24,6 +24,8 @@ browser.runtime.onMessage.addListener((msg, sender) => { 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')); } @@ -93,6 +95,60 @@ async function uploadAttachment(msg) { 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' }); + 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 }); + 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 }).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'; diff --git a/browser-extension/content.js b/browser-extension/content.js index f4ff8af..4ce8698 100644 --- a/browser-extension/content.js +++ b/browser-extension/content.js @@ -23,7 +23,9 @@ // ── Extension→Page messaging (capture context for the popup) ────────────── browser.runtime.onMessage.addListener(function (msg) { - if (msg && msg.kind === 'get_context') { + if (!msg || typeof msg.kind !== 'string') return Promise.resolve(undefined); + + if (msg.kind === 'get_context') { var selection = ''; try { selection = window.getSelection().toString().trim(); } catch (e) {} return Promise.resolve({ @@ -32,6 +34,26 @@ url: window.location.href || '', }); } + + if (msg.kind === 'get_viewport') { + var ph = Math.max( + document.documentElement ? document.documentElement.scrollHeight : 0, + document.body ? document.body.scrollHeight : 0 + ); + return Promise.resolve({ + viewportWidth: window.innerWidth || document.documentElement.clientWidth || 0, + viewportHeight: window.innerHeight || document.documentElement.clientHeight || 0, + pageHeight: ph, + devicePixelRatio: window.devicePixelRatio || 1, + scrollY: window.scrollY || window.pageYOffset || 0, + }); + } + + if (msg.kind === 'scroll_to') { + window.scrollTo(0, msg.y); + return Promise.resolve({ y: window.scrollY || window.pageYOffset || 0 }); + } + return Promise.resolve(undefined); }); })(); diff --git a/browser-extension/popup.html b/browser-extension/popup.html index 380526e..d0660de 100644 --- a/browser-extension/popup.html +++ b/browser-extension/popup.html @@ -11,7 +11,7 @@
- +
diff --git a/browser-extension/popup.js b/browser-extension/popup.js index a80b517..e0d725e 100644 --- a/browser-extension/popup.js +++ b/browser-extension/popup.js @@ -68,16 +68,23 @@ noteEl.value = v + link + '\n'; captureBtn.disabled = true; + showStatus('Capturing full page…', true); try { - const dataUrl = await browser.tabs.captureVisibleTab({ format: 'png' }); + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + if (!tabs.length || tabs[0].id == null) { showStatus('No active tab ', false); return; } + const shot = await browser.runtime.sendMessage({ + kind: 'capture_full_page', + tabId: tabs[0].id, + windowId: tabs[0].windowId, + }); const r = await browser.runtime.sendMessage({ kind: 'upload_attachment', - dataUrl: dataUrl, + dataUrl: shot.dataUrl, filename: 'page.png', username: userEl.value, }); noteEl.value += '![Screenshot](' + r.url + ')\n'; - showStatus('Screenshot attached', true); + showStatus('Full-page screenshot attached', true); } catch (e) { showStatus('Screenshot failed: ' + e.message, false); } finally { diff --git a/static/app.css b/static/app.css index 46dd6f0..5d3bca8 100644 --- a/static/app.css +++ b/static/app.css @@ -272,8 +272,16 @@ body { .note-title { font-weight: 600; color: var(--secondary); } .note-meta { color: var(--muted); font-size: 12px; margin-top: 2px; } .note-tags { color: var(--primary); font-size: 12px; margin-top: 4px; } +.note-snippet { color: var(--text); font-size: 13px; line-height: 1.45; margin-top: 6px; overflow: hidden; } +.note-snippet p { margin: 0 0 0.3em; } +.note-snippet p:last-child { margin-bottom: 0; } +.note-snippet ul, .note-snippet ol { margin: 0.2em 0; padding-left: 1.4em; } +.note-snippet code { background: var(--code-bg); color: var(--code-fg); padding: 1px 4px; border-radius: 4px; font-size: 0.9em; } +.note-snippet a { color: var(--secondary); text-decoration: underline; } +.note-snippet h1, .note-snippet h2, .note-snippet h3 { margin: 0.3em 0; font-size: 1em; color: var(--secondary); } +.note-snippet blockquote { border-left: 3px solid var(--secondary); margin: 0.3em 0; padding-left: 10px; color: var(--muted); } -.note-item { display: flex; align-items: center; gap: 12px; } +.note-item { display: flex; align-items: flex-start; gap: 12px; } .note-body { flex: 1; min-width: 0; } .note-thumb { width: 56px; diff --git a/static/app.js b/static/app.js index 941beca..7230f32 100644 --- a/static/app.js +++ b/static/app.js @@ -415,6 +415,7 @@ function noteList(notes) { el("div", { class: "note-title", text: n.title || "Untitled" }), meta, el("div", { class: "note-tags", text: tags }), + n.snippet ? el("div", { class: "note-snippet", html: marked.parse(n.snippet) }) : null, ]); ul.appendChild(el("li", { class: isCommon ? "note-item note-common" : "note-item", onclick: () => openNote(n.id) }, [ n.image ? el("img", { class: "note-thumb", src: n.image, alt: "", loading: "lazy" }) : null,