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?`
'+title+'
'+descHtml+siteHtml+'
'+imgHtml+'
`:`
๐Ÿ”–
Click to add bookmark URL
`; + var safeUrl=url.replace(/"/g,'"'); + return url?`
${title}
${descHtml}${siteHtml}
${imgHtml}
`:`
๐Ÿ”–
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 0000000..0d91d87 Binary files /dev/null and b/extension/flowdeck-clipper.zip differ 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 5bbef2f..0d91d87 100644 Binary files a/static/extension/flowdeck-clipper.zip and b/static/extension/flowdeck-clipper.zip differ 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();