Files
flowdeck/extension/content.js
T
bruno 0b251649e5
FlowDeck CI / lint (push) Successful in 1m11s
FlowDeck CI / test (push) Failing after 8m32s
FlowDeck CI / docker (push) Skipped
feat: v6.2.0 Web Clipper — extension navigateur (capture article/selection/bookmark/screenshot)
- 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
2026-09-19 23:26:03 -04:00

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();
}
})();