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
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
// 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();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user