Files
flowdeck/static/extension/background.js
T
bruno ea19d1d050
FlowDeck CI / lint (push) Successful in 1m14s
FlowDeck CI / test (push) Failing after 8m44s
FlowDeck CI / docker (push) Skipped
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
2026-09-20 11:02:49 -04:00

144 lines
4.6 KiB
JavaScript

// FlowDeck Web Clipper — background service worker (Manifest V3)
const STORAGE_KEY = "flowdeck_clipper";
const DEFAULT_SERVER = "http://localhost:8080";
async function getConfig() {
const data = await chrome.storage.local.get(STORAGE_KEY);
const cfg = data[STORAGE_KEY] || { serverUrl: DEFAULT_SERVER, token: "", deviceId: "" };
if (cfg.showButton === undefined) cfg.showButton = true;
return cfg;
}
async function setConfig(cfg) {
await chrome.storage.local.set({ [STORAGE_KEY]: cfg });
}
function genDeviceId() {
return "dev_" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 10);
}
async function ensureDeviceId() {
const cfg = await getConfig();
if (!cfg.deviceId) {
cfg.deviceId = genDeviceId();
await setConfig(cfg);
}
return cfg.deviceId;
}
chrome.runtime.onInstalled.addListener(async () => {
await ensureDeviceId();
chrome.contextMenus.create({
id: "fd-clip-selection",
title: "📌 Clip selection to FlowDeck",
contexts: ["selection"]
});
chrome.contextMenus.create({
id: "fd-clip-page",
title: "📌 Clip page to FlowDeck",
contexts: ["page"]
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (!tab || !tab.id) return;
if (info.menuItemId === "fd-clip-selection") {
chrome.tabs.sendMessage(tab.id, { action: "clipSelection" });
} else if (info.menuItemId === "fd-clip-page") {
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.__fdOpenClipper && window.__fdOpenClipper("article")
});
}
});
// Unified clip handler
async function handleClip(data) {
const cfg = await getConfig();
const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
const deviceId = cfg.deviceId || (await ensureDeviceId());
const token = cfg.token || "";
// Require token or let server handle session cookie fallback
if (!token) {
// Try clipboard without token; server may accept session if user logged in flowdeck tab
// but background fetch cannot send cookie cross-origin without credentials; warn
}
const payload = {
url: data.url,
title: data.title,
content: data.html || data.content,
content_type: data.content_type || "article",
selection_html: data.selection_html || "",
device_id: deviceId,
metadata: data.metadata || {}
};
const headers = { "Content-Type": "application/json", "X-Device-Id": deviceId };
if (token) headers["Authorization"] = "Bearer " + token;
const resp = await fetch(serverUrl + "/api/v2/web-clipper/clip", {
method: "POST",
headers,
body: JSON.stringify(payload)
});
const body = await resp.json().catch(() => ({}));
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();
if (!cfg.token) return { error: "no_token" };
const serverUrl = (cfg.serverUrl || DEFAULT_SERVER).replace(/\/$/, "");
const resp = await fetch(serverUrl + "/api/v2/web-clipper/status", {
headers: { Authorization: "Bearer " + cfg.token }
});
const body = await resp.json().catch(() => ({}));
return body;
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "clip") {
handleClip(message.data).then((r) => sendResponse(r));
return true;
}
if (message.action === "auth") {
handleAuth().then((r) => sendResponse(r));
return true;
}
if (message.action === "getConfig") {
getConfig().then((c) => sendResponse(c));
return true;
}
if (message.action === "setConfig") {
setConfig(message.config).then(() => sendResponse({ ok: true }));
return true;
}
});