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 = `

Clipped from ${tab.url}

`; } 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: `

${linkText}

Found on: ${tab.title}

`, 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: `

Clipped image

Source: ${tab.title}

`, 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: `

Unlocked via ${result.strategy}

Original: ${targetUrl}

Archive: ${result.archiveUrl}

`, 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 = `

${info.selectionText || ''}

`; } if (!content && info.selectionText) content = `

${info.selectionText}

`; 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); });