const DEFAULT_HOST = 'https://rspace.online'; let currentTab = null; let selectedText = ''; let selectedHtml = ''; // --- 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; } function decodeToken(token) { try { const payload = JSON.parse(atob(token.split('.')[1])); if (payload.exp && payload.exp * 1000 < Date.now()) return null; return payload; } catch { return null; } } function parseTags(tagString) { if (!tagString || !tagString.trim()) return []; return tagString.split(',').map(t => t.trim().toLowerCase()).filter(Boolean); } function showStatus(message, type) { const el = document.getElementById('status'); el.textContent = message; el.className = `status ${type}`; if (type === 'success') { setTimeout(() => { el.className = 'status'; }, 3000); } } // --- API calls --- async function createNote(data) { const token = await getToken(); const settings = await getSettings(); if (!settings.slug) { showStatus('Configure space slug in Settings first', 'error'); return; } const body = { title: data.title, content: data.content, type: data.type || 'CLIP', url: data.url, }; const notebookId = document.getElementById('notebook').value; if (notebookId) body.notebook_id = notebookId; const tags = parseTags(document.getElementById('tags').value); if (tags.length > 0) body.tags = tags; 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 fetchNotebooks() { const token = await getToken(); const settings = await getSettings(); if (!settings.slug) return []; const response = await fetch(`${apiBase(settings)}/api/notebooks`, { headers: { 'Authorization': `Bearer ${token}` }, }); if (!response.ok) return []; const data = await response.json(); return data.notebooks || (Array.isArray(data) ? data : []); } // --- UI --- async function populateNotebooks() { const select = document.getElementById('notebook'); try { const notebooks = await fetchNotebooks(); for (const nb of notebooks) { const option = document.createElement('option'); option.value = nb.id; option.textContent = nb.title; select.appendChild(option); } const { lastNotebookId } = await chrome.storage.local.get(['lastNotebookId']); if (lastNotebookId) select.value = lastNotebookId; } catch (err) { console.error('Failed to load notebooks:', err); } } function setupNotebookMemory() { document.getElementById('notebook').addEventListener('change', (e) => { chrome.storage.local.set({ lastNotebookId: e.target.value }); }); } async function init() { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); currentTab = tab; document.getElementById('pageTitle').textContent = tab.title || 'Untitled'; document.getElementById('pageUrl').textContent = tab.url || ''; const token = await getToken(); const claims = token ? decodeToken(token) : null; const settings = await getSettings(); if (!claims) { document.getElementById('userStatus').textContent = 'Not signed in'; document.getElementById('userStatus').classList.add('not-authed'); document.getElementById('authWarning').style.display = 'block'; return; } if (!settings.slug) { document.getElementById('userStatus').textContent = 'No space configured'; document.getElementById('userStatus').classList.add('not-authed'); document.getElementById('authWarning').style.display = 'block'; document.getElementById('authWarning').innerHTML = 'Configure your space slug. Open Settings'; document.getElementById('openSettings')?.addEventListener('click', (e) => { e.preventDefault(); chrome.runtime.openOptionsPage(); }); return; } document.getElementById('userStatus').textContent = claims.username || claims.sub?.slice(0, 16) || 'Authenticated'; document.getElementById('authWarning').style.display = 'none'; document.getElementById('clipPageBtn').disabled = false; document.getElementById('unlockBtn').disabled = false; document.getElementById('voiceBtn').disabled = false; await populateNotebooks(); setupNotebookMemory(); try { const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { const selection = window.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return { text: '', html: '' }; const range = selection.getRangeAt(0); const div = document.createElement('div'); div.appendChild(range.cloneContents()); return { text: selection.toString(), html: div.innerHTML }; }, }); if (result?.result?.text) { selectedText = result.result.text; selectedHtml = result.result.html; document.getElementById('clipSelectionBtn').disabled = false; } } catch (err) { console.warn('Cannot access page content:', err); } } // --- Event handlers --- document.getElementById('clipPageBtn').addEventListener('click', async () => { const btn = document.getElementById('clipPageBtn'); btn.disabled = true; showStatus('Clipping page...', 'loading'); try { let pageContent = ''; try { const [result] = await chrome.scripting.executeScript({ target: { tabId: currentTab.id }, func: () => document.body.innerHTML, }); pageContent = result?.result || ''; } catch { pageContent = `
Clipped from ${currentTab.url}
`; } await createNote({ title: currentTab.title || 'Untitled Clip', content: pageContent, type: 'CLIP', url: currentTab.url, }); showStatus('Clipped! Note saved.', 'success'); chrome.runtime.sendMessage({ type: 'notify', title: 'Page Clipped', message: `"${currentTab.title}" saved to rSpace` }); } catch (err) { showStatus(`Error: ${err.message}`, 'error'); } finally { btn.disabled = false; } }); document.getElementById('clipSelectionBtn').addEventListener('click', async () => { const btn = document.getElementById('clipSelectionBtn'); btn.disabled = true; showStatus('Clipping selection...', 'loading'); try { const content = selectedHtml || `${selectedText}
`; await createNote({ title: `Selection from ${currentTab.title || 'page'}`, content, type: 'CLIP', url: currentTab.url, }); showStatus('Selection clipped!', 'success'); chrome.runtime.sendMessage({ type: 'notify', title: 'Selection Clipped', message: 'Saved to rSpace' }); } catch (err) { showStatus(`Error: ${err.message}`, 'error'); } finally { btn.disabled = false; } }); document.getElementById('unlockBtn').addEventListener('click', async () => { const btn = document.getElementById('unlockBtn'); btn.disabled = true; showStatus('Unlocking article...', 'loading'); try { const token = await getToken(); 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: currentTab.url }), }); const result = await response.json(); if (result.success && result.archiveUrl) { await createNote({ title: currentTab.title || 'Unlocked Article', content: `Unlocked via ${result.strategy}
Original: ${currentTab.url}
Archive: ${result.archiveUrl}
`, type: 'CLIP', url: currentTab.url, }); showStatus(`Unlocked via ${result.strategy}! Opening...`, 'success'); chrome.tabs.create({ url: result.archiveUrl }); } else { showStatus(result.error || 'No archived version found', 'error'); } } catch (err) { showStatus(`Error: ${err.message}`, 'error'); } finally { btn.disabled = false; } }); document.getElementById('voiceBtn').addEventListener('click', async () => { 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 }); window.close(); }); document.getElementById('optionsLink').addEventListener('click', (e) => { e.preventDefault(); chrome.runtime.openOptionsPage(); }); document.getElementById('openSettings')?.addEventListener('click', (e) => { e.preventDefault(); chrome.runtime.openOptionsPage(); }); document.addEventListener('DOMContentLoaded', init);