rspace-online/browser-extension/popup.js

300 lines
9.2 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();
});
document.addEventListener('DOMContentLoaded', init);