176 lines
7.6 KiB
JavaScript
176 lines
7.6 KiB
JavaScript
const DEFAULT_HOST = 'https://rspace.online';
|
|
|
|
// --- Context Menu Setup ---
|
|
|
|
chrome.runtime.onInstalled.addListener(() => {
|
|
chrome.contextMenus.create({ id: 'clip-page', title: 'Clip page to rSpace', contexts: ['page'] });
|
|
chrome.contextMenus.create({ id: 'save-link', title: 'Save link to rSpace', contexts: ['link'] });
|
|
chrome.contextMenus.create({ id: 'save-image', title: 'Save image to rSpace', contexts: ['image'] });
|
|
chrome.contextMenus.create({ id: 'clip-selection', title: 'Clip selection to rSpace', contexts: ['selection'] });
|
|
chrome.contextMenus.create({ id: 'unlock-article', title: 'Unlock & Clip article to rSpace', contexts: ['page', 'link'] });
|
|
});
|
|
|
|
// --- 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;
|
|
}
|
|
|
|
async function getDefaultNotebook() {
|
|
const result = await chrome.storage.local.get(['lastNotebookId']);
|
|
return result.lastNotebookId || null;
|
|
}
|
|
|
|
function showNotification(title, message) {
|
|
chrome.notifications.create({
|
|
type: 'basic', iconUrl: 'icons/icon-128.png', title, message,
|
|
});
|
|
}
|
|
|
|
async function createNote(data) {
|
|
const token = await getToken();
|
|
if (!token) { showNotification('rSpace Error', 'Not signed in. Open extension settings.'); return; }
|
|
|
|
const settings = await getSettings();
|
|
if (!settings.slug) { showNotification('rSpace Error', 'Configure space slug in extension settings.'); return; }
|
|
|
|
const notebookId = await getDefaultNotebook();
|
|
const body = { title: data.title, content: data.content, type: data.type || 'CLIP', url: data.url };
|
|
if (notebookId) body.notebook_id = notebookId;
|
|
if (data.fileUrl) body.file_url = data.fileUrl;
|
|
if (data.mimeType) body.mime_type = data.mimeType;
|
|
if (data.fileSize) body.file_size = data.fileSize;
|
|
|
|
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 uploadImage(imageUrl) {
|
|
const token = await getToken();
|
|
const settings = await getSettings();
|
|
|
|
const imgResponse = await fetch(imageUrl);
|
|
const blob = await imgResponse.blob();
|
|
let filename;
|
|
try { filename = new URL(imageUrl).pathname.split('/').pop() || `image-${Date.now()}.jpg`; }
|
|
catch { filename = `image-${Date.now()}.jpg`; }
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', blob, filename);
|
|
|
|
const response = await fetch(`${apiBase(settings)}/api/uploads`, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
body: formData,
|
|
});
|
|
if (!response.ok) { const text = await response.text(); throw new Error(`Upload failed: ${response.status} ${text}`); }
|
|
return response.json();
|
|
}
|
|
|
|
async function unlockArticle(url) {
|
|
const token = await getToken();
|
|
if (!token) { showNotification('rSpace Error', 'Not signed in.'); return null; }
|
|
|
|
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 }),
|
|
});
|
|
if (!response.ok) { const text = await response.text(); throw new Error(`Unlock failed: ${response.status} ${text}`); }
|
|
return response.json();
|
|
}
|
|
|
|
// --- Context Menu Handler ---
|
|
|
|
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
|
try {
|
|
switch (info.menuItemId) {
|
|
case 'clip-page': {
|
|
let content = '';
|
|
try {
|
|
const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.body.innerHTML });
|
|
content = result?.result || '';
|
|
} catch { content = `<p>Clipped from <a href="${tab.url}">${tab.url}</a></p>`; }
|
|
await createNote({ title: tab.title || 'Untitled Clip', content, type: 'CLIP', url: tab.url });
|
|
showNotification('Page Clipped', `"${tab.title}" saved to rSpace`);
|
|
break;
|
|
}
|
|
case 'save-link': {
|
|
const linkUrl = info.linkUrl;
|
|
const linkText = info.selectionText || linkUrl;
|
|
await createNote({ title: linkText, content: `<p><a href="${linkUrl}">${linkText}</a></p><p>Found on: <a href="${tab.url}">${tab.title}</a></p>`, type: 'BOOKMARK', url: linkUrl });
|
|
showNotification('Link Saved', 'Bookmark saved to rSpace');
|
|
break;
|
|
}
|
|
case 'save-image': {
|
|
const upload = await uploadImage(info.srcUrl);
|
|
await createNote({ title: `Image from ${tab.title || 'page'}`, content: `<p><img src="${upload.url}" alt="Clipped image" /></p><p>Source: <a href="${tab.url}">${tab.title}</a></p>`, type: 'IMAGE', url: tab.url, fileUrl: upload.url, mimeType: upload.mimeType, fileSize: upload.size });
|
|
showNotification('Image Saved', 'Image saved to rSpace');
|
|
break;
|
|
}
|
|
case 'unlock-article': {
|
|
const targetUrl = info.linkUrl || tab.url;
|
|
showNotification('Unlocking Article', `Finding readable version of ${new URL(targetUrl).hostname}...`);
|
|
const result = await unlockArticle(targetUrl);
|
|
if (result?.success && result.archiveUrl) {
|
|
await createNote({ title: tab.title || 'Unlocked Article', content: `<p>Unlocked via ${result.strategy}</p><p>Original: <a href="${targetUrl}">${targetUrl}</a></p><p>Archive: <a href="${result.archiveUrl}">${result.archiveUrl}</a></p>`, type: 'CLIP', url: targetUrl });
|
|
showNotification('Article Unlocked', `Readable version found via ${result.strategy}`);
|
|
chrome.tabs.create({ url: result.archiveUrl });
|
|
} else {
|
|
showNotification('Unlock Failed', result?.error || 'No archived version found');
|
|
}
|
|
break;
|
|
}
|
|
case 'clip-selection': {
|
|
let content = '';
|
|
try {
|
|
const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { const s = window.getSelection(); if (!s || s.rangeCount === 0) return ''; const r = s.getRangeAt(0); const d = document.createElement('div'); d.appendChild(r.cloneContents()); return d.innerHTML; } });
|
|
content = result?.result || '';
|
|
} catch { content = `<p>${info.selectionText || ''}</p>`; }
|
|
if (!content && info.selectionText) content = `<p>${info.selectionText}</p>`;
|
|
await createNote({ title: `Selection from ${tab.title || 'page'}`, content, type: 'CLIP', url: tab.url });
|
|
showNotification('Selection Clipped', 'Saved to rSpace');
|
|
break;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Context menu action failed:', err);
|
|
showNotification('rSpace Error', err.message || 'Failed to save');
|
|
}
|
|
});
|
|
|
|
// --- Keyboard shortcut handler ---
|
|
|
|
chrome.commands.onCommand.addListener(async (command) => {
|
|
if (command === 'open-voice-recorder') {
|
|
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 });
|
|
}
|
|
});
|
|
|
|
// --- Message Handler ---
|
|
|
|
chrome.runtime.onMessage.addListener((message) => {
|
|
if (message.type === 'notify') showNotification(message.title, message.message);
|
|
});
|