diff --git a/apps/web-clipper/.gitignore b/apps/web-clipper/.gitignore index 77738287f0..3e99175bf8 100644 --- a/apps/web-clipper/.gitignore +++ b/apps/web-clipper/.gitignore @@ -1 +1,2 @@ -dist/ \ No newline at end of file +.output +.wxt \ No newline at end of file diff --git a/apps/web-clipper/.wxt/tsconfig.json b/apps/web-clipper/.wxt/tsconfig.json deleted file mode 100644 index 0967ef424b..0000000000 --- a/apps/web-clipper/.wxt/tsconfig.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/apps/web-clipper/entrypoints/background/index.js b/apps/web-clipper/entrypoints/background/index.js index 4074987abb..ee92f709a5 100644 --- a/apps/web-clipper/entrypoints/background/index.js +++ b/apps/web-clipper/entrypoints/background/index.js @@ -1,451 +1,453 @@ -// Keyboard shortcuts -chrome.commands.onCommand.addListener(async function (command) { - if (command == "saveSelection") { - await saveSelection(); - } else if (command == "saveWholePage") { - await saveWholePage(); - } else if (command == "saveTabs") { - await saveTabs(); - } else if (command == "saveCroppedScreenshot") { +export default defineBackground(() => { + // Keyboard shortcuts + chrome.commands.onCommand.addListener(async function (command) { + if (command == "saveSelection") { + await saveSelection(); + } else if (command == "saveWholePage") { + await saveWholePage(); + } else if (command == "saveTabs") { + await saveTabs(); + } else if (command == "saveCroppedScreenshot") { + const activeTab = await getActiveTab(); + + await saveCroppedScreenshot(activeTab.url); + } else { + console.log("Unrecognized command", command); + } + }); + + function cropImage(newArea, dataUrl) { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = function () { + const canvas = document.createElement('canvas'); + canvas.width = newArea.width; + canvas.height = newArea.height; + + const ctx = canvas.getContext('2d'); + + ctx.drawImage(img, newArea.x, newArea.y, newArea.width, newArea.height, 0, 0, newArea.width, newArea.height); + + resolve(canvas.toDataURL()); + }; + + img.src = dataUrl; + }); + } + + async function takeCroppedScreenshot(cropRect) { + const activeTab = await getActiveTab(); + const zoom = await browser.tabs.getZoom(activeTab.id) * window.devicePixelRatio; + + const newArea = Object.assign({}, cropRect); + newArea.x *= zoom; + newArea.y *= zoom; + newArea.width *= zoom; + newArea.height *= zoom; + + const dataUrl = await browser.tabs.captureVisibleTab(null, { format: 'png' }); + + return await cropImage(newArea, dataUrl); + } + + async function takeWholeScreenshot() { + // this saves only visible portion of the page + // workaround to save the whole page is to scroll & stitch + // example in https://github.com/mrcoles/full-page-screen-capture-chrome-extension + // see page.js and popup.js + return await browser.tabs.captureVisibleTab(null, { format: 'png' }); + } + + browser.runtime.onInstalled.addListener(() => { + if (isDevEnv()) { + browser.browserAction.setIcon({ + path: 'icons/32-dev.png', + }); + } + }); + + browser.contextMenus.create({ + id: "trilium-save-selection", + title: "Save selection to Trilium", + contexts: ["selection"] + }); + + browser.contextMenus.create({ + id: "trilium-save-cropped-screenshot", + title: "Clip screenshot to Trilium", + contexts: ["page"] + }); + + browser.contextMenus.create({ + id: "trilium-save-cropped-screenshot", + title: "Crop screen shot to Trilium", + contexts: ["page"] + }); + + browser.contextMenus.create({ + id: "trilium-save-whole-screenshot", + title: "Save whole screen shot to Trilium", + contexts: ["page"] + }); + + browser.contextMenus.create({ + id: "trilium-save-page", + title: "Save whole page to Trilium", + contexts: ["page"] + }); + + browser.contextMenus.create({ + id: "trilium-save-link", + title: "Save link to Trilium", + contexts: ["link"] + }); + + browser.contextMenus.create({ + id: "trilium-save-image", + title: "Save image to Trilium", + contexts: ["image"] + }); + + async function getActiveTab() { + const tabs = await browser.tabs.query({ + active: true, + currentWindow: true + }); + + return tabs[0]; + } + + async function getWindowTabs() { + const tabs = await browser.tabs.query({ + currentWindow: true + }); + + return tabs; + } + + async function sendMessageToActiveTab(message) { const activeTab = await getActiveTab(); - await saveCroppedScreenshot(activeTab.url); - } else { - console.log("Unrecognized command", command); + if (!activeTab) { + throw new Error("No active tab."); + } + + try { + return await browser.tabs.sendMessage(activeTab.id, message); + } + catch (e) { + throw e; + } } -}); - -function cropImage(newArea, dataUrl) { - return new Promise((resolve, reject) => { - const img = new Image(); - - img.onload = function () { - const canvas = document.createElement('canvas'); - canvas.width = newArea.width; - canvas.height = newArea.height; - - const ctx = canvas.getContext('2d'); - - ctx.drawImage(img, newArea.x, newArea.y, newArea.width, newArea.height, 0, 0, newArea.width, newArea.height); - - resolve(canvas.toDataURL()); - }; - - img.src = dataUrl; - }); -} - -async function takeCroppedScreenshot(cropRect) { - const activeTab = await getActiveTab(); - const zoom = await browser.tabs.getZoom(activeTab.id) * window.devicePixelRatio; - - const newArea = Object.assign({}, cropRect); - newArea.x *= zoom; - newArea.y *= zoom; - newArea.width *= zoom; - newArea.height *= zoom; - - const dataUrl = await browser.tabs.captureVisibleTab(null, { format: 'png' }); - - return await cropImage(newArea, dataUrl); -} - -async function takeWholeScreenshot() { - // this saves only visible portion of the page - // workaround to save the whole page is to scroll & stitch - // example in https://github.com/mrcoles/full-page-screen-capture-chrome-extension - // see page.js and popup.js - return await browser.tabs.captureVisibleTab(null, { format: 'png' }); -} - -browser.runtime.onInstalled.addListener(() => { - if (isDevEnv()) { - browser.browserAction.setIcon({ - path: 'icons/32-dev.png', - }); - } -}); - -browser.contextMenus.create({ - id: "trilium-save-selection", - title: "Save selection to Trilium", - contexts: ["selection"] -}); - -browser.contextMenus.create({ - id: "trilium-save-cropped-screenshot", - title: "Clip screenshot to Trilium", - contexts: ["page"] -}); - -browser.contextMenus.create({ - id: "trilium-save-cropped-screenshot", - title: "Crop screen shot to Trilium", - contexts: ["page"] -}); - -browser.contextMenus.create({ - id: "trilium-save-whole-screenshot", - title: "Save whole screen shot to Trilium", - contexts: ["page"] -}); - -browser.contextMenus.create({ - id: "trilium-save-page", - title: "Save whole page to Trilium", - contexts: ["page"] -}); - -browser.contextMenus.create({ - id: "trilium-save-link", - title: "Save link to Trilium", - contexts: ["link"] -}); - -browser.contextMenus.create({ - id: "trilium-save-image", - title: "Save image to Trilium", - contexts: ["image"] -}); - -async function getActiveTab() { - const tabs = await browser.tabs.query({ - active: true, - currentWindow: true - }); - - return tabs[0]; -} - -async function getWindowTabs() { - const tabs = await browser.tabs.query({ - currentWindow: true - }); - - return tabs; -} - -async function sendMessageToActiveTab(message) { - const activeTab = await getActiveTab(); - - if (!activeTab) { - throw new Error("No active tab."); - } - - try { - return await browser.tabs.sendMessage(activeTab.id, message); - } - catch (e) { - throw e; - } -} - -function toast(message, noteId = null, tabIds = null) { - sendMessageToActiveTab({ - name: 'toast', - message: message, - noteId: noteId, - tabIds: tabIds - }); -} - -function blob2base64(blob) { - return new Promise(resolve => { - const reader = new FileReader(); - reader.onloadend = function() { - resolve(reader.result); - }; - reader.readAsDataURL(blob); - }); -} - -async function fetchImage(url) { - const resp = await fetch(url); - const blob = await resp.blob(); - - return await blob2base64(blob); -} - -async function postProcessImage(image) { - if (image.src.startsWith("data:image/")) { - image.dataUrl = image.src; - image.src = "inline." + image.src.substr(11, 3); // this should extract file type - png/jpg - } - else { - try { - image.dataUrl = await fetchImage(image.src, image); - } - catch (e) { - console.log(`Cannot fetch image from ${image.src}`); - } - } -} - -async function postProcessImages(resp) { - if (resp.images) { - for (const image of resp.images) { - await postProcessImage(image); - } - } -} - -async function saveSelection() { - const payload = await sendMessageToActiveTab({name: 'trilium-save-selection'}); - - await postProcessImages(payload); - - const resp = await triliumServerFacade.callService('POST', 'clippings', payload); - - if (!resp) { - return; - } - - toast("Selection has been saved to Trilium.", resp.noteId); -} - -async function getImagePayloadFromSrc(src, pageUrl) { - const image = { - imageId: randomString(20), - src: src - }; - - await postProcessImage(image); - - const activeTab = await getActiveTab(); - - return { - title: activeTab.title, - content: ``, - images: [image], - pageUrl: pageUrl - }; -} - -async function saveCroppedScreenshot(pageUrl) { - const cropRect = await sendMessageToActiveTab({name: 'trilium-get-rectangle-for-screenshot'}); - - const src = await takeCroppedScreenshot(cropRect); - - const payload = await getImagePayloadFromSrc(src, pageUrl); - - const resp = await triliumServerFacade.callService("POST", "clippings", payload); - - if (!resp) { - return; - } - - toast("Screenshot has been saved to Trilium.", resp.noteId); -} - -async function saveWholeScreenshot(pageUrl) { - const src = await takeWholeScreenshot(); - - const payload = await getImagePayloadFromSrc(src, pageUrl); - - const resp = await triliumServerFacade.callService("POST", "clippings", payload); - - if (!resp) { - return; - } - - toast("Screenshot has been saved to Trilium.", resp.noteId); -} - -async function saveImage(srcUrl, pageUrl) { - const payload = await getImagePayloadFromSrc(srcUrl, pageUrl); - - const resp = await triliumServerFacade.callService("POST", "clippings", payload); - - if (!resp) { - return; - } - - toast("Image has been saved to Trilium.", resp.noteId); -} - -async function saveWholePage() { - const payload = await sendMessageToActiveTab({name: 'trilium-save-page'}); - - await postProcessImages(payload); - - const resp = await triliumServerFacade.callService('POST', 'notes', payload); - - if (!resp) { - return; - } - - toast("Page has been saved to Trilium.", resp.noteId); -} - -async function saveLinkWithNote(title, content) { - const activeTab = await getActiveTab(); - - if (!title.trim()) { - title = activeTab.title; - } - - const resp = await triliumServerFacade.callService('POST', 'notes', { - title: title, - content: content, - clipType: 'note', - pageUrl: activeTab.url - }); - - if (!resp) { - return false; - } - - toast("Link with note has been saved to Trilium.", resp.noteId); - - return true; -} - -async function getTabsPayload(tabs) { - let content = ''; - - const domainsCount = tabs.map(tab => tab.url) - .reduce((acc, url) => { - const hostname = new URL(url).hostname - return acc.set(hostname, (acc.get(hostname) || 0) + 1) - }, new Map()); - - let topDomains = [...domainsCount] - .sort((a, b) => {return b[1]-a[1]}) - .slice(0,3) - .map(domain=>domain[0]) - .join(', ') - - if (tabs.length > 3) { topDomains += '...' } - - return { - title: `${tabs.length} browser tabs: ${topDomains}`, - content: content, - clipType: 'tabs' - }; -} - -async function saveTabs() { - const tabs = await getWindowTabs(); - - const payload = await getTabsPayload(tabs); - - const resp = await triliumServerFacade.callService('POST', 'notes', payload); - - if (!resp) { - return; - } - - const tabIds = tabs.map(tab=>{return tab.id}); - - toast(`${tabs.length} links have been saved to Trilium.`, resp.noteId, tabIds); -} - -browser.contextMenus.onClicked.addListener(async function(info, tab) { - 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 - }); - - if (!resp) { - return; - } - - toast("Link has been saved to Trilium.", resp.noteId); - } - else if (info.menuItemId === 'trilium-save-page') { - await saveWholePage(); - } - else { - console.log("Unrecognized menuItemId", info.menuItemId); - } -}); - -browser.runtime.onMessage.addListener(async request => { - console.log("Received", request); - - if (request.name === 'openNoteInTrilium') { - const resp = await triliumServerFacade.callService('POST', 'open/' + request.noteId); - - if (!resp) { - return; - } - - // desktop app is not available so we need to open in browser - if (resp.result === 'open-in-browser') { - const {triliumServerUrl} = await browser.storage.sync.get("triliumServerUrl"); - - if (triliumServerUrl) { - const noteUrl = triliumServerUrl + '/#' + request.noteId; - - console.log("Opening new tab in browser", noteUrl); - - browser.tabs.create({ - url: noteUrl - }); - } - else { - console.error("triliumServerUrl not found in local storage."); - } - } - } - else if (request.name === 'closeTabs') { - return await browser.tabs.remove(request.tabIds) - } - else if (request.name === 'load-script') { - return await browser.tabs.executeScript({file: request.file}); - } - 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); - } + + function toast(message, noteId = null, tabIds = null) { + sendMessageToActiveTab({ + name: 'toast', + message: message, + noteId: noteId, + tabIds: tabIds + }); + } + + function blob2base64(blob) { + return new Promise(resolve => { + const reader = new FileReader(); + reader.onloadend = function() { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + } + + async function fetchImage(url) { + const resp = await fetch(url); + const blob = await resp.blob(); + + return await blob2base64(blob); + } + + async function postProcessImage(image) { + if (image.src.startsWith("data:image/")) { + image.dataUrl = image.src; + image.src = "inline." + image.src.substr(11, 3); // this should extract file type - png/jpg + } + else { + try { + image.dataUrl = await fetchImage(image.src, image); + } + catch (e) { + console.log(`Cannot fetch image from ${image.src}`); + } + } + } + + async function postProcessImages(resp) { + if (resp.images) { + for (const image of resp.images) { + await postProcessImage(image); + } + } + } + + async function saveSelection() { + const payload = await sendMessageToActiveTab({name: 'trilium-save-selection'}); + + await postProcessImages(payload); + + const resp = await triliumServerFacade.callService('POST', 'clippings', payload); + + if (!resp) { + return; + } + + toast("Selection has been saved to Trilium.", resp.noteId); + } + + async function getImagePayloadFromSrc(src, pageUrl) { + const image = { + imageId: randomString(20), + src: src + }; + + await postProcessImage(image); + + const activeTab = await getActiveTab(); + + return { + title: activeTab.title, + content: ``, + images: [image], + pageUrl: pageUrl + }; + } + + async function saveCroppedScreenshot(pageUrl) { + const cropRect = await sendMessageToActiveTab({name: 'trilium-get-rectangle-for-screenshot'}); + + const src = await takeCroppedScreenshot(cropRect); + + const payload = await getImagePayloadFromSrc(src, pageUrl); + + const resp = await triliumServerFacade.callService("POST", "clippings", payload); + + if (!resp) { + return; + } + + toast("Screenshot has been saved to Trilium.", resp.noteId); + } + + async function saveWholeScreenshot(pageUrl) { + const src = await takeWholeScreenshot(); + + const payload = await getImagePayloadFromSrc(src, pageUrl); + + const resp = await triliumServerFacade.callService("POST", "clippings", payload); + + if (!resp) { + return; + } + + toast("Screenshot has been saved to Trilium.", resp.noteId); + } + + async function saveImage(srcUrl, pageUrl) { + const payload = await getImagePayloadFromSrc(srcUrl, pageUrl); + + const resp = await triliumServerFacade.callService("POST", "clippings", payload); + + if (!resp) { + return; + } + + toast("Image has been saved to Trilium.", resp.noteId); + } + + async function saveWholePage() { + const payload = await sendMessageToActiveTab({name: 'trilium-save-page'}); + + await postProcessImages(payload); + + const resp = await triliumServerFacade.callService('POST', 'notes', payload); + + if (!resp) { + return; + } + + toast("Page has been saved to Trilium.", resp.noteId); + } + + async function saveLinkWithNote(title, content) { + const activeTab = await getActiveTab(); + + if (!title.trim()) { + title = activeTab.title; + } + + const resp = await triliumServerFacade.callService('POST', 'notes', { + title: title, + content: content, + clipType: 'note', + pageUrl: activeTab.url + }); + + if (!resp) { + return false; + } + + toast("Link with note has been saved to Trilium.", resp.noteId); + + return true; + } + + async function getTabsPayload(tabs) { + let content = ''; + + const domainsCount = tabs.map(tab => tab.url) + .reduce((acc, url) => { + const hostname = new URL(url).hostname + return acc.set(hostname, (acc.get(hostname) || 0) + 1) + }, new Map()); + + let topDomains = [...domainsCount] + .sort((a, b) => {return b[1]-a[1]}) + .slice(0,3) + .map(domain=>domain[0]) + .join(', ') + + if (tabs.length > 3) { topDomains += '...' } + + return { + title: `${tabs.length} browser tabs: ${topDomains}`, + content: content, + clipType: 'tabs' + }; + } + + async function saveTabs() { + const tabs = await getWindowTabs(); + + const payload = await getTabsPayload(tabs); + + const resp = await triliumServerFacade.callService('POST', 'notes', payload); + + if (!resp) { + return; + } + + const tabIds = tabs.map(tab=>{return tab.id}); + + toast(`${tabs.length} links have been saved to Trilium.`, resp.noteId, tabIds); + } + + browser.contextMenus.onClicked.addListener(async function(info, tab) { + 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 + }); + + if (!resp) { + return; + } + + toast("Link has been saved to Trilium.", resp.noteId); + } + else if (info.menuItemId === 'trilium-save-page') { + await saveWholePage(); + } + else { + console.log("Unrecognized menuItemId", info.menuItemId); + } + }); + + browser.runtime.onMessage.addListener(async request => { + console.log("Received", request); + + if (request.name === 'openNoteInTrilium') { + const resp = await triliumServerFacade.callService('POST', 'open/' + request.noteId); + + if (!resp) { + return; + } + + // desktop app is not available so we need to open in browser + if (resp.result === 'open-in-browser') { + const {triliumServerUrl} = await browser.storage.sync.get("triliumServerUrl"); + + if (triliumServerUrl) { + const noteUrl = triliumServerUrl + '/#' + request.noteId; + + console.log("Opening new tab in browser", noteUrl); + + browser.tabs.create({ + url: noteUrl + }); + } + else { + console.error("triliumServerUrl not found in local storage."); + } + } + } + else if (request.name === 'closeTabs') { + return await browser.tabs.remove(request.tabIds) + } + else if (request.name === 'load-script') { + return await browser.tabs.executeScript({file: request.file}); + } + 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); + } + }); }); diff --git a/apps/web-clipper/entrypoints/content/index.js b/apps/web-clipper/entrypoints/content/index.js index faacfa5464..c6ba6c438c 100644 --- a/apps/web-clipper/entrypoints/content/index.js +++ b/apps/web-clipper/entrypoints/content/index.js @@ -1,351 +1,358 @@ -function absoluteUrl(url) { - if (!url) { - return url; - } - - const protocol = url.toLowerCase().split(':')[0]; - if (['http', 'https', 'file'].indexOf(protocol) >= 0) { - return url; - } - - if (url.indexOf('//') === 0) { - return location.protocol + url; - } else if (url[0] === '/') { - return location.protocol + '//' + location.host + url; - } else { - return getBaseUrl() + '/' + url; - } -} - -function pageTitle() { - const titleElements = document.getElementsByTagName("title"); - - return titleElements.length ? titleElements[0].text.trim() : document.title.trim(); -} - -function getReadableDocument() { - // Readability directly change the passed document, so clone to preserve the original web page. - const documentCopy = document.cloneNode(true); - const readability = new Readability(documentCopy, { - serializer: el => el // so that .content is returned as DOM element instead of HTML - }); - - const article = readability.parse(); - - if (!article) { - throw new Error('Could not parse HTML document with Readability'); - } - - return { - title: article.title, - body: article.content, - } -} - -function getDocumentDates() { - var dates = { - publishedDate: null, - modifiedDate: null, - }; - - const articlePublishedTime = document.querySelector("meta[property='article:published_time']"); - if (articlePublishedTime && articlePublishedTime.getAttribute('content')) { - dates.publishedDate = new Date(articlePublishedTime.getAttribute('content')); - } - - const articleModifiedTime = document.querySelector("meta[property='article:modified_time']"); - if (articleModifiedTime && 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 - - return dates; -} - -function getRectangleArea() { - return new Promise((resolve, reject) => { - const overlay = document.createElement('div'); - overlay.style.opacity = '0.6'; - overlay.style.background = 'black'; - overlay.style.width = '100%'; - overlay.style.height = '100%'; - overlay.style.zIndex = 99999999; - overlay.style.top = 0; - overlay.style.left = 0; - overlay.style.position = 'fixed'; - - document.body.appendChild(overlay); - - const messageComp = document.createElement('div'); - - const messageCompWidth = 300; - messageComp.setAttribute("tabindex", "0"); // so that it can be focused - messageComp.style.position = 'fixed'; - messageComp.style.opacity = '0.95'; - messageComp.style.fontSize = '14px'; - messageComp.style.width = messageCompWidth + 'px'; - messageComp.style.maxWidth = messageCompWidth + 'px'; - messageComp.style.border = '1px solid black'; - messageComp.style.background = 'white'; - messageComp.style.color = 'black'; - messageComp.style.top = '10px'; - messageComp.style.textAlign = 'center'; - messageComp.style.padding = '10px'; - messageComp.style.left = Math.round(document.body.clientWidth / 2 - messageCompWidth / 2) + 'px'; - messageComp.style.zIndex = overlay.style.zIndex + 1; - - messageComp.textContent = 'Drag and release to capture a screenshot'; - - document.body.appendChild(messageComp); - - const selection = document.createElement('div'); - selection.style.opacity = '0.5'; - selection.style.border = '1px solid red'; - selection.style.background = 'white'; - selection.style.border = '2px solid black'; - selection.style.zIndex = overlay.style.zIndex - 1; - selection.style.top = 0; - selection.style.left = 0; - selection.style.position = 'fixed'; - - document.body.appendChild(selection); - - messageComp.focus(); // we listen on keypresses on this element to cancel on escape - - let isDragging = false; - let draggingStartPos = null; - let selectionArea = {}; - - function updateSelection() { - selection.style.left = selectionArea.x + 'px'; - selection.style.top = selectionArea.y + 'px'; - selection.style.width = selectionArea.width + 'px'; - selection.style.height = selectionArea.height + 'px'; - } - - function setSelectionSizeFromMouse(event) { - if (event.clientX < draggingStartPos.x) { - selectionArea.x = event.clientX; - } - - if (event.clientY < draggingStartPos.y) { - selectionArea.y = event.clientY; - } - - selectionArea.width = Math.max(1, Math.abs(event.clientX - draggingStartPos.x)); - selectionArea.height = Math.max(1, Math.abs(event.clientY - draggingStartPos.y)); - updateSelection(); - } - - function selection_mouseDown(event) { - selectionArea = {x: event.clientX, y: event.clientY, width: 0, height: 0}; - draggingStartPos = {x: event.clientX, y: event.clientY}; - isDragging = true; - updateSelection(); - } - - function selection_mouseMove(event) { - if (!isDragging) return; - setSelectionSizeFromMouse(event); - } - - function removeOverlay() { - isDragging = false; - - overlay.removeEventListener('mousedown', selection_mouseDown); - overlay.removeEventListener('mousemove', selection_mouseMove); - overlay.removeEventListener('mouseup', selection_mouseUp); - - document.body.removeChild(overlay); - document.body.removeChild(selection); - document.body.removeChild(messageComp); - } - - function selection_mouseUp(event) { - setSelectionSizeFromMouse(event); - - removeOverlay(); - - console.info('selectionArea:', selectionArea); - - if (!selectionArea || !selectionArea.width || !selectionArea.height) { - return; - } - - // Need to wait a bit before taking the screenshot to make sure - // the overlays have been removed and don't appear in the - // screenshot. 10ms is not enough. - setTimeout(() => resolve(selectionArea), 100); - } - - function cancel(event) { - if (event.key === "Escape") { - removeOverlay(); - } - } - - overlay.addEventListener('mousedown', selection_mouseDown); - overlay.addEventListener('mousemove', selection_mouseMove); - overlay.addEventListener('mouseup', selection_mouseUp); - overlay.addEventListener('mouseup', selection_mouseUp); - messageComp.addEventListener('keydown', cancel); - }); -} - -function makeLinksAbsolute(container) { - for (const link of container.getElementsByTagName('a')) { - if (link.href) { - link.href = absoluteUrl(link.href); - } - } -} - -function getImages(container) { - const images = []; - - for (const img of container.getElementsByTagName('img')) { - if (!img.src) { - continue; - } - - const existingImage = images.find(image => image.src === img.src); - - if (existingImage) { - img.src = existingImage.imageId; - } - else { - const imageId = randomString(20); - - images.push({ - imageId: imageId, - src: img.src - }); - - img.src = imageId; - } - } - - return images; -} - -function createLink(clickAction, text, color = "lightskyblue") { - const link = document.createElement('a'); - link.href = "javascript:"; - link.style.color = color; - link.appendChild(document.createTextNode(text)); - link.addEventListener("click", () => { - browser.runtime.sendMessage(null, clickAction) - }); - - return link -} - -async function prepareMessageResponse(message) { - console.info('Message: ' + message.name); - - if (message.name === "toast") { - let messageText; - - if (message.noteId) { - messageText = document.createElement('p'); - messageText.setAttribute("style", "padding: 0; margin: 0; font-size: larger;") - messageText.appendChild(document.createTextNode(message.message + " ")); - messageText.appendChild(createLink( - {name: 'openNoteInTrilium', noteId: message.noteId}, - "Open in Trilium." - )); - - // only after saving tabs - if (message.tabIds) { - messageText.appendChild(document.createElement("br")); - messageText.appendChild(createLink( - {name: 'closeTabs', tabIds: message.tabIds}, - "Close saved tabs.", - "tomato" - )); - } - } - else { - messageText = message.message; - } - - await requireLib('/lib/toast.js'); - - showToast(messageText, { - settings: { - duration: 7000 - } - }); - } - else if (message.name === "trilium-save-selection") { - const container = document.createElement('div'); - - const selection = window.getSelection(); - - for (let i = 0; i < selection.rangeCount; i++) { - const range = selection.getRangeAt(i); - - container.appendChild(range.cloneContents()); - } - - makeLinksAbsolute(container); - - const images = getImages(container); - - return { - title: pageTitle(), - content: container.innerHTML, - images: images, - pageUrl: getPageLocationOrigin() + location.pathname + location.search + location.hash - }; - - } - else if (message.name === 'trilium-get-rectangle-for-screenshot') { - return getRectangleArea(); - } - else if (message.name === "trilium-save-page") { - await requireLib("/lib/JSDOMParser.js"); - await requireLib("/lib/Readability.js"); - await requireLib("/lib/Readability-readerable.js"); - - const {title, body} = getReadableDocument(); - - makeLinksAbsolute(body); - - const images = getImages(body); - - var labels = {}; - const dates = getDocumentDates(); - if (dates.publishedDate) { - labels['publishedDate'] = dates.publishedDate.toISOString().substring(0, 10); - } - if (dates.modifiedDate) { - labels['modifiedDate'] = dates.publishedDate.toISOString().substring(0, 10); - } - - return { - title: title, - content: body.innerHTML, - images: images, - pageUrl: getPageLocationOrigin() + location.pathname + location.search, - clipType: 'page', - labels: labels - }; - } - else { - throw new Error('Unknown command: ' + JSON.stringify(message)); - } -} - -browser.runtime.onMessage.addListener(prepareMessageResponse); - -const loadedLibs = []; - -async function requireLib(libPath) { - if (!loadedLibs.includes(libPath)) { - loadedLibs.push(libPath); - - await browser.runtime.sendMessage({name: 'load-script', file: libPath}); - } -} +export default defineContentScript({ + matches: [ + "" + ], + main: () => { + function absoluteUrl(url) { + if (!url) { + return url; + } + + const protocol = url.toLowerCase().split(':')[0]; + if (['http', 'https', 'file'].indexOf(protocol) >= 0) { + return url; + } + + if (url.indexOf('//') === 0) { + return location.protocol + url; + } else if (url[0] === '/') { + return location.protocol + '//' + location.host + url; + } else { + return getBaseUrl() + '/' + url; + } + } + + function pageTitle() { + const titleElements = document.getElementsByTagName("title"); + + return titleElements.length ? titleElements[0].text.trim() : document.title.trim(); + } + + function getReadableDocument() { + // Readability directly change the passed document, so clone to preserve the original web page. + const documentCopy = document.cloneNode(true); + const readability = new Readability(documentCopy, { + serializer: el => el // so that .content is returned as DOM element instead of HTML + }); + + const article = readability.parse(); + + if (!article) { + throw new Error('Could not parse HTML document with Readability'); + } + + return { + title: article.title, + body: article.content, + } + } + + function getDocumentDates() { + var dates = { + publishedDate: null, + modifiedDate: null, + }; + + const articlePublishedTime = document.querySelector("meta[property='article:published_time']"); + if (articlePublishedTime && articlePublishedTime.getAttribute('content')) { + dates.publishedDate = new Date(articlePublishedTime.getAttribute('content')); + } + + const articleModifiedTime = document.querySelector("meta[property='article:modified_time']"); + if (articleModifiedTime && 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 + + return dates; + } + + function getRectangleArea() { + return new Promise((resolve, reject) => { + const overlay = document.createElement('div'); + overlay.style.opacity = '0.6'; + overlay.style.background = 'black'; + overlay.style.width = '100%'; + overlay.style.height = '100%'; + overlay.style.zIndex = 99999999; + overlay.style.top = 0; + overlay.style.left = 0; + overlay.style.position = 'fixed'; + + document.body.appendChild(overlay); + + const messageComp = document.createElement('div'); + + const messageCompWidth = 300; + messageComp.setAttribute("tabindex", "0"); // so that it can be focused + messageComp.style.position = 'fixed'; + messageComp.style.opacity = '0.95'; + messageComp.style.fontSize = '14px'; + messageComp.style.width = messageCompWidth + 'px'; + messageComp.style.maxWidth = messageCompWidth + 'px'; + messageComp.style.border = '1px solid black'; + messageComp.style.background = 'white'; + messageComp.style.color = 'black'; + messageComp.style.top = '10px'; + messageComp.style.textAlign = 'center'; + messageComp.style.padding = '10px'; + messageComp.style.left = Math.round(document.body.clientWidth / 2 - messageCompWidth / 2) + 'px'; + messageComp.style.zIndex = overlay.style.zIndex + 1; + + messageComp.textContent = 'Drag and release to capture a screenshot'; + + document.body.appendChild(messageComp); + + const selection = document.createElement('div'); + selection.style.opacity = '0.5'; + selection.style.border = '1px solid red'; + selection.style.background = 'white'; + selection.style.border = '2px solid black'; + selection.style.zIndex = overlay.style.zIndex - 1; + selection.style.top = 0; + selection.style.left = 0; + selection.style.position = 'fixed'; + + document.body.appendChild(selection); + + messageComp.focus(); // we listen on keypresses on this element to cancel on escape + + let isDragging = false; + let draggingStartPos = null; + let selectionArea = {}; + + function updateSelection() { + selection.style.left = selectionArea.x + 'px'; + selection.style.top = selectionArea.y + 'px'; + selection.style.width = selectionArea.width + 'px'; + selection.style.height = selectionArea.height + 'px'; + } + + function setSelectionSizeFromMouse(event) { + if (event.clientX < draggingStartPos.x) { + selectionArea.x = event.clientX; + } + + if (event.clientY < draggingStartPos.y) { + selectionArea.y = event.clientY; + } + + selectionArea.width = Math.max(1, Math.abs(event.clientX - draggingStartPos.x)); + selectionArea.height = Math.max(1, Math.abs(event.clientY - draggingStartPos.y)); + updateSelection(); + } + + function selection_mouseDown(event) { + selectionArea = {x: event.clientX, y: event.clientY, width: 0, height: 0}; + draggingStartPos = {x: event.clientX, y: event.clientY}; + isDragging = true; + updateSelection(); + } + + function selection_mouseMove(event) { + if (!isDragging) return; + setSelectionSizeFromMouse(event); + } + + function removeOverlay() { + isDragging = false; + + overlay.removeEventListener('mousedown', selection_mouseDown); + overlay.removeEventListener('mousemove', selection_mouseMove); + overlay.removeEventListener('mouseup', selection_mouseUp); + + document.body.removeChild(overlay); + document.body.removeChild(selection); + document.body.removeChild(messageComp); + } + + function selection_mouseUp(event) { + setSelectionSizeFromMouse(event); + + removeOverlay(); + + console.info('selectionArea:', selectionArea); + + if (!selectionArea || !selectionArea.width || !selectionArea.height) { + return; + } + + // Need to wait a bit before taking the screenshot to make sure + // the overlays have been removed and don't appear in the + // screenshot. 10ms is not enough. + setTimeout(() => resolve(selectionArea), 100); + } + + function cancel(event) { + if (event.key === "Escape") { + removeOverlay(); + } + } + + overlay.addEventListener('mousedown', selection_mouseDown); + overlay.addEventListener('mousemove', selection_mouseMove); + overlay.addEventListener('mouseup', selection_mouseUp); + overlay.addEventListener('mouseup', selection_mouseUp); + messageComp.addEventListener('keydown', cancel); + }); + } + + function makeLinksAbsolute(container) { + for (const link of container.getElementsByTagName('a')) { + if (link.href) { + link.href = absoluteUrl(link.href); + } + } + } + + function getImages(container) { + const images = []; + + for (const img of container.getElementsByTagName('img')) { + if (!img.src) { + continue; + } + + const existingImage = images.find(image => image.src === img.src); + + if (existingImage) { + img.src = existingImage.imageId; + } + else { + const imageId = randomString(20); + + images.push({ + imageId: imageId, + src: img.src + }); + + img.src = imageId; + } + } + + return images; + } + + function createLink(clickAction, text, color = "lightskyblue") { + const link = document.createElement('a'); + link.href = "javascript:"; + link.style.color = color; + link.appendChild(document.createTextNode(text)); + link.addEventListener("click", () => { + browser.runtime.sendMessage(null, clickAction) + }); + + return link + } + + async function prepareMessageResponse(message) { + console.info('Message: ' + message.name); + + if (message.name === "toast") { + let messageText; + + if (message.noteId) { + messageText = document.createElement('p'); + messageText.setAttribute("style", "padding: 0; margin: 0; font-size: larger;") + messageText.appendChild(document.createTextNode(message.message + " ")); + messageText.appendChild(createLink( + {name: 'openNoteInTrilium', noteId: message.noteId}, + "Open in Trilium." + )); + + // only after saving tabs + if (message.tabIds) { + messageText.appendChild(document.createElement("br")); + messageText.appendChild(createLink( + {name: 'closeTabs', tabIds: message.tabIds}, + "Close saved tabs.", + "tomato" + )); + } + } + else { + messageText = message.message; + } + + await requireLib('/lib/toast.js'); + + showToast(messageText, { + settings: { + duration: 7000 + } + }); + } + else if (message.name === "trilium-save-selection") { + const container = document.createElement('div'); + + const selection = window.getSelection(); + + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + + container.appendChild(range.cloneContents()); + } + + makeLinksAbsolute(container); + + const images = getImages(container); + + return { + title: pageTitle(), + content: container.innerHTML, + images: images, + pageUrl: getPageLocationOrigin() + location.pathname + location.search + location.hash + }; + + } + else if (message.name === 'trilium-get-rectangle-for-screenshot') { + return getRectangleArea(); + } + else if (message.name === "trilium-save-page") { + await requireLib("/lib/JSDOMParser.js"); + await requireLib("/lib/Readability.js"); + await requireLib("/lib/Readability-readerable.js"); + + const {title, body} = getReadableDocument(); + + makeLinksAbsolute(body); + + const images = getImages(body); + + var labels = {}; + const dates = getDocumentDates(); + if (dates.publishedDate) { + labels['publishedDate'] = dates.publishedDate.toISOString().substring(0, 10); + } + if (dates.modifiedDate) { + labels['modifiedDate'] = dates.publishedDate.toISOString().substring(0, 10); + } + + return { + title: title, + content: body.innerHTML, + images: images, + pageUrl: getPageLocationOrigin() + location.pathname + location.search, + clipType: 'page', + labels: labels + }; + } + else { + throw new Error('Unknown command: ' + JSON.stringify(message)); + } + } + + const loadedLibs = []; + + async function requireLib(libPath) { + if (!loadedLibs.includes(libPath)) { + loadedLibs.push(libPath); + + await browser.runtime.sendMessage({name: 'load-script', file: libPath}); + } + } + + browser.runtime.onMessage.addListener(prepareMessageResponse); + } +}); diff --git a/apps/web-clipper/manifest.json b/apps/web-clipper/manifest.json index 8e0c1ba2bc..70da3d1b1b 100644 --- a/apps/web-clipper/manifest.json +++ b/apps/web-clipper/manifest.json @@ -26,9 +26,6 @@ }, "content_scripts": [ { - "matches": [ - "" - ], "js": [ "lib/browser-polyfill.js", "utils.js"