rspace-online/browser-extension/background.js

176 lines
7.6 KiB
JavaScript

const DEFAULT_HOST = 'https://rspace.online';
// --- Context Menu Setup ---
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({ id: 'clip-page', title: 'Clip page to rSpace', contexts: ['page'] });
chrome.contextMenus.create({ id: 'save-link', title: 'Save link to rSpace', contexts: ['link'] });
chrome.contextMenus.create({ id: 'save-image', title: 'Save image to rSpace', contexts: ['image'] });
chrome.contextMenus.create({ id: 'clip-selection', title: 'Clip selection to rSpace', contexts: ['selection'] });
chrome.contextMenus.create({ id: 'unlock-article', title: 'Unlock & Clip article to rSpace', contexts: ['page', 'link'] });
});
// --- Helpers ---
async function getSettings() {
const result = await chrome.storage.sync.get(['rspaceHost', 'rspaceSlug']);
return {
host: result.rspaceHost || DEFAULT_HOST,
slug: result.rspaceSlug || '',
};
}
function apiBase(settings) {
return settings.slug ? `${settings.host}/${settings.slug}/rnotes` : `${settings.host}/rnotes`;
}
async function getToken() {
const result = await chrome.storage.local.get(['encryptid_token']);
return result.encryptid_token || null;
}
async function getDefaultNotebook() {
const result = await chrome.storage.local.get(['lastNotebookId']);
return result.lastNotebookId || null;
}
function showNotification(title, message) {
chrome.notifications.create({
type: 'basic', iconUrl: 'icons/icon-128.png', title, message,
});
}
async function createNote(data) {
const token = await getToken();
if (!token) { showNotification('rSpace Error', 'Not signed in. Open extension settings.'); return; }
const settings = await getSettings();
if (!settings.slug) { showNotification('rSpace Error', 'Configure space slug in extension settings.'); return; }
const notebookId = await getDefaultNotebook();
const body = { title: data.title, content: data.content, type: data.type || 'CLIP', url: data.url };
if (notebookId) body.notebook_id = notebookId;
if (data.fileUrl) body.file_url = data.fileUrl;
if (data.mimeType) body.mime_type = data.mimeType;
if (data.fileSize) body.file_size = data.fileSize;
const response = await fetch(`${apiBase(settings)}/api/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify(body),
});
if (!response.ok) { const text = await response.text(); throw new Error(`${response.status}: ${text}`); }
return response.json();
}
async function uploadImage(imageUrl) {
const token = await getToken();
const settings = await getSettings();
const imgResponse = await fetch(imageUrl);
const blob = await imgResponse.blob();
let filename;
try { filename = new URL(imageUrl).pathname.split('/').pop() || `image-${Date.now()}.jpg`; }
catch { filename = `image-${Date.now()}.jpg`; }
const formData = new FormData();
formData.append('file', blob, filename);
const response = await fetch(`${apiBase(settings)}/api/uploads`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: formData,
});
if (!response.ok) { const text = await response.text(); throw new Error(`Upload failed: ${response.status} ${text}`); }
return response.json();
}
async function unlockArticle(url) {
const token = await getToken();
if (!token) { showNotification('rSpace Error', 'Not signed in.'); return null; }
const settings = await getSettings();
const response = await fetch(`${apiBase(settings)}/api/articles/unlock`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ url }),
});
if (!response.ok) { const text = await response.text(); throw new Error(`Unlock failed: ${response.status} ${text}`); }
return response.json();
}
// --- Context Menu Handler ---
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
try {
switch (info.menuItemId) {
case 'clip-page': {
let content = '';
try {
const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.body.innerHTML });
content = result?.result || '';
} catch { content = `<p>Clipped from <a href="${tab.url}">${tab.url}</a></p>`; }
await createNote({ title: tab.title || 'Untitled Clip', content, type: 'CLIP', url: tab.url });
showNotification('Page Clipped', `"${tab.title}" saved to rSpace`);
break;
}
case 'save-link': {
const linkUrl = info.linkUrl;
const linkText = info.selectionText || linkUrl;
await createNote({ title: linkText, content: `<p><a href="${linkUrl}">${linkText}</a></p><p>Found on: <a href="${tab.url}">${tab.title}</a></p>`, type: 'BOOKMARK', url: linkUrl });
showNotification('Link Saved', 'Bookmark saved to rSpace');
break;
}
case 'save-image': {
const upload = await uploadImage(info.srcUrl);
await createNote({ title: `Image from ${tab.title || 'page'}`, content: `<p><img src="${upload.url}" alt="Clipped image" /></p><p>Source: <a href="${tab.url}">${tab.title}</a></p>`, type: 'IMAGE', url: tab.url, fileUrl: upload.url, mimeType: upload.mimeType, fileSize: upload.size });
showNotification('Image Saved', 'Image saved to rSpace');
break;
}
case 'unlock-article': {
const targetUrl = info.linkUrl || tab.url;
showNotification('Unlocking Article', `Finding readable version of ${new URL(targetUrl).hostname}...`);
const result = await unlockArticle(targetUrl);
if (result?.success && result.archiveUrl) {
await createNote({ title: tab.title || 'Unlocked Article', content: `<p>Unlocked via ${result.strategy}</p><p>Original: <a href="${targetUrl}">${targetUrl}</a></p><p>Archive: <a href="${result.archiveUrl}">${result.archiveUrl}</a></p>`, type: 'CLIP', url: targetUrl });
showNotification('Article Unlocked', `Readable version found via ${result.strategy}`);
chrome.tabs.create({ url: result.archiveUrl });
} else {
showNotification('Unlock Failed', result?.error || 'No archived version found');
}
break;
}
case 'clip-selection': {
let content = '';
try {
const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { const s = window.getSelection(); if (!s || s.rangeCount === 0) return ''; const r = s.getRangeAt(0); const d = document.createElement('div'); d.appendChild(r.cloneContents()); return d.innerHTML; } });
content = result?.result || '';
} catch { content = `<p>${info.selectionText || ''}</p>`; }
if (!content && info.selectionText) content = `<p>${info.selectionText}</p>`;
await createNote({ title: `Selection from ${tab.title || 'page'}`, content, type: 'CLIP', url: tab.url });
showNotification('Selection Clipped', 'Saved to rSpace');
break;
}
}
} catch (err) {
console.error('Context menu action failed:', err);
showNotification('rSpace Error', err.message || 'Failed to save');
}
});
// --- Keyboard shortcut handler ---
chrome.commands.onCommand.addListener(async (command) => {
if (command === 'open-voice-recorder') {
const settings = await getSettings();
const voiceUrl = settings.slug ? `${settings.host}/${settings.slug}/rnotes/voice` : `${settings.host}/rnotes/voice`;
chrome.windows.create({ url: voiceUrl, type: 'popup', width: 400, height: 600, focused: true });
}
});
// --- Message Handler ---
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'notify') showNotification(message.title, message.message);
});