firefox-focus/background.js
2026-04-06 12:46:20 -07:00

479 lines
11 KiB
JavaScript

const HOME_URL = browser.runtime.getURL("home.html");
const OPTIONS_URL = browser.runtime.getURL("options.html");
const DEFAULT_SETTINGS = {
bookmarks: [
{ title: "Mozilla", url: "https://www.mozilla.org/" },
{ title: "DuckDuckGo", url: "https://duckduckgo.com/" },
{ title: "Wikipedia", url: "https://www.wikipedia.org/" },
{ title: "Proton", url: "https://proton.me/" }
],
blocking: {
trackers: true,
ads: false,
analytics: false,
social: false
}
};
const BLOCKLISTS = {
trackers: [
"doubleclick.net",
"googlesyndication.com",
"googletagmanager.com",
"googleadservices.com",
"taboola.com",
"outbrain.com",
"scorecardresearch.com",
"zedo.com",
"criteo.com"
],
ads: [
"ads.",
"adservice.",
"adserver.",
"/ads/",
"amazon-adsystem.com",
"adnxs.com",
"rubiconproject.com",
"openx.net"
],
analytics: [
"google-analytics.com",
"analytics.",
"segment.com",
"mixpanel.com",
"hotjar.com",
"fullstory.com",
"amplitude.com",
"plausible.io"
],
social: [
"facebook.net",
"facebook.com/plugins",
"platform.twitter.com",
"connect.facebook.net",
"platform.linkedin.com",
"assets.pinterest.com",
"redditstatic.com"
]
};
let isCollapsingTabs = false;
let currentSettings = cloneDefaultSettings();
const pendingCollapseTimers = new Map();
const pendingCollapseStartedAt = new Map();
const NON_MEANINGFUL_COLLAPSE_GRACE_MS = 800;
function cloneDefaultSettings() {
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
function normalizeUrl(rawUrl) {
if (!rawUrl) {
return "";
}
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(rawUrl)) {
return rawUrl;
}
if (rawUrl.includes(" ") && !rawUrl.includes(".")) {
return `https://duckduckgo.com/?q=${encodeURIComponent(rawUrl)}`;
}
return `https://${rawUrl}`;
}
function sanitizeBookmarks(bookmarks) {
const fallback = cloneDefaultSettings().bookmarks;
const sanitized = Array.isArray(bookmarks) ? bookmarks.slice(0, 4).map((bookmark, index) => {
const safeFallback = fallback[index] || { title: "", url: "" };
return {
title: String(bookmark?.title || safeFallback.title).trim(),
url: String(bookmark?.url || safeFallback.url).trim()
};
}) : [];
while (sanitized.length < 4) {
sanitized.push(fallback[sanitized.length]);
}
return sanitized;
}
async function loadSettings() {
const stored = await browser.storage.local.get("settings");
const defaults = cloneDefaultSettings();
const settings = {
bookmarks: sanitizeBookmarks(stored.settings?.bookmarks || defaults.bookmarks),
blocking: {
trackers: stored.settings?.blocking?.trackers ?? defaults.blocking.trackers,
ads: stored.settings?.blocking?.ads ?? defaults.blocking.ads,
analytics: stored.settings?.blocking?.analytics ?? defaults.blocking.analytics,
social: stored.settings?.blocking?.social ?? defaults.blocking.social
}
};
currentSettings = settings;
return settings;
}
async function saveSettings(settings) {
currentSettings = {
bookmarks: sanitizeBookmarks(settings.bookmarks),
blocking: {
trackers: Boolean(settings.blocking?.trackers),
ads: Boolean(settings.blocking?.ads),
analytics: Boolean(settings.blocking?.analytics),
social: Boolean(settings.blocking?.social)
}
};
await browser.storage.local.set({ settings: currentSettings });
return currentSettings;
}
function isMeaningfulUrl(url) {
if (!url) {
return false;
}
return ![
"about:blank",
"about:newtab",
HOME_URL
].includes(url);
}
async function getAllNormalTabs() {
const windows = await browser.windows.getAll({ populate: true, windowTypes: ["normal"] });
return windows
.sort((left, right) => left.id - right.id)
.flatMap((windowInfo) => (windowInfo.tabs || []).map((tab) => ({ ...tab, __window: windowInfo })));
}
async function getKeeperTab(excludedTabId) {
const windows = await browser.windows.getAll({ populate: true, windowTypes: ["normal"] });
const sortedWindows = windows.sort((left, right) => {
if (left.focused && !right.focused) {
return -1;
}
if (!left.focused && right.focused) {
return 1;
}
return left.id - right.id;
});
for (const windowInfo of sortedWindows) {
const activeTab = (windowInfo.tabs || []).find((tab) => tab.active && tab.id !== excludedTabId);
if (activeTab) {
return activeTab;
}
const firstTab = (windowInfo.tabs || []).find((tab) => tab.id !== excludedTabId);
if (firstTab) {
return firstTab;
}
}
return null;
}
async function focusTab(tabId, windowId) {
await browser.tabs.update(tabId, { active: true });
await browser.windows.update(windowId, { focused: true });
}
function clearPendingCollapse(tabId) {
const timerId = pendingCollapseTimers.get(tabId);
if (timerId) {
clearTimeout(timerId);
pendingCollapseTimers.delete(tabId);
}
pendingCollapseStartedAt.delete(tabId);
}
function queueCollapse(tabId, delay = 150) {
const existingTimerId = pendingCollapseTimers.get(tabId);
if (existingTimerId) {
clearTimeout(existingTimerId);
}
if (!pendingCollapseStartedAt.has(tabId)) {
pendingCollapseStartedAt.set(tabId, Date.now());
}
const timerId = setTimeout(() => {
pendingCollapseTimers.delete(tabId);
void collapseExtraTab(tabId);
}, delay);
pendingCollapseTimers.set(tabId, timerId);
}
async function collapseExtraTab(tabId) {
const timerId = pendingCollapseTimers.get(tabId);
if (timerId) {
clearTimeout(timerId);
pendingCollapseTimers.delete(tabId);
}
if (isCollapsingTabs) {
return;
}
const tab = await browser.tabs.get(tabId).catch(() => null);
if (!tab) {
pendingCollapseStartedAt.delete(tabId);
return;
}
if (!isMeaningfulUrl(tab.url) && (tab.pendingUrl || tab.openerTabId)) {
queueCollapse(tabId);
return;
}
if (!isMeaningfulUrl(tab.url)) {
const allTabs = await getAllNormalTabs();
if (allTabs.length <= 1) {
pendingCollapseStartedAt.delete(tabId);
return;
}
const startedAt = pendingCollapseStartedAt.get(tabId) || Date.now();
if (Date.now() - startedAt < NON_MEANINGFUL_COLLAPSE_GRACE_MS) {
queueCollapse(tabId);
return;
}
}
const allTabs = await getAllNormalTabs();
if (allTabs.length <= 1) {
pendingCollapseStartedAt.delete(tabId);
return;
}
const keeper = await getKeeperTab(tab.id);
if (!keeper) {
pendingCollapseStartedAt.delete(tabId);
return;
}
isCollapsingTabs = true;
try {
const nextUrl = isMeaningfulUrl(tab.url) ? tab.url : HOME_URL;
await browser.tabs.update(keeper.id, { url: nextUrl });
await browser.tabs.remove(tab.id).catch(() => {});
await focusTab(keeper.id, keeper.windowId);
if (tab.windowId !== keeper.windowId) {
await browser.windows.remove(tab.windowId).catch(() => {});
}
} finally {
pendingCollapseStartedAt.delete(tabId);
isCollapsingTabs = false;
}
}
async function enforceSingleTab() {
if (isCollapsingTabs) {
return;
}
const allTabs = await getAllNormalTabs();
if (allTabs.length <= 1) {
return;
}
const keeper = await getKeeperTab();
if (!keeper) {
return;
}
isCollapsingTabs = true;
try {
for (const tab of allTabs) {
if (tab.id === keeper.id) {
continue;
}
if (isMeaningfulUrl(tab.url)) {
await browser.tabs.update(keeper.id, { url: tab.url });
}
await browser.tabs.remove(tab.id).catch(() => {});
}
const windows = await browser.windows.getAll({ windowTypes: ["normal"] });
for (const windowInfo of windows) {
if (windowInfo.id !== keeper.windowId) {
await browser.windows.remove(windowInfo.id).catch(() => {});
}
}
await focusTab(keeper.id, keeper.windowId);
} finally {
isCollapsingTabs = false;
}
}
function shouldBlock(details) {
if (details.tabId < 0) {
return false;
}
const url = details.url || "";
let decodedUrl = url;
try {
decodedUrl = decodeURIComponent(url);
} catch (error) {
decodedUrl = url;
}
return Object.entries(currentSettings.blocking).some(([category, enabled]) => {
if (!enabled) {
return false;
}
return BLOCKLISTS[category].some((pattern) => decodedUrl.includes(pattern));
});
}
async function clearSession(options = {}) {
const settings = options.preserveSettings ? await loadSettings() : currentSettings;
const redirectHome = options.redirectHome ?? true;
await browser.browsingData.remove(
{},
{
cache: true,
cookies: true,
downloads: true,
formData: true,
history: true,
indexedDB: true,
localStorage: true,
serviceWorkers: true
}
);
if (options.preserveSettings) {
await browser.storage.local.clear();
await browser.storage.local.set({ settings });
currentSettings = settings;
}
const keeper = await getKeeperTab();
if (keeper && (redirectHome || !isMeaningfulUrl(keeper.url))) {
await browser.tabs.update(keeper.id, { url: HOME_URL, active: true });
await focusTab(keeper.id, keeper.windowId);
} else if (!keeper) {
await browser.tabs.create({ url: HOME_URL });
}
await enforceSingleTab();
}
async function openInKeeperTab(url) {
const normalized = normalizeUrl(url);
const keeper = await getKeeperTab();
if (keeper) {
await browser.tabs.update(keeper.id, { url: normalized, active: true });
await focusTab(keeper.id, keeper.windowId);
} else {
await browser.tabs.create({ url: normalized });
}
await enforceSingleTab();
}
browser.runtime.onInstalled.addListener(async () => {
await saveSettings(await loadSettings());
});
browser.runtime.onStartup.addListener(async () => {
await loadSettings();
await clearSession({ preserveSettings: true, redirectHome: false });
});
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName === "local" && changes.settings?.newValue) {
currentSettings = changes.settings.newValue;
}
});
browser.tabs.onCreated.addListener((tab) => {
if (tab.id) {
queueCollapse(tab.id);
}
});
browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
if (changeInfo.url || changeInfo.status === "complete") {
clearPendingCollapse(tabId);
}
if (changeInfo.url) {
void collapseExtraTab(tabId);
} else if (changeInfo.status === "complete") {
queueCollapse(tabId, 0);
}
});
browser.tabs.onRemoved.addListener((tabId) => {
clearPendingCollapse(tabId);
});
browser.windows.onCreated.addListener(() => {
void enforceSingleTab();
});
browser.webNavigation.onCreatedNavigationTarget.addListener((details) => {
if (details.url) {
void openInKeeperTab(details.url);
}
});
browser.webRequest.onBeforeRequest.addListener(
(details) => ({ cancel: shouldBlock(details) }),
{ urls: ["<all_urls>"] },
["blocking"]
);
browser.runtime.onMessage.addListener((message) => {
if (message?.type === "get-settings") {
return loadSettings();
}
if (message?.type === "save-settings") {
return saveSettings(message.settings || currentSettings);
}
if (message?.type === "clear-session") {
return clearSession({ preserveSettings: true });
}
if (message?.type === "open-url") {
return openInKeeperTab(message.url || HOME_URL);
}
if (message?.type === "open-settings") {
return browser.tabs.create({ url: OPTIONS_URL });
}
if (message?.type === "go-home") {
return openInKeeperTab(HOME_URL);
}
return undefined;
});
void loadSettings();