160 lines
4.6 KiB
JavaScript
160 lines
4.6 KiB
JavaScript
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' });
|
|
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';
|
|
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 });
|
|
}
|