various fixes
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
<textarea id="note" placeholder="Type a note…" spellcheck="false"></textarea>
|
||||
<div class="actions">
|
||||
<button id="capture-page" title="Insert page title + link" type="button">⤴ Page</button>
|
||||
<button id="capture-page" title="Insert page title + link and attach a full-page screenshot" type="button">⤴ Page</button>
|
||||
<div class="spacer"></div>
|
||||
<button id="save" class="primary" type="button">Save</button>
|
||||
</div>
|
||||
|
||||
@@ -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 += '\n';
|
||||
showStatus('Screenshot attached', true);
|
||||
showStatus('Full-page screenshot attached', true);
|
||||
} catch (e) {
|
||||
showStatus('Screenshot failed: ' + e.message, false);
|
||||
} finally {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user