// 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"; const STORAGE_KEY = "flowdeck_clipper"; let dragState = null; async function getCfg() { try { const data = await chrome.storage.local.get(STORAGE_KEY); return data[STORAGE_KEY] || {}; } catch (e) { return {}; } } async function createFloatButton() { const cfg = await getCfg(); if (cfg.showButton === false) return; if (document.getElementById(BTN_ID)) return; if (!document.body) return; const btn = document.createElement("button"); btn.id = BTN_ID; btn.className = "fd-clipper-btn"; btn.textContent = "📌"; btn.title = "Clip to FlowDeck (glisser pour déplacer)"; btn.setAttribute("aria-label", "Clip to FlowDeck"); // Apply saved position const pos = cfg.pos; if (pos && typeof pos.left === "number" && typeof pos.top === "number") { btn.style.left = pos.left + "px"; btn.style.top = pos.top + "px"; btn.style.right = "auto"; btn.style.bottom = "auto"; } btn.addEventListener("click", (e) => { if (btn.dataset.dragged === "1") { btn.dataset.dragged = "0"; e.preventDefault(); e.stopPropagation(); return; } openClipper("article"); }); btn.addEventListener("mousedown", onDragStart); document.body.appendChild(btn); } function removeFloatButton() { const el = document.getElementById(BTN_ID); if (el) el.remove(); } function applyVisibility(show) { if (show === false) { removeFloatButton(); } else { const el = document.getElementById(BTN_ID); if (el) el.style.display = ""; else createFloatButton(); } } function onDragStart(e) { if (e.button !== 0) return; const btn = e.currentTarget; const rect = btn.getBoundingClientRect(); dragState = { btn: btn, offsetX: e.clientX - rect.left, offsetY: e.clientY - rect.top, startX: e.clientX, startY: e.clientY, moved: false }; // Prevent text selection during drag e.preventDefault(); document.addEventListener("mousemove", onDragMove); document.addEventListener("mouseup", onDragEnd); btn.classList.add("fd-dragging"); } function onDragMove(e) { if (!dragState) return; const dx = Math.abs(e.clientX - dragState.startX); const dy = Math.abs(e.clientY - dragState.startY); if (dx > 3 || dy > 3) dragState.moved = true; const btn = dragState.btn; let left = e.clientX - dragState.offsetX; let top = e.clientY - dragState.offsetY; const maxLeft = window.innerWidth - btn.offsetWidth - 4; const maxTop = window.innerHeight - btn.offsetHeight - 4; left = Math.max(4, Math.min(left, maxLeft)); top = Math.max(4, Math.min(top, maxTop)); btn.style.left = left + "px"; btn.style.top = top + "px"; btn.style.right = "auto"; btn.style.bottom = "auto"; } async function onDragEnd(e) { document.removeEventListener("mousemove", onDragMove); document.removeEventListener("mouseup", onDragEnd); if (!dragState) return; const btn = dragState.btn; btn.classList.remove("fd-dragging"); const moved = dragState.moved; dragState = null; if (moved) { // Suppress next click btn.dataset.dragged = "1"; setTimeout(() => { btn.dataset.dragged = "0"; }, 400); const left = parseInt(btn.style.left, 10); const top = parseInt(btn.style.top, 10); if (!isNaN(left) && !isNaN(top)) { try { const data = await chrome.storage.local.get(STORAGE_KEY); const cfg = data[STORAGE_KEY] || {}; cfg.pos = { left: left, top: top }; await chrome.storage.local.set({ [STORAGE_KEY]: cfg }); } catch (_e) {} } } } // Listen to storage changes (show/hide toggle & position sync across tabs) try { if (chrome.storage && chrome.storage.onChanged) { chrome.storage.onChanged.addListener((changes, area) => { if (area !== "local" || !changes[STORAGE_KEY]) return; const nv = changes[STORAGE_KEY].newValue || {}; const ov = changes[STORAGE_KEY].oldValue || {}; if (nv.showButton !== ov.showButton) { applyVisibility(nv.showButton !== false); } // Position already handled via direct style; if changed elsewhere sync it if (nv.pos && JSON.stringify(nv.pos) !== JSON.stringify(ov.pos)) { const el = document.getElementById(BTN_ID); if (el && nv.pos.left != null) { el.style.left = nv.pos.left + "px"; el.style.top = nv.pos.top + "px"; el.style.right = "auto"; el.style.bottom = "auto"; } } }); } } catch (_e) {} 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 || "" } }; 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 \u2713 \u2014 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); } // Runtime messages: from background (context menu, refresh) or popup try { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!message || !message.action) return; if (message.action === "clipSelection") { openClipper("selection"); try { sendResponse({ ok: true }); } catch (_e) {} return true; } if (message.action === "fdRefreshTree" || message.action === "refreshTree" || message.action === "fdClipCreated") { // FlowDeck page refresh signal (injected on FlowDeck origin) try { if (window.appState && typeof window.appState.refreshSidebarTree === "function") { window.appState.refreshSidebarTree(); } else { window.dispatchEvent(new CustomEvent("flowdeck:clip-created", { detail: message.data || {} })); document.dispatchEvent(new CustomEvent("flowdeck:workspace-changed")); // Fallback: try calling global if (window.FlowDeck && window.FlowDeck._toast) {} } // Also try direct fetch fallback const ul = document.getElementById("sidebar-workspace-items"); if (ul && window.appState && window.appState.refreshSidebarTree) { window.appState.refreshSidebarTree(); } } catch (_e) {} try { sendResponse({ ok: true }); } catch (_e) {} return true; } if (message.action === "toggleButton") { applyVisibility(message.show); try { sendResponse({ ok: true }); } catch (_e) {} return true; } if (message.action === "getSelectionHtml") { try { sendResponse({ html: getSelectionHtml() }); } catch (_e) {} return true; } }); } catch (_e) {} // 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 / background scripting window.__fdOpenClipper = openClipper; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", createFloatButton); } else { createFloatButton(); } })();