rnotes-online/browser-extension/popup.js

314 lines
8.6 KiB
JavaScript

const DEFAULT_HOST = 'https://rnotes.online';
let currentTab = null;
let selectedText = '';
let selectedHtml = '';
// --- Helpers ---
async function getSettings() {
const result = await chrome.storage.sync.get(['rnotesHost']);
return {
host: result.rnotesHost || DEFAULT_HOST,
};
}
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]));
// Check expiry
if (payload.exp && payload.exp * 1000 < Date.now()) {
return null; // expired
}
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();
const body = {
title: data.title,
content: data.content,
type: data.type || 'CLIP',
url: data.url,
};
const notebookId = document.getElementById('notebook').value;
if (notebookId) body.notebookId = notebookId;
const tags = parseTags(document.getElementById('tags').value);
if (tags.length > 0) body.tags = tags;
const response = await fetch(`${settings.host}/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();
const response = await fetch(`${settings.host}/api/notebooks`, {
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) return [];
const data = await response.json();
return Array.isArray(data) ? data : [];
}
// --- UI ---
async function populateNotebooks() {
const select = document.getElementById('notebook');
try {
const notebooks = await fetchNotebooks();
// Keep the "No notebook" option
for (const nb of notebooks) {
const option = document.createElement('option');
option.value = nb.id;
option.textContent = nb.title;
select.appendChild(option);
}
// Restore last used notebook
const { lastNotebookId } = await chrome.storage.local.get(['lastNotebookId']);
if (lastNotebookId) {
select.value = lastNotebookId;
}
} catch (err) {
console.error('Failed to load notebooks:', err);
}
}
// Save last used notebook when changed
function setupNotebookMemory() {
document.getElementById('notebook').addEventListener('change', (e) => {
chrome.storage.local.set({ lastNotebookId: e.target.value });
});
}
async function init() {
// Get current tab
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
currentTab = tab;
// Display page info
document.getElementById('pageTitle').textContent = tab.title || 'Untitled';
document.getElementById('pageUrl').textContent = tab.url || '';
// Check auth
const token = await getToken();
const claims = token ? decodeToken(token) : null;
if (!claims) {
document.getElementById('userStatus').textContent = 'Not signed in';
document.getElementById('userStatus').classList.add('not-authed');
document.getElementById('authWarning').style.display = 'block';
return;
}
document.getElementById('userStatus').textContent = claims.username || claims.sub?.slice(0, 16) || 'Authenticated';
document.getElementById('authWarning').style.display = 'none';
// Enable buttons
document.getElementById('clipPageBtn').disabled = false;
document.getElementById('unlockBtn').disabled = false;
// Load notebooks
await populateNotebooks();
setupNotebookMemory();
// Detect text selection
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) {
// Can't access some pages (chrome://, etc.)
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 {
// Get page HTML content
let pageContent = '';
try {
const [result] = await chrome.scripting.executeScript({
target: { tabId: currentTab.id },
func: () => document.body.innerHTML,
});
pageContent = result?.result || '';
} catch {
// Fallback: just use URL as content
pageContent = `<p>Clipped from <a href="${currentTab.url}">${currentTab.url}</a></p>`;
}
const note = await createNote({
title: currentTab.title || 'Untitled Clip',
content: pageContent,
type: 'CLIP',
url: currentTab.url,
});
showStatus(`Clipped! Note saved.`, 'success');
// Notify background worker
chrome.runtime.sendMessage({
type: 'notify',
title: 'Page Clipped',
message: `"${currentTab.title}" saved to rNotes`,
});
} 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>`;
const note = await createNote({
title: `Selection from ${currentTab.title || 'page'}`,
content: content,
type: 'CLIP',
url: currentTab.url,
});
showStatus(`Selection clipped!`, 'success');
chrome.runtime.sendMessage({
type: 'notify',
title: 'Selection Clipped',
message: `Saved to rNotes`,
});
} 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(`${settings.host}/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) {
// Also save as a note
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');
// Open archive in new tab
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('optionsLink').addEventListener('click', (e) => {
e.preventDefault();
chrome.runtime.openOptionsPage();
});
document.getElementById('openSettings')?.addEventListener('click', (e) => {
e.preventDefault();
chrome.runtime.openOptionsPage();
});
// Init on load
document.addEventListener('DOMContentLoaded', init);