// 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; } });