From ea19d1d0507c60c0ab44ed707446da40dea811e4 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 20 Sep 2026 11:02:49 -0400 Subject: [PATCH] fix(web-clipper): bouton rond transparent draggable + toggle affichage + refresh auto sidebar + fix bloc bookmark (v6.2.1) - bouton flottant rond (44px), semi-transparent blur, hover plus fonce, deplacable souris (pos persistee storage) - option afficher/cacher dans popup extension (showButton) - clipper: notification instantanee des onglets FlowDeck via tabs.sendMessage + polling 15s + BroadcastChannel + visibilitychange - fix bookmark template literal '+title+' -> \/\/\/\/\ (_page_editor_scripts.html) - sync static/extension + zip --- app/templates/_page_editor_scripts.html | 5 +- app/templates/base.html | 41 +++++ extension/background.js | 25 +++- extension/clipper.css | 18 ++- extension/content.js | 190 ++++++++++++++++++++++-- extension/flowdeck-clipper.zip | Bin 0 -> 9977 bytes extension/popup.html | 2 + extension/popup.js | 21 ++- static/extension/background.js | 25 +++- static/extension/clipper.css | 18 ++- static/extension/content.js | 190 ++++++++++++++++++++++-- static/extension/flowdeck-clipper.zip | Bin 7574 -> 9977 bytes static/extension/popup.html | 2 + static/extension/popup.js | 21 ++- 14 files changed, 520 insertions(+), 38 deletions(-) create mode 100644 extension/flowdeck-clipper.zip diff --git a/app/templates/_page_editor_scripts.html b/app/templates/_page_editor_scripts.html index 57219b7..2a1c8d9 100644 --- a/app/templates/_page_editor_scripts.html +++ b/app/templates/_page_editor_scripts.html @@ -548,10 +548,11 @@ window.__fdEditorScriptsLoaded = true; } if(b.type==='bookmark'){ var url=b.url||b.src||'';var title=esc(b.title||url);var desc=esc(b.description||'');var img=b.image||'';var site=esc(b.site_name||''); - var imgHtml=img?'':''; + var imgHtml=img?'':''; var descHtml=desc?'
'+desc+'
':''; var siteHtml=site?'
'+site+'
':''; - return url?`
`:`
๐Ÿ”–
Click to add bookmark URL
`; + var safeUrl=url.replace(/"/g,'"'); + return url?`
`:`
๐Ÿ”–
Click to add bookmark URL
`; } if(b.type==='embed'){ if(b.embed_type==='inline_dbs')return `
๐Ÿ“Š Inline Databases
`; diff --git a/app/templates/base.html b/app/templates/base.html index 189b655..e84c01c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1100,6 +1100,47 @@ this.loadAgents(); this.initSidebarWidth(); document.addEventListener('fd-toggle-sidebar', () => this.toggleSidebar()); + this.startClipAutoRefresh(); + }, + // โ”€โ”€ Auto-refresh sidebar after a web clip (no manual reload) โ”€โ”€ + _clipRefreshTimer: null, + _clipRefreshInterval: null, + startClipAutoRefresh() { + const self = this; + // Instant refresh when extension notifies (content script forwards event) + window.addEventListener('flowdeck:clip-created', () => { + clearTimeout(self._clipRefreshTimer); + self._clipRefreshTimer = setTimeout(() => self.refreshSidebarTree(), 400); + }); + document.addEventListener('flowdeck:workspace-changed', () => { + clearTimeout(self._clipRefreshTimer); + self._clipRefreshTimer = setTimeout(() => self.refreshSidebarTree(), 400); + }); + // Also refresh when tab becomes visible (user returns to FlowDeck after clipping) + document.addEventListener('visibilitychange', () => { + if (!document.hidden) self.refreshSidebarTree(); + }); + window.addEventListener('focus', () => self.refreshSidebarTree()); + // Polling fallback every 15s while tab is visible (covers extension clip without message) + try { + self._clipRefreshInterval = setInterval(() => { + if (!document.hidden) self.refreshSidebarTree(); + }, 15000); + } catch (_e) {} + // BroadcastChannel cross-tab sync (all FlowDeck tabs refresh together) + try { + if (window.BroadcastChannel) { + const bc = new BroadcastChannel('flowdeck'); + bc.onmessage = (ev) => { + if (ev.data && ev.data.type === 'clip-created') self.refreshSidebarTree(); + }; + window._fdBC = bc; + // Bridge DOM event โ†’ BroadcastChannel for extension-triggered clips + window.addEventListener('flowdeck:clip-created', () => { + try { bc.postMessage({ type: 'clip-created', at: Date.now() }); } catch (_e) {} + }); + } + } catch (_e) {} }, agentList: [], loadAgents() { diff --git a/extension/background.js b/extension/background.js index 61c9f1e..28e01e2 100644 --- a/extension/background.js +++ b/extension/background.js @@ -4,7 +4,9 @@ const DEFAULT_SERVER = "http://localhost:8080"; async function getConfig() { const data = await chrome.storage.local.get(STORAGE_KEY); - return data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" }; + const cfg = data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" }; + if (cfg.showButton === undefined) cfg.showButton = true; + return cfg; } async function setConfig(cfg) { @@ -85,9 +87,30 @@ async function handleClip(data) { if (!resp.ok) { return { error: body.detail || body.error || "clip_failed", status: resp.status, ...body }; } + // Notify open FlowDeck tabs so the sidebar refreshes instantly without manual reload + try { + notifyFlowDeckTabs(body); + } catch (_e) {} return body; } +async function notifyFlowDeckTabs(clipResult) { + try { + const cfg = await getConfig(); + const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, ""); + let origin = ""; + try { origin = new URL(serverUrl).origin; } catch (_e) { origin = serverUrl; } + const tabs = await chrome.tabs.query({}); + for (const t of tabs) { + if (!t.id || !t.url) continue; + // Only notify tabs that look like FlowDeck (same origin or localhost variants) + const isFlowDeck = t.url.startsWith(origin) || t.url.includes("flowdeck") || t.url.includes("localhost"); + if (!isFlowDeck) continue; + chrome.tabs.sendMessage(t.id, { action: "fdRefreshTree", data: clipResult }).catch(() => {}); + } + } catch (_e) {} +} + async function handleAuth() { // Token is set via popup; verify it const cfg = await getConfig(); diff --git a/extension/clipper.css b/extension/clipper.css index f185cdf..21c6160 100644 --- a/extension/clipper.css +++ b/extension/clipper.css @@ -1,12 +1,20 @@ .fd-clipper-btn{ position:fixed;bottom:20px;right:20px;z-index:2147483647; - background:#2383E2;color:#fff;border:none;border-radius:999px; - padding:10px 16px;font-size:13px;font-weight:600;cursor:pointer; - box-shadow:0 4px 16px rgba(0,0,0,.25);font-family:system-ui,sans-serif; + width:44px;height:44px;border-radius:50%; + background:rgba(35,131,226,0.14);color:#2383E2;border:1px solid rgba(35,131,226,0.22); + backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px); + font-size:18px;font-weight:600;cursor:grab; + box-shadow:0 4px 16px rgba(0,0,0,.18);font-family:system-ui,sans-serif; + display:flex;align-items:center;justify-content:center; + user-select:none;touch-action:none; + transition:background .15s, border-color .15s, transform .1s; + padding:0;line-height:1; } -.fd-clipper-btn:hover{background:#1a6fc2} +.fd-clipper-btn:hover{background:rgba(35,131,226,0.26);border-color:rgba(35,131,226,0.35);color:#1a6fc2} +.fd-clipper-btn:active{cursor:grabbing;transform:scale(0.96);background:rgba(35,131,226,0.32)} +.fd-clipper-btn.fd-dragging{cursor:grabbing;opacity:.92;transition:none} .fd-clipper-toast{ - position:fixed;bottom:60px;right:20px;z-index:2147483647; + position:fixed;bottom:72px;right:20px;z-index:2147483647; color:#fff;padding:10px 14px;border-radius:8px;font-size:13px;font-family:system-ui,sans-serif; transform:translateY(10px);opacity:0;transition:all .2s;pointer-events:none; } diff --git a/extension/content.js b/extension/content.js index fb24bb7..9a11ac3 100644 --- a/extension/content.js +++ b/extension/content.js @@ -5,18 +5,151 @@ window.__fdClipperInjected = true; const BTN_ID = "fd-clipper-btn"; + const STORAGE_KEY = "flowdeck_clipper"; + let dragState = null; - function createFloatButton() { + 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 = "๐Ÿ“Œ Clip to FlowDeck"; - btn.title = "Clip this page to FlowDeck"; - btn.addEventListener("click", () => openClipper("article")); + 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 ""; @@ -44,13 +177,12 @@ 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); + showToast("Clipped to FlowDeck \u2713 \u2014 page " + resp.page_id); } else if (resp && resp.error) { showToast("Clip failed: " + resp.error, true); } @@ -74,10 +206,46 @@ 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 - }); + // 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) => { @@ -87,7 +255,7 @@ } }); - // Expose for popup + // Expose for popup / background scripting window.__fdOpenClipper = openClipper; if (document.readyState === "loading") { diff --git a/extension/flowdeck-clipper.zip b/extension/flowdeck-clipper.zip new file mode 100644 index 0000000000000000000000000000000000000000..0d91d876dd0a2c0b80a9de3c6b48e7125409edc4 GIT binary patch literal 9977 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 Kc=3e*fd2q$Lr3NS literal 0 HcmV?d00001 diff --git a/extension/popup.html b/extension/popup.html index 1daad90..24486cc 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -43,6 +43,8 @@ a{color:#2383E2;text-decoration:none} + +

Rond, semi-transparent, dรฉplaรงable ร  la souris.

Need a token? Open FlowDeck โ†’ Settings โ†’ API tokens or Extensions.

diff --git a/extension/popup.js b/extension/popup.js index b258e7d..3221ed7 100644 --- a/extension/popup.js +++ b/extension/popup.js @@ -5,19 +5,36 @@ async function load() { const cfg = await chrome.runtime.sendMessage({ action: "getConfig" }); $("serverUrl").value = cfg.serverUrl || "http://localhost:8080"; $("token").value = cfg.token || ""; + const sb = $("showButton"); + if (sb) sb.checked = cfg.showButton !== false; } async function save() { const serverUrl = $("serverUrl").value.trim() || "http://localhost:8080"; const token = $("token").value.trim(); - // deviceId preserved + const showButton = $("showButton") ? $("showButton").checked : true; const cfg = await chrome.runtime.sendMessage({ action: "getConfig" }); cfg.serverUrl = serverUrl.replace(/\/$/, ""); cfg.token = token; + cfg.showButton = showButton; await chrome.runtime.sendMessage({ action: "setConfig", config: cfg }); show("Saved โœ“", "ok"); } +async function toggleButton(show) { + const cfg = await chrome.runtime.sendMessage({ action: "getConfig" }); + cfg.showButton = show; + await chrome.runtime.sendMessage({ action: "setConfig", config: cfg }); + // Notify all tabs immediately for instant UX + try { + const tabs = await chrome.tabs.query({}); + for (const t of tabs) { + if (!t.id) continue; + chrome.tabs.sendMessage(t.id, { action: "toggleButton", show }).catch(() => {}); + } + } catch (_e) {} +} + async function verify() { await save(); const res = await chrome.runtime.sendMessage({ action: "auth" }); @@ -72,5 +89,7 @@ $("clipBookmark").addEventListener("click", () => doClip("bookmark")); $("clipSelection").addEventListener("click", () => doClip("selection")); $("openFlowDeck").addEventListener("click", (e) => { e.preventDefault(); chrome.tabs.create({ url: $("serverUrl").value || "http://localhost:8080" }); }); $("openExtensions").addEventListener("click", (e) => { e.preventDefault(); chrome.tabs.create({ url: ($("serverUrl").value || "http://localhost:8080") + "/extensions" }); }); +const _sb = $("showButton"); +if (_sb) _sb.addEventListener("change", (e) => toggleButton(e.target.checked)); load(); diff --git a/static/extension/background.js b/static/extension/background.js index 61c9f1e..28e01e2 100644 --- a/static/extension/background.js +++ b/static/extension/background.js @@ -4,7 +4,9 @@ const DEFAULT_SERVER = "http://localhost:8080"; async function getConfig() { const data = await chrome.storage.local.get(STORAGE_KEY); - return data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" }; + const cfg = data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" }; + if (cfg.showButton === undefined) cfg.showButton = true; + return cfg; } async function setConfig(cfg) { @@ -85,9 +87,30 @@ async function handleClip(data) { if (!resp.ok) { return { error: body.detail || body.error || "clip_failed", status: resp.status, ...body }; } + // Notify open FlowDeck tabs so the sidebar refreshes instantly without manual reload + try { + notifyFlowDeckTabs(body); + } catch (_e) {} return body; } +async function notifyFlowDeckTabs(clipResult) { + try { + const cfg = await getConfig(); + const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, ""); + let origin = ""; + try { origin = new URL(serverUrl).origin; } catch (_e) { origin = serverUrl; } + const tabs = await chrome.tabs.query({}); + for (const t of tabs) { + if (!t.id || !t.url) continue; + // Only notify tabs that look like FlowDeck (same origin or localhost variants) + const isFlowDeck = t.url.startsWith(origin) || t.url.includes("flowdeck") || t.url.includes("localhost"); + if (!isFlowDeck) continue; + chrome.tabs.sendMessage(t.id, { action: "fdRefreshTree", data: clipResult }).catch(() => {}); + } + } catch (_e) {} +} + async function handleAuth() { // Token is set via popup; verify it const cfg = await getConfig(); diff --git a/static/extension/clipper.css b/static/extension/clipper.css index f185cdf..21c6160 100644 --- a/static/extension/clipper.css +++ b/static/extension/clipper.css @@ -1,12 +1,20 @@ .fd-clipper-btn{ position:fixed;bottom:20px;right:20px;z-index:2147483647; - background:#2383E2;color:#fff;border:none;border-radius:999px; - padding:10px 16px;font-size:13px;font-weight:600;cursor:pointer; - box-shadow:0 4px 16px rgba(0,0,0,.25);font-family:system-ui,sans-serif; + width:44px;height:44px;border-radius:50%; + background:rgba(35,131,226,0.14);color:#2383E2;border:1px solid rgba(35,131,226,0.22); + backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px); + font-size:18px;font-weight:600;cursor:grab; + box-shadow:0 4px 16px rgba(0,0,0,.18);font-family:system-ui,sans-serif; + display:flex;align-items:center;justify-content:center; + user-select:none;touch-action:none; + transition:background .15s, border-color .15s, transform .1s; + padding:0;line-height:1; } -.fd-clipper-btn:hover{background:#1a6fc2} +.fd-clipper-btn:hover{background:rgba(35,131,226,0.26);border-color:rgba(35,131,226,0.35);color:#1a6fc2} +.fd-clipper-btn:active{cursor:grabbing;transform:scale(0.96);background:rgba(35,131,226,0.32)} +.fd-clipper-btn.fd-dragging{cursor:grabbing;opacity:.92;transition:none} .fd-clipper-toast{ - position:fixed;bottom:60px;right:20px;z-index:2147483647; + position:fixed;bottom:72px;right:20px;z-index:2147483647; color:#fff;padding:10px 14px;border-radius:8px;font-size:13px;font-family:system-ui,sans-serif; transform:translateY(10px);opacity:0;transition:all .2s;pointer-events:none; } diff --git a/static/extension/content.js b/static/extension/content.js index fb24bb7..9a11ac3 100644 --- a/static/extension/content.js +++ b/static/extension/content.js @@ -5,18 +5,151 @@ window.__fdClipperInjected = true; const BTN_ID = "fd-clipper-btn"; + const STORAGE_KEY = "flowdeck_clipper"; + let dragState = null; - function createFloatButton() { + 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 = "๐Ÿ“Œ Clip to FlowDeck"; - btn.title = "Clip this page to FlowDeck"; - btn.addEventListener("click", () => openClipper("article")); + 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 ""; @@ -44,13 +177,12 @@ 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); + showToast("Clipped to FlowDeck \u2713 \u2014 page " + resp.page_id); } else if (resp && resp.error) { showToast("Clip failed: " + resp.error, true); } @@ -74,10 +206,46 @@ 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 - }); + // 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) => { @@ -87,7 +255,7 @@ } }); - // Expose for popup + // Expose for popup / background scripting window.__fdOpenClipper = openClipper; if (document.readyState === "loading") { diff --git a/static/extension/flowdeck-clipper.zip b/static/extension/flowdeck-clipper.zip index 5bbef2f3d7ac7028adfe9ff49826ab81c845cb4c..0d91d876dd0a2c0b80a9de3c6b48e7125409edc4 100644 GIT binary patch delta 8331 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 delta 5881 zcmZvgWl+^!*TxUsAtfGKx(%(uab^TUJ7Nt~ILPIA5fk4=x@cQS*FV*CntuR2KB*KJI zG%`SUHHPX1@jal%TdH28;o1mGQm+|hNZJn|^rmijgIB2ic|CJ6)#0G)!UP;IF9v#8 zkvQrmJ;t_iII>D;9MimJoSUH?_I-Xf7&T7TQaEwSek$x6swGtR$~gB^tKS`3e$=p1 z88|IS>La62hvu%ZT^LU)83q`Shn66wE*|LaxVk>w>rJsvs2KM1Ck=~G4t)?*T>Mh=Z3G&6LbTRL1^wXb81?(Br6C2gql#~WocCEBPZ zalLv%Wcm?(<$QC1PY4D1t^@U(ZB5&EeV9RbxK>)C%{pyE`wq5bt|P)N6Q%g?VXsk30m9XGKrLu~^Ye65%vL zo6V9x3feueUqe~@POM1UKh~LgB-^mK)`Ve&>sXY8aIMRIBP7wni8<+H@I5s>K8*MD z3LWDI6UANS5$_4GHX}4PUZb4IH34i10>1aOk^qtFMP^>4{`n4kU7P0N3 zJSRmen-?V7QYXZ#n#J`c{d?8`&j_Wi`9Ttq8Nq<@5BjGB>#9=NdlYcKE02B%HI^*FbPiHF z8K*^dNuRngI*tI;?-ni)1ia|-7YW6}FrssWb|qoZLP7>6`O=glZmVQxky}2OOHdLy zWutb^C`?r&Xiou$%4$7XNEpS22R%FQP<_H%e{#-ytLcF$)W9R#qV z-QsLYIlFmP0c8o*P~z9(D$N4$X>tLou%q&UCClmTPL&6sIv{Kdz>!SvAirP0R(Vsv z2+QuE@II!+2g|gtC=Oysw--lPnf2&4%(A>xSv9P9qg)WNOP$;?q)loXWjKh3MC}%y zLXJalZSd_1wGGS(rKLrmhm)6`+FINM)k*eSx1}XU@b@-h^w#ig>Dy}H&YG=&T@!WT zeNm>p2shuIr#jVu4vg~k?9JFnTP}h%y8HqhGVk`_<0v%;eO8T^VqS|a=i@@!SZu_7 z$DNXvjJTK88zvnCA09Yg`+C1ui^e&xPiDS14M-kc%*r-y_^ohf|pO?gxQ-s_z3uQCM1i_!gAR|L$?)T0- zJ<|;8D3NcKbz1sXN=`Nf!{48okd+9*&EQ`kgao}FHrR&J8ev#C*1-d{#UGl_nRjr% zD-?MdJvHM8Do|>4MO?uN)ZCA(j1ky2yVKFb{eIUGY`L#ad0Gl@yFY(#Vp($cV6~h znne(q_awF9ffanX1~1qe_F?l6uVXu3EqZ5$m^EJl9$}CDTz9~c5aJ#)7EC9ySfOJ=nV?a7|be7#^%(Pz7}mu7>B+*G1Q z;(nM~w@CJT+WZDNPiwnNeCN%2BjhLNdE=mT>me|@$O1}0-8s=;7RUc#v1l?7TKf_O z1PVe0f$;ty+QPvW3bk~BSh%`Ie;$nKpeGBt{D?~;m@Y2^J86P>Ksr z7Fm(1)Pq84aN&)3*TdiPNo^_>Wl4n7?HQ!OTpwuoDU00_11E`5@%dQ=_zLDB1E;|? zkNC#KI0ZM6g?$7{#Km4`vw#R?bXH{348@VKeHND7V?}n|Akr}nG}$G7k2gD`yvFR3Z|Lsij6;HD`~bZX%>sLu;;8eNsg`6BCzvvkQe zDBk$F+l5Ev_{{ocWjc0VOHk+UX)>4CACTV1e=8mQr_w};N?o#le!WHl5D535LOVOT zSvt8v06W(ty+NmWS>l!(_I&&mb+~l>IfK6@i~PICyymR0f98bd5bJJ)EqK@W9}>cd zEB#+eI`39WE_17m$gz2(01;d-#1(R^TJE-PJgh?#aC})OuBj75uW%keBW>c+=qy-% z2!5p?tzb$rs3elEyrOv@fn$dI2DMy$TD=Ot0DzN4JW6@O#qpGR_4{@UE1SA56crPL z<$F?aMz1zBjV7dD@mg*b`z1BXF>|mzM5`ud>&$ZQnQaPnfNU|s*8?3cfoo20yjq>h z2q(jDFPX1B>vs)f(9c&Rp}Tu0#Rui334O)Lcsn$8(lPyTRKwRT|yZM z+}~e?o;{Lx-y-Z3#=+XLaB<3r^vN+uHz`#gBNq%Nai?SoWc6&HnWK=v(#dg9#zxBz zXGIOt(9+ktmxj52?<27k>93dX#87x#4a~YQ$TZk`_Stj1vJRY{$nvjq2Pmlo_6;gp zsx&Sy1%$?wt%S6a$XQ}m5g%3eKJlj*2t~-BJ*9q)YAGx1i)USyM$FG~RYODe9U?As z-G1+@u)1qQnRq&hGOpEPN2B)JPKhv-mRVZCA%owH(_E$}AIh%L+S1&zmuHcm3@Grd zj1UwnEllwgNaO#mc?I%N>}Hw=f?nB>mW(g#s}y9 zv1`+keU)VvFW;uYT=z?1Usn-uAiD8P@-%Zvi1~%2_c@4zJD=M+o5V^h=gqpy0(zay zi=Qy})_I99vLf!7z86p@yflxzIRFqfcm48w(IcCHBTm0BOhxo7h~o<|v{{Sm{?k%d z#MhzP4^qRSp+$qm@ueYnC8a0#vBvz_(Gdf7x6B0Dop@W)t;9?aB9ojjM_$*Inp%;L z?QZ9=UUVn*e55c$-zCLXr+4B`rs43Nvmtsq31Dt&-;ERh zrUaGISYZuA7Sqx@x*Lw)Nz|5+qy=wA;CHX6IMgoY3v2kZ z-tM-+F1X}8mZSBeU;_R&dKFvyu}fYH*L771uKz8x3U2}HfJz$)tY5^U@e_BhImYsb z^xiI$Jt@YJz0(M;o5V7aJjPZVKE_A$aTL2w7dQSY?nvRxHpku_!*6E#+DVCDzP3qH zM`I^6-3-w!T?SX7?3~>#YW8L(x1u8qE^pD4gL) zYjQ@5)rPm8`f_>Y6X5fxCy1okq0jhWELoUr3jIfY~vF+m{B}?LD zMIT4xGMxjF!?M>JL$T6w`rV5vL<@uLPWYV3pykx4IQ{7F#(gWnx9rL0;3&_xLbqQ} zxyzr-c`Pg3w#-Kz)B-5~fHy&s1zgQXIfDX3|AFu5Koy(~9R%vc`44=d&QNzK#Kz6h zAyaogW>0|Zp*GI&uXSa@|WPk1dRlXsh%lHxmUHXxGkn zfqv4PZ|a7k=KAEnfAI18SyQX`qa`bTCKDBbu>a~~aEXz+WrD*j#0D+ zee+4H6eX;`ob-c~w84j!xyI@z$lRW#Ou9v%2Zr;1WkGq%_g*MW&)^1MZ9 za7bSti+TSEm$Ip3wRwZ(1Z2GZneOC!S3b>2x0qhLY)f_QRFYnp>Jh!i`U>|A zL6g}x29-k{ImT2a`%Mv=qP-O$gbZIecE6+;EnS&K&uC9)2pwI?G!mwg0R3<*^c78_ zM;^kz&-}Nc zNc~8otal4a@i!)Xb&@#=C-nKX^7aGY8q?QLuHO8bBRsy(vHk*-CIhGQad?DC9-2=YXB7XOToJMw%`Ui0x zc#_KcUUU8E@uSfd~uPkx|f$b6Qs{7CUB*+H3&Z6}0V!EEJe`=le%HgR#p3 z^jq!}!l}3H$?o6DI|Kc2JBbydaj$#BxAI@QRP3fzzrOU!yDj(1Lff$Y&rFJ}W(9-H`zc{c=G(MLezDD5dVC z7%A|RobT~7ZpovBz>?M`0s?f28lc-QRzm~XETmVxHO6?+B`Pn9GYHVJ)TOQ52shD2 z%J-6O6sF|Ws5UBMdA{2F`SOE{pk|z@?#QR>T>PrLT%rAsBHFxez0sOl(|ha>7qe|e2+#dMq1CJe{3qgppowVym>)g zUM@KhK4;En`v#U?kEgxxZRuMCS>OWd&&P)+Tn!Pqqt}MKQf1JJR0iYUI@<9~23EfX z+kRDEFKoWBVaKFWWdX9@TXOi8XP;A!?>gAY3T4*8?z?d{95-ICt%8?E=4i zxX|OoI-w^RXZ$ZtI^`W0&M_8GVzFJ`AO_pD3F&o4X*E3kd+yXsn=w+D?ODoxhS zK=RK3c}EK}Jv|4m=BgL8jWN3rrBtW6C*+>%6FpOnA2?zoo@Mt23zYZ;U#BD&JC5$H z*W0>vd)Wbbf*E^{EXUvIMpvhHbG`c=W8GH{tMq7XbAS1Z&49qty@lN$mSLab8e9GK zvwV6sW+aODt+?2h>f@7-LM7fd1!66)nSLHWLq;wryy@UloA!4Ync@${f;!_88Ay_% zpyJNpcMyNLxk9Kl_Dq{1UulZ9b*YAf%3n^VVkVvgP$P|nOuxj8;Y0Z4%~T_WxN%~~ zWLM0WD<~_#rWGKDAt+mE7Y?iIH4fwMIR8}iH6KCNH=XT&xzI<%nE(C=)5h5g1FF;| z@1(7;S(@E;T5|9c4k zn}rPc{Ab|(FGoRL>96{~&BT8nUSTFcSokH70aky=>S*RcKUCPe{1dj zYG@KBWV29m{2QnLCgqU)rFOz+CN^|rfcxWwW+t&e!8^)?J0{UTf##D0F=kP2WPl_X m1hV}zxx4cGb8_Pi@x{|Ms#*_wZB6bRKnxBmgEi0Zun diff --git a/static/extension/popup.html b/static/extension/popup.html index 1daad90..24486cc 100644 --- a/static/extension/popup.html +++ b/static/extension/popup.html @@ -43,6 +43,8 @@ a{color:#2383E2;text-decoration:none} + +

