feat: implement M3 multi-browser support (Firefox, Edge, Safari)

Add browser abstraction layer (compat.ts) that normalizes Chrome/Firefox/
Edge/Safari extension APIs behind a unified promise-based interface.
Replace all direct chrome.* calls with compat layer across service worker,
sync engine, badge, storage, popup, and options modules.

- Browser-specific manifests in manifests/ (Firefox MV3 with gecko settings,
  Edge/Safari variants)
- Multi-target build system: `npm run build` produces all four browser
  builds in build/<browser>/
- Per-browser build scripts: build:chrome, build:firefox, build:edge, build:safari
- Auto-detects browser at runtime for platform-specific device registration
- All 38 existing tests pass, typecheck clean

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
徐枫
2026-03-17 16:58:44 +08:00
parent dc3be4d73f
commit f39ff8c215
14 changed files with 580 additions and 88 deletions

View File

@@ -1,24 +1,19 @@
/**
* Options page script — manages extension settings.
* Uses the browser-agnostic compat layer.
*/
import { runtime, storage } from "../lib/compat";
export {};
// --- Messaging helper ---
function sendMessage(type: string, payload?: unknown): Promise<any> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type, payload }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
return;
}
if (response?.error) {
reject(new Error(response.error));
return;
}
resolve(response);
});
});
async function sendMessage(type: string, payload?: unknown): Promise<any> {
const response = await runtime.sendMessage({ type, payload });
if (response?.error) {
throw new Error(response.error);
}
return response;
}
// --- Elements ---
@@ -59,7 +54,7 @@ let blacklist: string[] = [];
// --- Load Settings ---
async function loadSettings() {
const state = await chrome.storage.local.get(null);
const state = await storage.get(null);
optDeviceName.textContent = state.deviceName || "—";
optDeviceId.textContent = state.deviceId || "—";
@@ -168,7 +163,7 @@ btnAddBlacklist.addEventListener("click", () => {
// Save
btnSave.addEventListener("click", async () => {
await chrome.storage.local.set({
await storage.set({
serverUrl: optServerUrl.value.trim(),
connectionMode: optConnectionMode.value,
pollIntervalSec: parseInt(optPollInterval.value),
@@ -247,7 +242,7 @@ btnClearData.addEventListener("click", async () => {
return;
}
await sendMessage("LOGOUT");
await chrome.storage.local.clear();
await storage.clear();
alert("All data cleared.");
window.close();
});