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(); }); // --- Mode switching --- document.querySelectorAll('.mode-tab').forEach((tab) => { tab.addEventListener('click', () => { const mode = tab.dataset.mode; document.querySelectorAll('.mode-tab').forEach((t) => t.classList.remove('active')); tab.classList.add('active'); document.getElementById('clipperPanel').style.display = mode === 'clipper' ? '' : 'none'; document.getElementById('cartPanel').style.display = mode === 'cart' ? '' : 'none'; chrome.storage.local.set({ lastPopupMode: mode }); if (mode === 'cart') initCartMode(); }); }); // --- Cart helpers --- function showCartStatus(message, type) { const el = document.getElementById('cartStatus'); el.textContent = message; el.className = `cart-status ${type}`; if (type === 'success') { setTimeout(() => { el.className = 'cart-status'; }, 3000); } } async function fetchSpaces() { const settings = await getSettings(); const token = await getToken(); try { const headers = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${settings.host}/api/spaces`, { headers }); if (res.ok) { const data = await res.json(); return data.spaces || data || []; } } catch {} return [{ slug: 'demo', name: 'demo' }]; } async function fetchCarts(space) { const settings = await getSettings(); const token = await getToken(); try { const headers = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${settings.host}/${space}/rcart/api/shopping-carts`, { headers }); if (res.ok) { const data = await res.json(); return data.carts || []; } } catch {} return []; } async function fetchCartSummary(space) { const settings = await getSettings(); const token = await getToken(); try { const headers = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${settings.host}/${space}/rcart/api/cart/summary`, { headers }); if (res.ok) return res.json(); } catch {} return null; } function renderCartPreview(summary) { const el = document.getElementById('cartPreview'); if (!summary || summary.totalItems === 0) { el.innerHTML = '
Cart is empty
'; return; } const vendors = summary.vendorGroups || []; const vendorHtml = vendors.map((v) => `
${v.domain || v.vendor || 'unknown'} (${v.count || v.items || 0})
` ).join(''); el.innerHTML = `
Items${summary.totalItems}
Total$${(summary.totalAmount || 0).toFixed(2)}
${vendors.length > 0 ? `
${vendorHtml}
` : ''}
`; } async function initCartMode() { const spaceSelect = document.getElementById('spaceSelect'); const cartSelect = document.getElementById('cartSelect'); // Populate spaces const spaces = await fetchSpaces(); const { selectedSpace } = await chrome.storage.sync.get(['selectedSpace']); const currentSpace = selectedSpace || 'demo'; spaceSelect.innerHTML = ''; for (const s of spaces) { const opt = document.createElement('option'); opt.value = s.slug || s.name || s; opt.textContent = s.name || s.slug || s; spaceSelect.appendChild(opt); } // Ensure current space option exists if (!spaceSelect.querySelector(`option[value="${currentSpace}"]`)) { const opt = document.createElement('option'); opt.value = currentSpace; opt.textContent = currentSpace; spaceSelect.prepend(opt); } spaceSelect.value = currentSpace; // Load carts for this space await loadCartsForSpace(currentSpace); } async function loadCartsForSpace(space) { const cartSelect = document.getElementById('cartSelect'); cartSelect.innerHTML = ''; const carts = await fetchCarts(space); const { selectedCartId } = await chrome.storage.sync.get(['selectedCartId']); cartSelect.innerHTML = ''; if (carts.length === 0) { cartSelect.innerHTML = ''; renderCartPreview(null); return; } for (const c of carts) { const opt = document.createElement('option'); opt.value = c.id; opt.textContent = c.name || c.id; cartSelect.appendChild(opt); } if (selectedCartId && cartSelect.querySelector(`option[value="${selectedCartId}"]`)) { cartSelect.value = selectedCartId; } // Load preview const summary = await fetchCartSummary(space); renderCartPreview(summary); } // Space change handler document.getElementById('spaceSelect').addEventListener('change', async (e) => { const space = e.target.value; await chrome.storage.sync.set({ selectedSpace: space }); await loadCartsForSpace(space); chrome.runtime.sendMessage({ type: 'cart-settings-changed', space }); }); // Cart change handler document.getElementById('cartSelect').addEventListener('change', async (e) => { await chrome.storage.sync.set({ selectedCartId: e.target.value }); }); // Add to Cart document.getElementById('addToCartBtn').addEventListener('click', async () => { const btn = document.getElementById('addToCartBtn'); btn.disabled = true; showCartStatus('Detecting product...', 'loading'); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const settings = await getSettings(); const token = await getToken(); const { selectedSpace } = await chrome.storage.sync.get(['selectedSpace']); const space = selectedSpace || 'demo'; // Try to detect product info from the page let product = null; try { const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { // Try JSON-LD const ldScripts = document.querySelectorAll('script[type="application/ld+json"]'); for (const s of ldScripts) { try { const data = JSON.parse(s.textContent); const item = data['@type'] === 'Product' ? data : (Array.isArray(data['@graph']) ? data['@graph'].find(i => i['@type'] === 'Product') : null); if (item) { return { name: item.name, image: item.image?.url || item.image?.[0] || item.image, price: item.offers?.price || item.offers?.[0]?.price, currency: item.offers?.priceCurrency || item.offers?.[0]?.priceCurrency, }; } } catch {} } // Fallback to meta tags const title = document.querySelector('meta[property="og:title"]')?.content || document.title; const image = document.querySelector('meta[property="og:image"]')?.content; return { name: title, image }; }, }); if (result?.result?.name) product = result.result; } catch {} showCartStatus('Adding to cart...', 'loading'); const headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; const body = { url: tab.url }; if (product) body.product = product; const res = await fetch(`${settings.host}/${space}/rcart/api/cart/quick-add`, { method: 'POST', headers, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); const added = await res.json(); showCartStatus(`Added: ${added.name || product?.name || 'item'}`, 'success'); // Update preview const summary = await fetchCartSummary(space); renderCartPreview(summary); // Notify background to update badge chrome.runtime.sendMessage({ type: 'cart-item-added', space }); } catch (err) { showCartStatus(`Error: ${err.message}`, 'error'); } finally { btn.disabled = false; } }); // View Cart document.getElementById('viewCartBtn').addEventListener('click', async () => { const settings = await getSettings(); const { selectedSpace } = await chrome.storage.sync.get(['selectedSpace']); const space = selectedSpace || 'demo'; chrome.tabs.create({ url: `${settings.host}/${space}/rcart` }); window.close(); }); // --- Init --- document.addEventListener('DOMContentLoaded', async () => { await init(); // Restore last popup mode const { lastPopupMode } = await chrome.storage.local.get(['lastPopupMode']); if (lastPopupMode === 'cart') { document.querySelectorAll('.mode-tab').forEach((t) => t.classList.remove('active')); document.querySelector('.mode-tab[data-mode="cart"]').classList.add('active'); document.getElementById('clipperPanel').style.display = 'none'; document.getElementById('cartPanel').style.display = ''; initCartMode(); } });