feat(web-clipper): handle manifest V3

This commit is contained in:
Elian Doran 2026-01-24 15:28:47 +02:00
parent a9b8ffd94c
commit e37487a1cf
No known key found for this signature in database
6 changed files with 84 additions and 36 deletions

View File

@ -1,7 +1,6 @@
import { randomString } from "../../utils";
import TriliumServerFacade, { isDevEnv } from "./trilium_server_facade";
import { randomString, Rect } from "@/utils";
type Rect = { x: number, y: number, width: number, height: number };
import TriliumServerFacade, { isDevEnv } from "./trilium_server_facade";
export default defineBackground(() => {
const triliumServerFacade = new TriliumServerFacade();
@ -23,38 +22,46 @@ export default defineBackground(() => {
}
});
function cropImage(newArea: Rect, dataUrl: string) {
return new Promise<string>((resolve) => {
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: Rect) {
async function takeCroppedScreenshot(cropRect: Rect, devicePixelRatio: number = 1) {
const activeTab = await getActiveTab();
const zoom = await browser.tabs.getZoom(activeTab.id) * window.devicePixelRatio;
const zoom = await browser.tabs.getZoom(activeTab.id) * devicePixelRatio;
const newArea: Rect = Object.assign({}, cropRect);
newArea.x *= zoom;
newArea.y *= zoom;
newArea.width *= zoom;
newArea.height *= zoom;
const newArea: Rect = {
x: cropRect.x * zoom,
y: cropRect.y * zoom,
width: cropRect.width * zoom,
height: cropRect.height * zoom
};
const dataUrl = await browser.tabs.captureVisibleTab({ format: 'png' });
return await cropImage(newArea, dataUrl);
// Create offscreen document if it doesn't exist
await ensureOffscreenDocument();
// Send cropping task to offscreen document
const croppedDataUrl = await browser.runtime.sendMessage({
type: 'CROP_IMAGE',
dataUrl,
cropRect: newArea
});
return croppedDataUrl;
}
async function ensureOffscreenDocument() {
const existingContexts = await browser.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT']
});
if (existingContexts.length > 0) {
return; // Already exists
}
await browser.offscreen.createDocument({
url: browser.runtime.getURL('/offscreen.html'),
reasons: ['DOM_SCRAPING'], // or 'DISPLAY_MEDIA' depending on browser support
justification: 'Image cropping requires canvas API'
});
}
async function takeWholeScreenshot() {
@ -224,9 +231,9 @@ export default defineBackground(() => {
}
async function saveCroppedScreenshot(pageUrl) {
const cropRect = await sendMessageToActiveTab({name: 'trilium-get-rectangle-for-screenshot'});
const { rect, devicePixelRatio } = await sendMessageToActiveTab({name: 'trilium-get-rectangle-for-screenshot'});
const src = await takeCroppedScreenshot(cropRect);
const src = await takeCroppedScreenshot(rect, devicePixelRatio);
const payload = await getImagePayloadFromSrc(src, pageUrl);

View File

@ -1,3 +1,5 @@
import { Rect } from "@/utils.js";
import Readability from "../../lib/Readability.js";
import { createLink, getBaseUrl, getPageLocationOrigin, randomString } from "../../utils.js";
@ -69,7 +71,7 @@ export default defineContentScript({
}
function getRectangleArea() {
return new Promise((resolve) => {
return new Promise<Rect>((resolve) => {
const overlay = document.createElement('div');
overlay.style.opacity = '0.6';
overlay.style.background = 'black';
@ -120,7 +122,7 @@ export default defineContentScript({
let isDragging = false;
let draggingStartPos: {x: number, y: number} | null = null;
let selectionArea: {x?: number, y?: number, width?: number, height?: number} = {};
let selectionArea: Rect;
function updateSelection() {
selection.style.left = `${selectionArea.x}px`;
@ -300,7 +302,10 @@ export default defineContentScript({
}
else if (message.name === 'trilium-get-rectangle-for-screenshot') {
return getRectangleArea();
return {
rect: await getRectangleArea(),
devicePixelRatio: window.devicePixelRatio
};
}
else if (message.name === "trilium-save-page") {
const {title, body} = getReadableDocument();

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script type="module" src="./index.ts"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
browser.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.type === 'CROP_IMAGE') {
cropImage(message.cropRect, message.dataUrl).then(sendResponse);
return true; // Keep channel open for async response
}
});
function cropImage(newArea: { x: number, y: number, width: number, height: number }, dataUrl: string) {
return new Promise<string>((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = newArea.width;
canvas.height = newArea.height;
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, newArea.x, newArea.y, newArea.width, newArea.height,
0, 0, newArea.width, newArea.height);
}
resolve(canvas.toDataURL());
};
img.src = dataUrl;
});
}

View File

@ -1,3 +1,5 @@
export type Rect = { x: number, y: number, width: number, height: number };
export function randomString(len: number) {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

View File

@ -12,7 +12,8 @@ export default defineConfig({
"https://*/",
"<all_urls>",
"storage",
"contextMenus"
"contextMenus",
"offscreen"
],
browser_specific_settings: {
gecko: {