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
This commit is contained in:
@@ -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?'<img src="'+img+'" alt="" style="width:120px;height:90px;object-fit:cover;border-radius:8px;flex-shrink:0;">':'';
|
||||
var imgHtml=img?'<img src="'+img.replace(/"/g,'"')+'" alt="" style="width:120px;height:90px;object-fit:cover;border-radius:8px;flex-shrink:0;">':'';
|
||||
var descHtml=desc?'<div style="font-size:13px;color:var(--text-dim);margin-top:4px;">'+desc+'</div>':'';
|
||||
var siteHtml=site?'<div style="font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.5px;margin-top:6px;">'+site+'</div>':'';
|
||||
return url?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark"><a href="'+url+'" target="_blank" rel="noopener noreferrer" style="text-decoration:none;color:inherit;"><div style="display:flex;gap:14px;align-items:center;border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin:0;background:var(--bg-secondary);"><div style="flex:1;min-width:0;"><div style="font-weight:600;font-size:15px;">'+title+'</div>'+descHtml+siteHtml+'</div>'+imgHtml+'</div></a></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setBookmarkUrl('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🔖</div><div style="font-size:14px;color:var(--text-dim);">Click to add bookmark URL</div></div></div>`;
|
||||
var safeUrl=url.replace(/"/g,'"');
|
||||
return url?`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark"><a href="${safeUrl}" target="_blank" rel="noopener noreferrer" style="text-decoration:none;color:inherit;"><div style="display:flex;gap:14px;align-items:center;border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin:0;background:var(--bg-secondary);"><div style="flex:1;min-width:0;"><div style="font-weight:600;font-size:15px;">${title}</div>${descHtml}${siteHtml}</div>${imgHtml}</div></a></div></div>`:`<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-bookmark" style="text-align:center;padding:60px 20px;background:var(--bg-secondary);border:1px dashed var(--border);border-radius:8px;cursor:pointer;" onclick="E.setBookmarkUrl('${b.id}')"><div style="font-size:48px;margin-bottom:12px;">🔖</div><div style="font-size:14px;color:var(--text-dim);">Click to add bookmark URL</div></div></div>`;
|
||||
}
|
||||
if(b.type==='embed'){
|
||||
if(b.embed_type==='inline_dbs')return `<div class="block-wrapper" data-id="${b.id}"><div class="block-content block-inline-dbs"><div style="padding:20px;text-align:center;color:var(--text-secondary);">📊 Inline Databases</div><div class="inline-dbs-grid">${(b.dbs||[]).map(db=>`<a href="/db/${db.id}/view/${db.view_type||'table'}" class="inline-db-card" target="_blank" style="display:block;padding:12px 16px;background:var(--bg-secondary);border-radius:8px;margin:8px 0;text-decoration:none;color:var(--text-primary);border:1px solid var(--border);"><span style="font-weight:500">📋 ${db.name}</span><span style="float:right;font-size:12px;color:var(--text-tertiary);">${db.view_type==='form'?'📝 Form':'📊 Table'} →</span></a>`).join('')}</div></div></div>`;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+24
-1
@@ -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();
|
||||
|
||||
+13
-5
@@ -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;
|
||||
}
|
||||
|
||||
+179
-11
@@ -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") {
|
||||
|
||||
Binary file not shown.
@@ -43,6 +43,8 @@ a{color:#2383E2;text-decoration:none}
|
||||
<button class="btn btn-secondary" id="save" style="flex:1;padding:7px;font-size:12px;">Save</button>
|
||||
<button class="btn btn-secondary" id="verify" style="flex:1;padding:7px;font-size:12px;">Verify</button>
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer;text-transform:none;letter-spacing:0;font-size:13px;color:#e0e0e0"><input type="checkbox" id="showButton" style="width:auto;accent-color:#2383E2"> Afficher le bouton flottant sur les pages</label>
|
||||
<p class="small" style="margin-left:22px">Rond, semi-transparent, déplaçable à la souris.</p>
|
||||
<div id="status" class="status"></div>
|
||||
<p class="small">Need a token? Open <a href="#" id="openFlowDeck">FlowDeck → Settings → API tokens</a> or <a href="#" id="openExtensions">Extensions</a>.</p>
|
||||
|
||||
|
||||
+20
-1
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+179
-11
@@ -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") {
|
||||
|
||||
Binary file not shown.
@@ -43,6 +43,8 @@ a{color:#2383E2;text-decoration:none}
|
||||
<button class="btn btn-secondary" id="save" style="flex:1;padding:7px;font-size:12px;">Save</button>
|
||||
<button class="btn btn-secondary" id="verify" style="flex:1;padding:7px;font-size:12px;">Verify</button>
|
||||
</div>
|
||||
<label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer;text-transform:none;letter-spacing:0;font-size:13px;color:#e0e0e0"><input type="checkbox" id="showButton" style="width:auto;accent-color:#2383E2"> Afficher le bouton flottant sur les pages</label>
|
||||
<p class="small" style="margin-left:22px">Rond, semi-transparent, déplaçable à la souris.</p>
|
||||
<div id="status" class="status"></div>
|
||||
<p class="small">Need a token? Open <a href="#" id="openFlowDeck">FlowDeck → Settings → API tokens</a> or <a href="#" id="openExtensions">Extensions</a>.</p>
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user