Rond, semi-transparent, dรฉplaรงable ร  la souris.

Need a token? Open FlowDeck โ†’ Settings โ†’ API tokens or Extensions.

diff --git a/static/extension/popup.js b/static/extension/popup.js index b258e7d..3221ed7 100644 --- a/static/extension/popup.js +++ b/static/extension/popup.js @@ -5,19 +5,36 @@ async function load() { const cfg = await chrome.runtime.sendMessage({ action: "getConfig" }); $("serverUrl").value = cfg.serverUrl || "http://localhost:8080"; $("token").value = cfg.token || ""; + const sb = $("showButton"); + if (sb) sb.checked = cfg.showButton !== false; } async function save() { const serverUrl = $("serverUrl").value.trim() || "http://localhost:8080"; const token = $("token").value.trim(); - // deviceId preserved + const showButton = $("showButton") ? $("showButton").checked : true; const cfg = await chrome.runtime.sendMessage({ action: "getConfig" }); cfg.serverUrl = serverUrl.replace(/\/$/, ""); cfg.token = token; + cfg.showButton = showButton; await chrome.runtime.sendMessage({ action: "setConfig", config: cfg }); show("Saved โœ“", "ok"); } +async function toggleButton(show) { + const cfg = await chrome.runtime.sendMessage({ action: "getConfig" }); + cfg.showButton = show; + await chrome.runtime.sendMessage({ action: "setConfig", config: cfg }); + // Notify all tabs immediately for instant UX + try { + const tabs = await chrome.tabs.query({}); + for (const t of tabs) { + if (!t.id) continue; + chrome.tabs.sendMessage(t.id, { action: "toggleButton", show }).catch(() => {}); + } + } catch (_e) {} +} + async function verify() { await save(); const res = await chrome.runtime.sendMessage({ action: "auth" }); @@ -72,5 +89,7 @@ $("clipBookmark").addEventListener("click", () => doClip("bookmark")); $("clipSelection").addEventListener("click", () => doClip("selection")); $("openFlowDeck").addEventListener("click", (e) => { e.preventDefault(); chrome.tabs.create({ url: $("serverUrl").value || "http://localhost:8080" }); }); $("openExtensions").addEventListener("click", (e) => { e.preventDefault(); chrome.tabs.create({ url: ($("serverUrl").value || "http://localhost:8080") + "/extensions" }); }); +const _sb = $("showButton"); +if (_sb) _sb.addEventListener("change", (e) => toggleButton(e.target.checked)); load();