- 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
144 lines
4.6 KiB
JavaScript
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;
|
|
}
|
|
});
|