- Service app/services/web_clipper.py: sanitize HTML, html->blocks, extraction article, creation page workspace-aware, rate limit 50/h, device registration - Router app/routers/web_clipper.py: POST /api/v2/web-clipper/clip, GET /status, POST /auth/verify, GET/DELETE /devices, GET /extensions (download page), auth via session ou Bearer (api_tokens / extension_devices) - Migration 19: extension_devices + extension_clips (+ indexes) - Extension Manifest V3: content.js (floating button, selection), background.js (clip + contextMenus), popup.html/js, clipper.css, icons - Settings UI: onglet Extensions (liste devices, revoke, test clip, liens download), page /extensions - Tests: 16 tests web_clipper (sanitize, blocks, article/bookmark/selection/screenshot, bearer, rate-limit, devices, extensions page) - Bump version 6.1.0 -> 6.2.0
99 lines
3.4 KiB
JavaScript
99 lines
3.4 KiB
JavaScript
// FlowDeck Web Clipper — content script
|
|
// Injected in every page: floating button + context menu + selection handling
|
|
(function () {
|
|
if (window.__fdClipperInjected) return;
|
|
window.__fdClipperInjected = true;
|
|
|
|
const BTN_ID = "fd-clipper-btn";
|
|
|
|
function createFloatButton() {
|
|
if (document.getElementById(BTN_ID)) return;
|
|
const btn = document.createElement("button");
|
|
btn.id = BTN_ID;
|
|
btn.className = "fd-clipper-btn";
|
|
btn.textContent = "📌 Clip to FlowDeck";
|
|
btn.title = "Clip this page to FlowDeck";
|
|
btn.addEventListener("click", () => openClipper("article"));
|
|
document.body.appendChild(btn);
|
|
}
|
|
|
|
function getSelectionHtml() {
|
|
const sel = window.getSelection();
|
|
if (!sel || sel.rangeCount === 0 || sel.toString().trim().length < 3) return "";
|
|
const range = sel.getRangeAt(0);
|
|
const div = document.createElement("div");
|
|
div.appendChild(range.cloneContents());
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function openClipper(type) {
|
|
const html = document.documentElement.outerHTML;
|
|
const selectionHtml = getSelectionHtml();
|
|
const selectionText = window.getSelection() ? window.getSelection().toString().trim() : "";
|
|
let clipType = type;
|
|
if (selectionHtml && selectionText.length > 20) clipType = "selection";
|
|
const payload = {
|
|
url: window.location.href,
|
|
title: document.title,
|
|
content: html,
|
|
content_type: clipType,
|
|
selection_html: selectionHtml,
|
|
metadata: {
|
|
og_title: document.querySelector('meta[property="og:title"]')?.content || "",
|
|
og_description: document.querySelector('meta[property="og:description"]')?.content || document.querySelector('meta[name="description"]')?.content || "",
|
|
author: document.querySelector('meta[name="author"]')?.content || ""
|
|
}
|
|
};
|
|
// Send to background
|
|
try {
|
|
const resp = await chrome.runtime.sendMessage({ action: "clip", data: payload });
|
|
if (resp && resp.error === "not_authenticated") {
|
|
alert("FlowDeck: not authenticated. Open FlowDeck settings to connect the clipper.");
|
|
} else if (resp && resp.status === "ok") {
|
|
showToast("Clipped to FlowDeck ✓ — page " + resp.page_id);
|
|
} else if (resp && resp.error) {
|
|
showToast("Clip failed: " + resp.error, true);
|
|
}
|
|
} catch (e) {
|
|
console.error("FlowDeck clipper", e);
|
|
showToast("Clip failed: " + e.message, true);
|
|
}
|
|
}
|
|
|
|
function showToast(msg, isError) {
|
|
let t = document.getElementById("fd-clipper-toast");
|
|
if (!t) {
|
|
t = document.createElement("div");
|
|
t.id = "fd-clipper-toast";
|
|
t.className = "fd-clipper-toast";
|
|
document.body.appendChild(t);
|
|
}
|
|
t.textContent = msg;
|
|
t.style.background = isError ? "#e03e3e" : "#1a7f37";
|
|
t.classList.add("show");
|
|
setTimeout(() => t.classList.remove("show"), 3000);
|
|
}
|
|
|
|
// Context menu via right-click: if selection exists, inject inline menu
|
|
document.addEventListener("mouseup", () => {
|
|
// No heavy UI here; popup/background handles contextMenus API
|
|
});
|
|
|
|
// Keyboard shortcut Ctrl+Shift+C
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === "c") {
|
|
e.preventDefault();
|
|
openClipper("article");
|
|
}
|
|
});
|
|
|
|
// Expose for popup
|
|
window.__fdOpenClipper = openClipper;
|
|
|
|
if (document.readyState === "loading") {
|
|
document.addEventListener("DOMContentLoaded", createFloatButton);
|
|
} else {
|
|
createFloatButton();
|
|
}
|
|
})();
|