feat: implement M2 Chrome browser extension
Build the CookieBridge Chrome extension (Manifest V3) with: - Background service worker: cookie monitoring via chrome.cookies.onChanged, WebSocket connection to relay server with auto-reconnect, HTTP polling fallback, device registration and pairing flow - Browser-compatible crypto: libsodium-wrappers-sumo for XChaCha20-Poly1305 encryption, Ed25519 signing, X25519 key exchange (mirrors server's sodium-native API) - Popup UI: device registration, connection status indicator (gray/blue/ green/red), cookie/device/sync stats, one-click current site sync, whitelist quick-add, device pairing with 6-digit code - Options page: server URL config, connection mode (auto/WS/polling), poll interval slider, auto-sync toggle, domain whitelist/blacklist management, paired device list, key export/import, data clearing - Sync engine: LWW conflict resolution with Lamport clocks (same as server), bidirectional cookie sync with all paired peers, echo suppression to prevent sync loops - Badge management: icon color reflects state (gray=not logged in, blue=connected, green=syncing with count, red=error) - Build system: esbuild bundling for Chrome 120+, TypeScript with strict mode, clean type checking Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
69
extension/src/lib/badge.ts
Normal file
69
extension/src/lib/badge.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Badge/icon management — updates extension icon color and badge text
|
||||
* based on connection status and sync activity.
|
||||
*
|
||||
* States:
|
||||
* - gray: Not logged in / no device identity
|
||||
* - blue: Connected, idle
|
||||
* - green: Syncing (with count badge)
|
||||
* - red: Error / disconnected
|
||||
*/
|
||||
|
||||
type IconColor = "gray" | "blue" | "green" | "red";
|
||||
|
||||
function iconPath(color: IconColor, size: number): string {
|
||||
return `src/icons/icon-${color}-${size}.png`;
|
||||
}
|
||||
|
||||
function iconSet(color: IconColor): Record<string, string> {
|
||||
return {
|
||||
"16": iconPath(color, 16),
|
||||
"48": iconPath(color, 48),
|
||||
"128": iconPath(color, 128),
|
||||
};
|
||||
}
|
||||
|
||||
export async function setIconState(
|
||||
state: "not_logged_in" | "connected" | "syncing" | "error",
|
||||
syncCount?: number,
|
||||
) {
|
||||
switch (state) {
|
||||
case "not_logged_in":
|
||||
await chrome.action.setIcon({ path: iconSet("gray") });
|
||||
await chrome.action.setBadgeText({ text: "" });
|
||||
break;
|
||||
|
||||
case "connected":
|
||||
await chrome.action.setIcon({ path: iconSet("blue") });
|
||||
await chrome.action.setBadgeText({ text: "" });
|
||||
break;
|
||||
|
||||
case "syncing":
|
||||
await chrome.action.setIcon({ path: iconSet("green") });
|
||||
if (syncCount && syncCount > 0) {
|
||||
await chrome.action.setBadgeText({
|
||||
text: syncCount > 99 ? "99+" : String(syncCount),
|
||||
});
|
||||
await chrome.action.setBadgeBackgroundColor({ color: "#22C55E" });
|
||||
}
|
||||
break;
|
||||
|
||||
case "error":
|
||||
await chrome.action.setIcon({ path: iconSet("red") });
|
||||
await chrome.action.setBadgeText({ text: "!" });
|
||||
await chrome.action.setBadgeBackgroundColor({ color: "#EF4444" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear the sync badge after a delay. */
|
||||
export function clearSyncBadge(delayMs = 3000) {
|
||||
setTimeout(async () => {
|
||||
const state = await chrome.storage.local.get(["apiToken"]);
|
||||
if (state.apiToken) {
|
||||
await setIconState("connected");
|
||||
} else {
|
||||
await setIconState("not_logged_in");
|
||||
}
|
||||
}, delayMs);
|
||||
}
|
||||
Reference in New Issue
Block a user