561 lines
18 KiB
JavaScript
561 lines
18 KiB
JavaScript
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. <a id="openSettings">Open Settings</a>';
|
|
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 = `<p>Clipped from <a href="${currentTab.url}">${currentTab.url}</a></p>`;
|
|
}
|
|
|
|
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 || `<p>${selectedText}</p>`;
|
|
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: `<p>Unlocked via ${result.strategy}</p><p>Original: <a href="${currentTab.url}">${currentTab.url}</a></p><p>Archive: <a href="${result.archiveUrl}">${result.archiveUrl}</a></p>`,
|
|
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 = '<div class="empty-msg">Cart is empty</div>';
|
|
return;
|
|
}
|
|
|
|
const vendors = summary.vendorGroups || [];
|
|
const vendorHtml = vendors.map((v) =>
|
|
`<div class="vendor-bullet">${v.domain || v.vendor || 'unknown'} (${v.count || v.items || 0})</div>`
|
|
).join('');
|
|
|
|
el.innerHTML = `
|
|
<div class="preview-card">
|
|
<div class="preview-row"><span>Items</span><span class="value">${summary.totalItems}</span></div>
|
|
<div class="preview-row"><span>Total</span><span class="value">$${(summary.totalAmount || 0).toFixed(2)}</span></div>
|
|
${vendors.length > 0 ? `<div class="vendor-list">${vendorHtml}</div>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
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 = '<option value="">Loading...</option>';
|
|
|
|
const carts = await fetchCarts(space);
|
|
const { selectedCartId } = await chrome.storage.sync.get(['selectedCartId']);
|
|
|
|
cartSelect.innerHTML = '';
|
|
if (carts.length === 0) {
|
|
cartSelect.innerHTML = '<option value="">No carts</option>';
|
|
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();
|
|
}
|
|
});
|