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: [""] }, ["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();