Reset web-clipper to origin/main baseline for MV3 migration

This commit is contained in:
Octech2722 2025-10-11 20:33:18 -05:00
parent 0c1de7e183
commit a00ea2e91d
8 changed files with 357 additions and 542 deletions

View File

@ -4,14 +4,12 @@
**Trilium is in maintenance mode and Web Clipper is not likely to get new releases.** **Trilium is in maintenance mode and Web Clipper is not likely to get new releases.**
Trilium Web Clipper is a web browser extension which allows user to clip text, screenshots, whole pages and short notes and save them directly to [Trilium Notes](https://github.com/zadam/trilium). Trilium Web Clipper is a web browser extension which allows user to clip text, screenshots, whole pages and short notes and save them directly to [Trilium Notes](https://github.com/zadam/trilium).
For more details, see the [wiki page](https://github.com/zadam/trilium/wiki/Web-clipper). For more details, see the [wiki page](https://github.com/zadam/trilium/wiki/Web-clipper).
## Keyboard shortcuts ## Keyboard shortcuts
Keyboard shortcuts are available for most functions:
Keyboard shortcuts are available for most functions:
* Save selected text: `Ctrl+Shift+S` (Mac: `Cmd+Shift+S`) * Save selected text: `Ctrl+Shift+S` (Mac: `Cmd+Shift+S`)
* Save whole page: `Alt+Shift+S` (Mac: `Opt+Shift+S`) * Save whole page: `Alt+Shift+S` (Mac: `Opt+Shift+S`)
* Save screenshot: `Ctrl+Shift+E` (Mac: `Cmd+Shift+E`) * Save screenshot: `Ctrl+Shift+E` (Mac: `Cmd+Shift+E`)
@ -23,5 +21,4 @@ To set custom shortcuts, follow the directions for your browser.
**Chrome**: `chrome://extensions/shortcuts` **Chrome**: `chrome://extensions/shortcuts`
## Credits ## Credits
Some parts of the code are based on the [Joplin Notes browser extension](https://github.com/laurent22/joplin/tree/master/Clipper). Some parts of the code are based on the [Joplin Notes browser extension](https://github.com/laurent22/joplin/tree/master/Clipper).

View File

@ -1,7 +1,3 @@
// Import modules
import { randomString } from './utils.js';
import { triliumServerFacade } from './trilium_server_facade.js';
// Keyboard shortcuts // Keyboard shortcuts
chrome.commands.onCommand.addListener(async function (command) { chrome.commands.onCommand.addListener(async function (command) {
if (command == "saveSelection") { if (command == "saveSelection") {
@ -12,6 +8,7 @@ chrome.commands.onCommand.addListener(async function (command) {
await saveTabs(); await saveTabs();
} else if (command == "saveCroppedScreenshot") { } else if (command == "saveCroppedScreenshot") {
const activeTab = await getActiveTab(); const activeTab = await getActiveTab();
await saveCroppedScreenshot(activeTab.url); await saveCroppedScreenshot(activeTab.url);
} else { } else {
console.log("Unrecognized command", command); console.log("Unrecognized command", command);
@ -19,547 +16,436 @@ chrome.commands.onCommand.addListener(async function (command) {
}); });
function cropImage(newArea, dataUrl) { function cropImage(newArea, dataUrl) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
img.onload = function () { img.onload = function () {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = newArea.width; canvas.width = newArea.width;
canvas.height = newArea.height; canvas.height = newArea.height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.drawImage(img, newArea.x, newArea.y, newArea.width, newArea.height, 0, 0, newArea.width, newArea.height); ctx.drawImage(img, newArea.x, newArea.y, newArea.width, newArea.height, 0, 0, newArea.width, newArea.height);
resolve(canvas.toDataURL()); resolve(canvas.toDataURL());
}; };
img.src = dataUrl; img.src = dataUrl;
}); });
} }
async function takeCroppedScreenshot(cropRect) { async function takeCroppedScreenshot(cropRect) {
const activeTab = await getActiveTab(); const activeTab = await getActiveTab();
const zoom = await chrome.tabs.getZoom(activeTab.id) * globalThis.devicePixelRatio || 1; const zoom = await browser.tabs.getZoom(activeTab.id) * window.devicePixelRatio;
const newArea = Object.assign({}, cropRect); const newArea = Object.assign({}, cropRect);
newArea.x *= zoom; newArea.x *= zoom;
newArea.y *= zoom; newArea.y *= zoom;
newArea.width *= zoom; newArea.width *= zoom;
newArea.height *= zoom; newArea.height *= zoom;
const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' }); const dataUrl = await browser.tabs.captureVisibleTab(null, { format: 'png' });
return await cropImage(newArea, dataUrl); return await cropImage(newArea, dataUrl);
} }
async function takeWholeScreenshot() { async function takeWholeScreenshot() {
// this saves only visible portion of the page // this saves only visible portion of the page
// workaround to save the whole page is to scroll & stitch // workaround to save the whole page is to scroll & stitch
// example in https://github.com/mrcoles/full-page-screen-capture-chrome-extension // example in https://github.com/mrcoles/full-page-screen-capture-chrome-extension
// see page.js and popup.js // see page.js and popup.js
return await chrome.tabs.captureVisibleTab(null, { format: 'png' }); return await browser.tabs.captureVisibleTab(null, { format: 'png' });
} }
chrome.runtime.onInstalled.addListener(() => { browser.runtime.onInstalled.addListener(() => {
if (isDevEnv()) { if (isDevEnv()) {
chrome.action.setIcon({ browser.browserAction.setIcon({
path: 'icons/32-dev.png', path: 'icons/32-dev.png',
}); });
} }
}); });
// Context menus browser.contextMenus.create({
chrome.contextMenus.create({ id: "trilium-save-selection",
id: "trilium-save-selection", title: "Save selection to Trilium",
title: "Save selection to Trilium", contexts: ["selection"]
contexts: ["selection"]
}); });
chrome.contextMenus.create({ browser.contextMenus.create({
id: "trilium-save-cropped-screenshot", id: "trilium-save-cropped-screenshot",
title: "Clip screenshot to Trilium", title: "Clip screenshot to Trilium",
contexts: ["page"] contexts: ["page"]
}); });
chrome.contextMenus.create({ browser.contextMenus.create({
id: "trilium-save-whole-screenshot", id: "trilium-save-cropped-screenshot",
title: "Save whole screen shot to Trilium", title: "Crop screen shot to Trilium",
contexts: ["page"] contexts: ["page"]
}); });
chrome.contextMenus.create({ browser.contextMenus.create({
id: "trilium-save-page", id: "trilium-save-whole-screenshot",
title: "Save whole page to Trilium", title: "Save whole screen shot to Trilium",
contexts: ["page"] contexts: ["page"]
}); });
chrome.contextMenus.create({ browser.contextMenus.create({
id: "trilium-save-link", id: "trilium-save-page",
title: "Save link to Trilium", title: "Save whole page to Trilium",
contexts: ["link"] contexts: ["page"]
}); });
chrome.contextMenus.create({ browser.contextMenus.create({
id: "trilium-save-image", id: "trilium-save-link",
title: "Save image to Trilium", title: "Save link to Trilium",
contexts: ["image"] contexts: ["link"]
});
browser.contextMenus.create({
id: "trilium-save-image",
title: "Save image to Trilium",
contexts: ["image"]
}); });
async function getActiveTab() { async function getActiveTab() {
const tabs = await chrome.tabs.query({ const tabs = await browser.tabs.query({
active: true, active: true,
currentWindow: true currentWindow: true
}); });
return tabs[0]; return tabs[0];
} }
async function getWindowTabs() { async function getWindowTabs() {
const tabs = await chrome.tabs.query({ const tabs = await browser.tabs.query({
currentWindow: true currentWindow: true
}); });
return tabs; return tabs;
} }
async function sendMessageToActiveTab(message) { async function sendMessageToActiveTab(message) {
const activeTab = await getActiveTab(); const activeTab = await getActiveTab();
if (!activeTab) { if (!activeTab) {
throw new Error("No active tab."); throw new Error("No active tab.");
} }
// In Manifest V3, we need to inject content script if not already present try {
try { return await browser.tabs.sendMessage(activeTab.id, message);
return await chrome.tabs.sendMessage(activeTab.id, message); }
} catch (error) { catch (e) {
// Content script might not be injected, try to inject it throw e;
try { }
await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
files: ['content.js']
});
// Wait a bit for the script to initialize
await new Promise(resolve => setTimeout(resolve, 200));
return await chrome.tabs.sendMessage(activeTab.id, message);
} catch (injectionError) {
console.error('Failed to inject content script:', injectionError);
throw new Error(`Failed to communicate with page: ${injectionError.message}`);
}
}
} }
async function toast(message, noteId = null, tabIds = null) { function toast(message, noteId = null, tabIds = null) {
try { sendMessageToActiveTab({
await sendMessageToActiveTab({ name: 'toast',
name: 'toast', message: message,
message: message, noteId: noteId,
noteId: noteId, tabIds: tabIds
tabIds: tabIds });
});
} catch (error) {
console.error('Failed to show toast:', error);
}
}
function showStatusToast(message, isProgress = true) {
// Make this completely async and fire-and-forget
// Only try to send status if we're confident the content script will be ready
(async () => {
try {
// Test if content script is ready with a quick ping
const activeTab = await getActiveTab();
if (!activeTab) return;
await chrome.tabs.sendMessage(activeTab.id, { name: 'ping' });
// If ping succeeds, send the status toast
await chrome.tabs.sendMessage(activeTab.id, {
name: 'status-toast',
message: message,
isProgress: isProgress
});
} catch (error) {
// Content script not ready or failed - silently skip
}
})();
}
function updateStatusToast(message, isProgress = true) {
// Make this completely async and fire-and-forget
(async () => {
try {
const activeTab = await getActiveTab();
if (!activeTab) return;
// Direct message without injection logic since content script should be ready by now
await chrome.tabs.sendMessage(activeTab.id, {
name: 'update-status-toast',
message: message,
isProgress: isProgress
});
} catch (error) {
// Content script not ready or failed - silently skip
}
})();
} }
function blob2base64(blob) { function blob2base64(blob) {
return new Promise(resolve => { return new Promise(resolve => {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = function() { reader.onloadend = function() {
resolve(reader.result); resolve(reader.result);
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
} }
async function fetchImage(url) { async function fetchImage(url) {
const resp = await fetch(url); const resp = await fetch(url);
const blob = await resp.blob(); const blob = await resp.blob();
return await blob2base64(blob); return await blob2base64(blob);
} }
async function postProcessImage(image) { async function postProcessImage(image) {
if (image.src.startsWith("data:image/")) { if (image.src.startsWith("data:image/")) {
image.dataUrl = image.src; image.dataUrl = image.src;
image.src = "inline." + image.src.substr(11, 3); // this should extract file type - png/jpg image.src = "inline." + image.src.substr(11, 3); // this should extract file type - png/jpg
} }
else { else {
try { try {
image.dataUrl = await fetchImage(image.src, image); image.dataUrl = await fetchImage(image.src, image);
} }
catch (e) { catch (e) {
console.log(`Cannot fetch image from ${image.src}`); console.log(`Cannot fetch image from ${image.src}`);
} }
} }
} }
async function postProcessImages(resp) { async function postProcessImages(resp) {
if (resp && resp.images) { if (resp.images) {
for (const image of resp.images) { for (const image of resp.images) {
await postProcessImage(image); await postProcessImage(image);
} }
} }
} }
async function saveSelection() { async function saveSelection() {
showStatusToast("📝 Capturing selection..."); const payload = await sendMessageToActiveTab({name: 'trilium-save-selection'});
const payload = await sendMessageToActiveTab({name: 'trilium-save-selection'}); await postProcessImages(payload);
if (!payload) { const resp = await triliumServerFacade.callService('POST', 'clippings', payload);
console.error('No payload received from content script');
updateStatusToast("❌ Failed to capture selection", false);
return;
}
if (payload.images && payload.images.length > 0) { if (!resp) {
updateStatusToast(`🖼️ Processing ${payload.images.length} image(s)...`); return;
} }
await postProcessImages(payload);
const triliumType = triliumServerFacade.triliumSearch?.status === 'found-desktop' ? 'Desktop' : 'Server'; toast("Selection has been saved to Trilium.", resp.noteId);
updateStatusToast(`💾 Saving to Trilium ${triliumType}...`);
const resp = await triliumServerFacade.callService('POST', 'clippings', payload);
if (!resp) {
updateStatusToast("❌ Failed to save to Trilium", false);
return;
}
await toast("✅ Selection has been saved to Trilium.", resp.noteId);
} }
async function getImagePayloadFromSrc(src, pageUrl) { async function getImagePayloadFromSrc(src, pageUrl) {
const image = { const image = {
imageId: randomString(20), imageId: randomString(20),
src: src src: src
}; };
await postProcessImage(image); await postProcessImage(image);
const activeTab = await getActiveTab(); const activeTab = await getActiveTab();
return { return {
title: activeTab.title, title: activeTab.title,
content: `<img src="${image.imageId}">`, content: `<img src="${image.imageId}">`,
images: [image], images: [image],
pageUrl: pageUrl pageUrl: pageUrl
}; };
} }
async function saveCroppedScreenshot(pageUrl) { async function saveCroppedScreenshot(pageUrl) {
showStatusToast("📷 Preparing screenshot..."); const cropRect = await sendMessageToActiveTab({name: 'trilium-get-rectangle-for-screenshot'});
const cropRect = await sendMessageToActiveTab({name: 'trilium-get-rectangle-for-screenshot'}); const src = await takeCroppedScreenshot(cropRect);
updateStatusToast("📸 Capturing screenshot..."); const payload = await getImagePayloadFromSrc(src, pageUrl);
const src = await takeCroppedScreenshot(cropRect);
const payload = await getImagePayloadFromSrc(src, pageUrl); const resp = await triliumServerFacade.callService("POST", "clippings", payload);
const triliumType = triliumServerFacade.triliumSearch?.status === 'found-desktop' ? 'Desktop' : 'Server'; if (!resp) {
updateStatusToast(`💾 Saving to Trilium ${triliumType}...`); return;
}
const resp = await triliumServerFacade.callService("POST", "clippings", payload); toast("Screenshot has been saved to Trilium.", resp.noteId);
if (!resp) {
updateStatusToast("❌ Failed to save screenshot", false);
return;
}
await toast("✅ Screenshot has been saved to Trilium.", resp.noteId);
} }
async function saveWholeScreenshot(pageUrl) { async function saveWholeScreenshot(pageUrl) {
showStatusToast("📸 Capturing full screenshot..."); const src = await takeWholeScreenshot();
const src = await takeWholeScreenshot(); const payload = await getImagePayloadFromSrc(src, pageUrl);
const payload = await getImagePayloadFromSrc(src, pageUrl); const resp = await triliumServerFacade.callService("POST", "clippings", payload);
const triliumType = triliumServerFacade.triliumSearch?.status === 'found-desktop' ? 'Desktop' : 'Server'; if (!resp) {
updateStatusToast(`💾 Saving to Trilium ${triliumType}...`); return;
}
const resp = await triliumServerFacade.callService("POST", "clippings", payload); toast("Screenshot has been saved to Trilium.", resp.noteId);
if (!resp) {
updateStatusToast("❌ Failed to save screenshot", false);
return;
}
await toast("✅ Screenshot has been saved to Trilium.", resp.noteId);
} }
async function saveImage(srcUrl, pageUrl) { async function saveImage(srcUrl, pageUrl) {
const payload = await getImagePayloadFromSrc(srcUrl, pageUrl); const payload = await getImagePayloadFromSrc(srcUrl, pageUrl);
const resp = await triliumServerFacade.callService("POST", "clippings", payload); const resp = await triliumServerFacade.callService("POST", "clippings", payload);
if (!resp) { if (!resp) {
return; return;
} }
await toast("Image has been saved to Trilium.", resp.noteId); toast("Image has been saved to Trilium.", resp.noteId);
} }
async function saveWholePage() { async function saveWholePage() {
// Step 1: Show initial status (completely non-blocking) const payload = await sendMessageToActiveTab({name: 'trilium-save-page'});
showStatusToast("📄 Page capture started...");
const payload = await sendMessageToActiveTab({name: 'trilium-save-page'}); await postProcessImages(payload);
if (!payload) { const resp = await triliumServerFacade.callService('POST', 'notes', payload);
console.error('No payload received from content script');
updateStatusToast("❌ Failed to capture page content", false);
return;
}
// Step 2: Processing images if (!resp) {
if (payload.images && payload.images.length > 0) { return;
updateStatusToast(`🖼️ Processing ${payload.images.length} image(s)...`); }
}
await postProcessImages(payload);
// Step 3: Saving to Trilium toast("Page has been saved to Trilium.", resp.noteId);
const triliumType = triliumServerFacade.triliumSearch?.status === 'found-desktop' ? 'Desktop' : 'Server';
updateStatusToast(`💾 Saving to Trilium ${triliumType}...`);
const resp = await triliumServerFacade.callService('POST', 'notes', payload);
if (!resp) {
updateStatusToast("❌ Failed to save to Trilium", false);
return;
}
// Step 4: Success with link
await toast("✅ Page has been saved to Trilium.", resp.noteId);
} }
async function saveLinkWithNote(title, content) { async function saveLinkWithNote(title, content) {
const activeTab = await getActiveTab(); const activeTab = await getActiveTab();
if (!title.trim()) { if (!title.trim()) {
title = activeTab.title; title = activeTab.title;
} }
const resp = await triliumServerFacade.callService('POST', 'notes', { const resp = await triliumServerFacade.callService('POST', 'notes', {
title: title, title: title,
content: content, content: content,
clipType: 'note', clipType: 'note',
pageUrl: activeTab.url pageUrl: activeTab.url
}); });
if (!resp) { if (!resp) {
return false; return false;
} }
await toast("Link with note has been saved to Trilium.", resp.noteId); toast("Link with note has been saved to Trilium.", resp.noteId);
return true; return true;
} }
async function getTabsPayload(tabs) { async function getTabsPayload(tabs) {
let content = '<ul>'; let content = '<ul>';
tabs.forEach(tab => { tabs.forEach(tab => {
content += `<li><a href="${tab.url}">${tab.title}</a></li>` content += `<li><a href="${tab.url}">${tab.title}</a></li>`
}); });
content += '</ul>'; content += '</ul>';
const domainsCount = tabs.map(tab => tab.url) const domainsCount = tabs.map(tab => tab.url)
.reduce((acc, url) => { .reduce((acc, url) => {
const hostname = new URL(url).hostname const hostname = new URL(url).hostname
return acc.set(hostname, (acc.get(hostname) || 0) + 1) return acc.set(hostname, (acc.get(hostname) || 0) + 1)
}, new Map()); }, new Map());
let topDomains = [...domainsCount] let topDomains = [...domainsCount]
.sort((a, b) => {return b[1]-a[1]}) .sort((a, b) => {return b[1]-a[1]})
.slice(0,3) .slice(0,3)
.map(domain=>domain[0]) .map(domain=>domain[0])
.join(', ') .join(', ')
if (tabs.length > 3) { topDomains += '...' } if (tabs.length > 3) { topDomains += '...' }
return { return {
title: `${tabs.length} browser tabs: ${topDomains}`, title: `${tabs.length} browser tabs: ${topDomains}`,
content: content, content: content,
clipType: 'tabs' clipType: 'tabs'
}; };
} }
async function saveTabs() { async function saveTabs() {
const tabs = await getWindowTabs(); const tabs = await getWindowTabs();
const payload = await getTabsPayload(tabs); const payload = await getTabsPayload(tabs);
const resp = await triliumServerFacade.callService('POST', 'notes', payload); const resp = await triliumServerFacade.callService('POST', 'notes', payload);
if (!resp) { if (!resp) {
return; return;
} }
const tabIds = tabs.map(tab=>{return tab.id}); const tabIds = tabs.map(tab=>{return tab.id});
await toast(`${tabs.length} links have been saved to Trilium.`, resp.noteId, tabIds); toast(`${tabs.length} links have been saved to Trilium.`, resp.noteId, tabIds);
} }
// Helper function browser.contextMenus.onClicked.addListener(async function(info, tab) {
function isDevEnv() { if (info.menuItemId === 'trilium-save-selection') {
const manifest = chrome.runtime.getManifest(); await saveSelection();
return manifest.name.endsWith('(dev)'); }
} else if (info.menuItemId === 'trilium-save-cropped-screenshot') {
await saveCroppedScreenshot(info.pageUrl);
}
else if (info.menuItemId === 'trilium-save-whole-screenshot') {
await saveWholeScreenshot(info.pageUrl);
}
else if (info.menuItemId === 'trilium-save-image') {
await saveImage(info.srcUrl, info.pageUrl);
}
else if (info.menuItemId === 'trilium-save-link') {
const link = document.createElement("a");
link.href = info.linkUrl;
// linkText might be available only in firefox
link.appendChild(document.createTextNode(info.linkText || info.linkUrl));
chrome.contextMenus.onClicked.addListener(async function(info, tab) { const activeTab = await getActiveTab();
if (info.menuItemId === 'trilium-save-selection') {
await saveSelection();
}
else if (info.menuItemId === 'trilium-save-cropped-screenshot') {
await saveCroppedScreenshot(info.pageUrl);
}
else if (info.menuItemId === 'trilium-save-whole-screenshot') {
await saveWholeScreenshot(info.pageUrl);
}
else if (info.menuItemId === 'trilium-save-image') {
await saveImage(info.srcUrl, info.pageUrl);
}
else if (info.menuItemId === 'trilium-save-link') {
const link = document.createElement("a");
link.href = info.linkUrl;
// linkText might be available only in firefox
link.appendChild(document.createTextNode(info.linkText || info.linkUrl));
const activeTab = await getActiveTab(); const resp = await triliumServerFacade.callService('POST', 'clippings', {
title: activeTab.title,
content: link.outerHTML,
pageUrl: info.pageUrl
});
const resp = await triliumServerFacade.callService('POST', 'clippings', { if (!resp) {
title: activeTab.title, return;
content: link.outerHTML, }
pageUrl: info.pageUrl
});
if (!resp) { toast("Link has been saved to Trilium.", resp.noteId);
return; }
} else if (info.menuItemId === 'trilium-save-page') {
await saveWholePage();
await toast("Link has been saved to Trilium.", resp.noteId); }
} else {
else if (info.menuItemId === 'trilium-save-page') { console.log("Unrecognized menuItemId", info.menuItemId);
await saveWholePage(); }
}
else {
console.log("Unrecognized menuItemId", info.menuItemId);
}
}); });
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { browser.runtime.onMessage.addListener(async request => {
console.log("Received", request); console.log("Received", request);
if (request.name === 'openNoteInTrilium') { if (request.name === 'openNoteInTrilium') {
const resp = await triliumServerFacade.callService('POST', 'open/' + request.noteId); const resp = await triliumServerFacade.callService('POST', 'open/' + request.noteId);
if (!resp) { if (!resp) {
return; return;
} }
// desktop app is not available so we need to open in browser // desktop app is not available so we need to open in browser
if (resp.result === 'open-in-browser') { if (resp.result === 'open-in-browser') {
const {triliumServerUrl} = await chrome.storage.sync.get("triliumServerUrl"); const {triliumServerUrl} = await browser.storage.sync.get("triliumServerUrl");
if (triliumServerUrl) { if (triliumServerUrl) {
const noteUrl = triliumServerUrl + '/#' + request.noteId; const noteUrl = triliumServerUrl + '/#' + request.noteId;
console.log("Opening new tab in browser", noteUrl); console.log("Opening new tab in browser", noteUrl);
chrome.tabs.create({ browser.tabs.create({
url: noteUrl url: noteUrl
}); });
} }
else { else {
console.error("triliumServerUrl not found in local storage."); console.error("triliumServerUrl not found in local storage.");
} }
} }
} }
else if (request.name === 'closeTabs') { else if (request.name === 'closeTabs') {
return await chrome.tabs.remove(request.tabIds) return await browser.tabs.remove(request.tabIds)
} }
else if (request.name === 'load-script') { else if (request.name === 'load-script') {
return await chrome.scripting.executeScript({ return await browser.tabs.executeScript({file: request.file});
target: { tabId: sender.tab?.id }, }
files: [request.file] else if (request.name === 'save-cropped-screenshot') {
}); const activeTab = await getActiveTab();
}
else if (request.name === 'save-cropped-screenshot') {
const activeTab = await getActiveTab();
return await saveCroppedScreenshot(activeTab.url);
}
else if (request.name === 'save-whole-screenshot') {
const activeTab = await getActiveTab();
return await saveWholeScreenshot(activeTab.url);
}
else if (request.name === 'save-whole-page') {
return await saveWholePage();
}
else if (request.name === 'save-link-with-note') {
return await saveLinkWithNote(request.title, request.content);
}
else if (request.name === 'save-tabs') {
return await saveTabs();
}
else if (request.name === 'trigger-trilium-search') {
triliumServerFacade.triggerSearchForTrilium();
}
else if (request.name === 'send-trilium-search-status') {
triliumServerFacade.sendTriliumSearchStatusToPopup();
}
else if (request.name === 'trigger-trilium-search-note-url') {
const activeTab = await getActiveTab();
triliumServerFacade.triggerSearchNoteByUrl(activeTab.url);
}
// Important: return true to indicate async response return await saveCroppedScreenshot(activeTab.url);
return true; }
else if (request.name === 'save-whole-screenshot') {
const activeTab = await getActiveTab();
return await saveWholeScreenshot(activeTab.url);
}
else if (request.name === 'save-whole-page') {
return await saveWholePage();
}
else if (request.name === 'save-link-with-note') {
return await saveLinkWithNote(request.title, request.content);
}
else if (request.name === 'save-tabs') {
return await saveTabs();
}
else if (request.name === 'trigger-trilium-search') {
triliumServerFacade.triggerSearchForTrilium();
}
else if (request.name === 'send-trilium-search-status') {
triliumServerFacade.sendTriliumSearchStatusToPopup();
}
else if (request.name === 'trigger-trilium-search-note-url') {
const activeTab = await getActiveTab();
triliumServerFacade.triggerSearchNoteByUrl(activeTab.url);
}
}); });

View File

@ -1,33 +1,3 @@
// Utility functions (inline to avoid module dependency issues)
function randomString(len) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function getBaseUrl() {
let output = getPageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') {
output = output.split('/');
output.pop();
output = output.join('/');
}
return output;
}
function getPageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null" in this case.
return location.protocol === 'file:' ? 'file://' : location.origin;
}
function absoluteUrl(url) { function absoluteUrl(url) {
if (!url) { if (!url) {
return url; return url;
@ -75,19 +45,19 @@ function getReadableDocument() {
function getDocumentDates() { function getDocumentDates() {
var dates = { var dates = {
publishedDate: null, publishedDate: null,
modifiedDate: null, modifiedDate: null,
}; };
const articlePublishedTime = document.querySelector("meta[property='article:published_time']"); const articlePublishedTime = document.querySelector("meta[property='article:published_time']");
if (articlePublishedTime && articlePublishedTime.getAttribute('content')) { if (articlePublishedTime && articlePublishedTime.getAttribute('content')) {
dates.publishedDate = new Date(articlePublishedTime.getAttribute('content')); dates.publishedDate = new Date(articlePublishedTime.getAttribute('content'));
} }
const articleModifiedTime = document.querySelector("meta[property='article:modified_time']"); const articleModifiedTime = document.querySelector("meta[property='article:modified_time']");
if (articleModifiedTime && articleModifiedTime.getAttribute('content')) { if (articleModifiedTime && articleModifiedTime.getAttribute('content')) {
dates.modifiedDate = new Date(articleModifiedTime.getAttribute('content')); dates.modifiedDate = new Date(articleModifiedTime.getAttribute('content'));
} }
// TODO: if we didn't get dates from meta, then try to get them from JSON-LD // TODO: if we didn't get dates from meta, then try to get them from JSON-LD
return dates; return dates;
@ -265,7 +235,7 @@ function createLink(clickAction, text, color = "lightskyblue") {
link.style.color = color; link.style.color = color;
link.appendChild(document.createTextNode(text)); link.appendChild(document.createTextNode(text));
link.addEventListener("click", () => { link.addEventListener("click", () => {
chrome.runtime.sendMessage(null, clickAction) browser.runtime.sendMessage(null, clickAction)
}); });
return link return link
@ -274,10 +244,7 @@ function createLink(clickAction, text, color = "lightskyblue") {
async function prepareMessageResponse(message) { async function prepareMessageResponse(message) {
console.info('Message: ' + message.name); console.info('Message: ' + message.name);
if (message.name === "ping") { if (message.name === "toast") {
return { success: true };
}
else if (message.name === "toast") {
let messageText; let messageText;
if (message.noteId) { if (message.noteId) {
@ -310,42 +277,6 @@ async function prepareMessageResponse(message) {
duration: 7000 duration: 7000
} }
}); });
return { success: true }; // Return a response
}
else if (message.name === "status-toast") {
await requireLib('/lib/toast.js');
// Hide any existing status toast
if (window.triliumStatusToast && window.triliumStatusToast.hide) {
window.triliumStatusToast.hide();
}
// Store reference to the status toast so we can replace it
window.triliumStatusToast = showToast(message.message, {
settings: {
duration: message.isProgress ? 60000 : 5000 // Long duration for progress, shorter for errors
}
});
return { success: true }; // Return a response
}
else if (message.name === "update-status-toast") {
await requireLib('/lib/toast.js');
// Hide the previous status toast
if (window.triliumStatusToast && window.triliumStatusToast.hide) {
window.triliumStatusToast.hide();
}
// Show new toast with updated message
window.triliumStatusToast = showToast(message.message, {
settings: {
duration: message.isProgress ? 60000 : 5000
}
});
return { success: true }; // Return a response
} }
else if (message.name === "trilium-save-selection") { else if (message.name === "trilium-save-selection") {
const container = document.createElement('div'); const container = document.createElement('div');
@ -407,10 +338,7 @@ async function prepareMessageResponse(message) {
} }
} }
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { browser.runtime.onMessage.addListener(prepareMessageResponse);
prepareMessageResponse(message).then(sendResponse);
return true; // Important: indicates async response
});
const loadedLibs = []; const loadedLibs = [];
@ -418,6 +346,6 @@ async function requireLib(libPath) {
if (!loadedLibs.includes(libPath)) { if (!loadedLibs.includes(libPath)) {
loadedLibs.push(libPath); loadedLibs.push(libPath);
await chrome.runtime.sendMessage({name: 'load-script', file: libPath}); await browser.runtime.sendMessage({name: 'load-script', file: libPath});
} }
} }

View File

@ -1,12 +1,10 @@
{ {
"manifest_version": 3, "manifest_version": 2,
"name": "Trilium Web Clipper (dev)", "name": "Trilium Web Clipper (dev)",
"version": "1.0.1", "version": "1.0.1",
"description": "Save web clippings to Trilium Notes.", "description": "Save web clippings to Trilium Notes.",
"homepage_url": "https://github.com/zadam/trilium-web-clipper", "homepage_url": "https://github.com/zadam/trilium-web-clipper",
"content_security_policy": { "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"extension_pages": "script-src 'self'; object-src 'self'"
},
"icons": { "icons": {
"32": "icons/32.png", "32": "icons/32.png",
"48": "icons/48.png", "48": "icons/48.png",
@ -15,30 +13,37 @@
"permissions": [ "permissions": [
"activeTab", "activeTab",
"tabs", "tabs",
"storage",
"contextMenus",
"scripting"
],
"host_permissions": [
"http://*/", "http://*/",
"https://*/" "https://*/",
"<all_urls>",
"storage",
"contextMenus"
], ],
"action": { "browser_action": {
"default_icon": "icons/32.png", "default_icon": "icons/32.png",
"default_title": "Trilium Web Clipper", "default_title": "Trilium Web Clipper",
"default_popup": "popup/popup.html" "default_popup": "popup/popup.html"
}, },
"content_scripts": [], "content_scripts": [
"background": {
"service_worker": "background.js",
"type": "module"
},
"web_accessible_resources": [
{ {
"resources": ["lib/*", "utils.js", "trilium_server_facade.js", "content.js"], "matches": [
"matches": ["<all_urls>"] "<all_urls>"
],
"js": [
"lib/browser-polyfill.js",
"utils.js",
"content.js"
]
} }
], ],
"background": {
"scripts": [
"lib/browser-polyfill.js",
"utils.js",
"trilium_server_facade.js",
"background.js"
]
},
"options_ui": { "options_ui": {
"page": "options/options.html" "page": "options/options.html"
}, },

View File

@ -56,7 +56,7 @@ async function saveTriliumServerSetup(e) {
$triliumServerPassword.val(''); $triliumServerPassword.val('');
chrome.storage.sync.set({ browser.storage.sync.set({
triliumServerUrl: $triliumServerUrl.val(), triliumServerUrl: $triliumServerUrl.val(),
authToken: json.token authToken: json.token
}); });
@ -73,7 +73,7 @@ const $resetTriliumServerSetupLink = $("#reset-trilium-server-setup");
$resetTriliumServerSetupLink.on("click", e => { $resetTriliumServerSetupLink.on("click", e => {
e.preventDefault(); e.preventDefault();
chrome.storage.sync.set({ browser.storage.sync.set({
triliumServerUrl: '', triliumServerUrl: '',
authToken: '' authToken: ''
}); });
@ -97,7 +97,7 @@ $triilumDesktopSetupForm.on("submit", e => {
return; return;
} }
chrome.storage.sync.set({ browser.storage.sync.set({
triliumDesktopPort: port triliumDesktopPort: port
}); });
@ -105,8 +105,8 @@ $triilumDesktopSetupForm.on("submit", e => {
}); });
async function restoreOptions() { async function restoreOptions() {
const {triliumServerUrl} = await chrome.storage.sync.get("triliumServerUrl"); const {triliumServerUrl} = await browser.storage.sync.get("triliumServerUrl");
const {authToken} = await chrome.storage.sync.get("authToken"); const {authToken} = await browser.storage.sync.get("authToken");
$errorMessage.hide(); $errorMessage.hide();
$successMessage.hide(); $successMessage.hide();
@ -127,7 +127,7 @@ async function restoreOptions() {
$triliumServerConfiguredDiv.hide(); $triliumServerConfiguredDiv.hide();
} }
const {triliumDesktopPort} = await chrome.storage.sync.get("triliumDesktopPort"); const {triliumDesktopPort} = await browser.storage.sync.get("triliumDesktopPort");
$triliumDesktopPort.val(triliumDesktopPort); $triliumDesktopPort.val(triliumDesktopPort);
} }

View File

@ -1,6 +1,6 @@
async function sendMessage(message) { async function sendMessage(message) {
try { try {
return await chrome.runtime.sendMessage(message); return await browser.runtime.sendMessage(message);
} }
catch (e) { catch (e) {
console.log("Calling browser runtime failed:", e); console.log("Calling browser runtime failed:", e);
@ -15,7 +15,7 @@ const $saveWholeScreenShotButton = $("#save-whole-screenshot-button");
const $saveWholePageButton = $("#save-whole-page-button"); const $saveWholePageButton = $("#save-whole-page-button");
const $saveTabsButton = $("#save-tabs-button"); const $saveTabsButton = $("#save-tabs-button");
$showOptionsButton.on("click", () => chrome.runtime.openOptionsPage()); $showOptionsButton.on("click", () => browser.runtime.openOptionsPage());
$saveCroppedScreenShotButton.on("click", () => { $saveCroppedScreenShotButton.on("click", () => {
sendMessage({name: 'save-cropped-screenshot'}); sendMessage({name: 'save-cropped-screenshot'});
@ -115,7 +115,7 @@ const $connectionStatus = $("#connection-status");
const $needsConnection = $(".needs-connection"); const $needsConnection = $(".needs-connection");
const $alreadyVisited = $("#already-visited"); const $alreadyVisited = $("#already-visited");
chrome.runtime.onMessage.addListener(request => { browser.runtime.onMessage.addListener(request => {
if (request.name === 'trilium-search-status') { if (request.name === 'trilium-search-status') {
const {triliumSearch} = request; const {triliumSearch} = request;
@ -146,7 +146,7 @@ chrome.runtime.onMessage.addListener(request => {
if (isConnected) { if (isConnected) {
$needsConnection.removeAttr("disabled"); $needsConnection.removeAttr("disabled");
$needsConnection.removeAttr("title"); $needsConnection.removeAttr("title");
chrome.runtime.sendMessage({name: "trigger-trilium-search-note-url"}); browser.runtime.sendMessage({name: "trigger-trilium-search-note-url"});
} }
else { else {
$needsConnection.attr("disabled", "disabled"); $needsConnection.attr("disabled", "disabled");
@ -164,7 +164,7 @@ chrome.runtime.onMessage.addListener(request => {
}else{ }else{
$alreadyVisited.html(''); $alreadyVisited.html('');
} }
} }
}); });
@ -172,9 +172,9 @@ chrome.runtime.onMessage.addListener(request => {
const $checkConnectionButton = $("#check-connection-button"); const $checkConnectionButton = $("#check-connection-button");
$checkConnectionButton.on("click", () => { $checkConnectionButton.on("click", () => {
chrome.runtime.sendMessage({ browser.runtime.sendMessage({
name: "trigger-trilium-search" name: "trigger-trilium-search"
}) })
}); });
$(() => chrome.runtime.sendMessage({name: "send-trilium-search-status"})); $(() => browser.runtime.sendMessage({name: "send-trilium-search-status"}));

View File

@ -1,7 +1,7 @@
const PROTOCOL_VERSION_MAJOR = 1; const PROTOCOL_VERSION_MAJOR = 1;
function isDevEnv() { function isDevEnv() {
const manifest = chrome.runtime.getManifest(); const manifest = browser.runtime.getManifest();
return manifest.name.endsWith('(dev)'); return manifest.name.endsWith('(dev)');
} }
@ -16,7 +16,7 @@ class TriliumServerFacade {
async sendTriliumSearchStatusToPopup() { async sendTriliumSearchStatusToPopup() {
try { try {
await chrome.runtime.sendMessage({ await browser.runtime.sendMessage({
name: "trilium-search-status", name: "trilium-search-status",
triliumSearch: this.triliumSearch triliumSearch: this.triliumSearch
}); });
@ -25,7 +25,7 @@ class TriliumServerFacade {
} }
async sendTriliumSearchNoteToPopup(){ async sendTriliumSearchNoteToPopup(){
try{ try{
await chrome.runtime.sendMessage({ await browser.runtime.sendMessage({
name: "trilium-previously-visited", name: "trilium-previously-visited",
searchNote: this.triliumSearchNote searchNote: this.triliumSearchNote
}) })
@ -95,8 +95,8 @@ class TriliumServerFacade {
// continue // continue
} }
const {triliumServerUrl} = await chrome.storage.sync.get("triliumServerUrl"); const {triliumServerUrl} = await browser.storage.sync.get("triliumServerUrl");
const {authToken} = await chrome.storage.sync.get("authToken"); const {authToken} = await browser.storage.sync.get("authToken");
if (triliumServerUrl && authToken) { if (triliumServerUrl && authToken) {
try { try {
@ -162,7 +162,7 @@ class TriliumServerFacade {
} }
async getPort() { async getPort() {
const {triliumDesktopPort} = await chrome.storage.sync.get("triliumDesktopPort"); const {triliumDesktopPort} = await browser.storage.sync.get("triliumDesktopPort");
if (triliumDesktopPort) { if (triliumDesktopPort) {
return parseInt(triliumDesktopPort); return parseInt(triliumDesktopPort);
@ -217,10 +217,9 @@ class TriliumServerFacade {
const absoff = Math.abs(off); const absoff = Math.abs(off);
return (new Date(date.getTime() - off * 60 * 1000).toISOString().substr(0,23).replace("T", " ") + return (new Date(date.getTime() - off * 60 * 1000).toISOString().substr(0,23).replace("T", " ") +
(off > 0 ? '-' : '+') + (off > 0 ? '-' : '+') +
(absoff / 60).toFixed(0).padStart(2,'0') + ':' + (absoff / 60).toFixed(0).padStart(2,'0') + ':' +
(absoff % 60).toString().padStart(2,'0')); (absoff % 60).toString().padStart(2,'0'));
} }
} }
export const triliumServerFacade = new TriliumServerFacade(); window.triliumServerFacade = new TriliumServerFacade();
export { TriliumServerFacade };

View File

@ -1,4 +1,4 @@
export function randomString(len) { function randomString(len) {
let text = ""; let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@ -9,7 +9,7 @@ export function randomString(len) {
return text; return text;
} }
export function getBaseUrl() { function getBaseUrl() {
let output = getPageLocationOrigin() + location.pathname; let output = getPageLocationOrigin() + location.pathname;
if (output[output.length - 1] !== '/') { if (output[output.length - 1] !== '/') {
@ -21,7 +21,7 @@ export function getBaseUrl() {
return output; return output;
} }
export function getPageLocationOrigin() { function getPageLocationOrigin() {
// location.origin normally returns the protocol + domain + port (eg. https://example.com:8080) // location.origin normally returns the protocol + domain + port (eg. https://example.com:8080)
// but for file:// protocol this is browser dependant and in particular Firefox returns "null" in this case. // but for file:// protocol this is browser dependant and in particular Firefox returns "null" in this case.
return location.protocol === 'file:' ? 'file://' : location.origin; return location.protocol === 'file:' ? 'file://' : location.origin;