diff --git a/background.js b/background.js index 3573677..9f2bd35 100644 --- a/background.js +++ b/background.js @@ -44,8 +44,19 @@ chrome.alarms.create("tabCleanup", { periodInMinutes: CHECK_INTERVAL_MIN }); chrome.alarms.onAlarm.addListener(async (alarm) => { if (alarm.name !== "tabCleanup") return; - const { exclusions = [], enabled = true, timeoutMin = DEFAULT_TIMEOUT_MIN } = - await chrome.storage.local.get(["exclusions", "enabled", "timeoutMin"]); + const { + exclusions = [], + enabled = true, + timeoutMin = DEFAULT_TIMEOUT_MIN, + excludeAllGroups = false, + excludedGroupTitles = [], + } = await chrome.storage.local.get([ + "exclusions", + "enabled", + "timeoutMin", + "excludeAllGroups", + "excludedGroupTitles", + ]); if (!enabled) return; @@ -53,6 +64,23 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { const timeoutMs = timeoutMin * 60 * 1000; const tabs = await chrome.tabs.query({}); + // Resolve titles for any groups containing tabs (only when needed) + const groupTitleById = {}; + if (excludeAllGroups || excludedGroupTitles.length) { + const groupIds = new Set(); + for (const tab of tabs) { + if (typeof tab.groupId === "number" && tab.groupId !== -1) { + groupIds.add(tab.groupId); + } + } + for (const gid of groupIds) { + try { + const g = await chrome.tabGroups.get(gid); + groupTitleById[gid] = g.title || ""; + } catch {} + } + } + // Never close the last tab in a window const windowTabCounts = {}; for (const tab of tabs) { @@ -97,6 +125,15 @@ chrome.alarms.onAlarm.addListener(async (alarm) => { } catch {} } + // Skip tabs in excluded groups + if (typeof tab.groupId === "number" && tab.groupId !== -1) { + if (excludeAllGroups) continue; + const title = groupTitleById[tab.groupId]; + if (title !== undefined && excludedGroupTitles.includes(title)) { + continue; + } + } + // Check inactivity const lastActive = tabActivity[tab.id] || 0; if (now - lastActive >= timeoutMs) { @@ -213,13 +250,18 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { // Set defaults on install chrome.runtime.onInstalled.addListener(() => { - chrome.storage.local.get(["enabled", "timeoutMin", "exclusions"], (data) => { - const defaults = {}; - if (data.enabled === undefined) defaults.enabled = true; - if (data.timeoutMin === undefined) defaults.timeoutMin = DEFAULT_TIMEOUT_MIN; - if (data.exclusions === undefined) defaults.exclusions = []; - if (Object.keys(defaults).length) { - chrome.storage.local.set(defaults); + chrome.storage.local.get( + ["enabled", "timeoutMin", "exclusions", "excludeAllGroups", "excludedGroupTitles"], + (data) => { + const defaults = {}; + if (data.enabled === undefined) defaults.enabled = true; + if (data.timeoutMin === undefined) defaults.timeoutMin = DEFAULT_TIMEOUT_MIN; + if (data.exclusions === undefined) defaults.exclusions = []; + if (data.excludeAllGroups === undefined) defaults.excludeAllGroups = false; + if (data.excludedGroupTitles === undefined) defaults.excludedGroupTitles = []; + if (Object.keys(defaults).length) { + chrome.storage.local.set(defaults); + } } - }); + ); }); diff --git a/manifest.json b/manifest.json index f891098..1a5afa6 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "🚀 SuperLevels", "version": "2.0", "description": "The super Chrome extension by @levelsio — dark mode, GDPR cookie dismisser, YouTube unhook, tab cleaner, cookie editor, redirect tracer, live CSS editor, X/Twitter dim mode, JS toggle, JSON formatter, music recognizer, picture-in-picture, Google Maps links, and view image.", - "permissions": ["tabs", "storage", "alarms", "cookies", "activeTab", "webRequest", "webNavigation", "tabCapture", "contentSettings", "scripting"], + "permissions": ["tabs", "tabGroups", "storage", "alarms", "cookies", "activeTab", "webRequest", "webNavigation", "tabCapture", "contentSettings", "scripting"], "host_permissions": [""], "background": { "service_worker": "background.js" diff --git a/popup.html b/popup.html index f555a2e..d08903e 100644 --- a/popup.html +++ b/popup.html @@ -123,6 +123,27 @@ font-size: 16px; cursor: pointer; padding: 0 4px; line-height: 1; } .exc-list .item button:hover { color: #ff6b81; } + .exc-list + h2 { margin-top: 14px; } + .group-list.dimmed { opacity: 0.5; pointer-events: none; } + .group-list .group-info { + display: flex; align-items: center; gap: 8px; + flex: 1; min-width: 0; + } + .group-list .group-color { + width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; + } + .group-list .group-title { + flex: 1; min-width: 0; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } + .group-list .group-title.untitled { color: #666; font-style: italic; } + .group-list .item { padding: 5px 10px; } + .group-list .switch { width: 32px; height: 18px; flex-shrink: 0; } + .group-list .slider { border-radius: 18px; } + .group-list .slider::before { + width: 12px; height: 12px; left: 3px; bottom: 3px; + } + .group-list .switch input:checked + .slider::before { transform: translateX(14px); } .closed-section { margin-top: 14px; } .closed-header { display: flex; align-items: center; justify-content: space-between; @@ -766,6 +787,15 @@

Excluded Hosts

+

Excluded Tab Groups

+
+ + +
+
diff --git a/popup.js b/popup.js index ebd1d1f..853633e 100644 --- a/popup.js +++ b/popup.js @@ -93,6 +93,103 @@ function renderExclusionList(exclusions) { }); } +// ── Tab Group Exclusions ── +const excludeAllGroupsEl = document.getElementById("excludeAllGroups"); +const groupListEl = document.getElementById("groupList"); + +const GROUP_COLOR_HEX = { + grey: "#5f6368", + blue: "#1a73e8", + red: "#d93025", + yellow: "#f9ab00", + green: "#1e8e3e", + pink: "#d01884", + purple: "#a142f4", + cyan: "#007b83", + orange: "#fa903e", +}; + +chrome.storage.local.get(["excludeAllGroups", "excludedGroupTitles"], (data) => { + excludeAllGroupsEl.checked = !!data.excludeAllGroups; + applyDimmed(); + renderGroupList(data.excludedGroupTitles || []); +}); + +function refreshGroupList() { + chrome.storage.local.get(["excludedGroupTitles"], (data) => { + renderGroupList(data.excludedGroupTitles || []); + }); +} + +if (chrome.tabGroups) { + chrome.tabGroups.onCreated.addListener(refreshGroupList); + chrome.tabGroups.onUpdated.addListener(refreshGroupList); + chrome.tabGroups.onRemoved.addListener(refreshGroupList); +} + +excludeAllGroupsEl.addEventListener("change", () => { + chrome.storage.local.set({ excludeAllGroups: excludeAllGroupsEl.checked }, applyDimmed); +}); + +function applyDimmed() { + groupListEl.classList.toggle("dimmed", excludeAllGroupsEl.checked); +} + +async function renderGroupList(excluded) { + let visible = []; + try { + if (chrome.tabGroups && chrome.tabGroups.query) { + visible = await chrome.tabGroups.query({}); + } + } catch {} + + // Only titled groups can be excluded individually; the global toggle catches untitled ones. + const visibleTitled = visible.filter((g) => g.title && g.title.trim()); + const visibleTitles = new Set(visibleTitled.map((g) => g.title)); + const orphans = excluded.filter((t) => t && !visibleTitles.has(t)); + const hasUntitled = visible.some((g) => !g.title || !g.title.trim()); + + if (!visibleTitled.length && !orphans.length) { + let msg = "No tab groups found"; + if (hasUntitled) msg += " — name your groups to exclude individually"; + groupListEl.innerHTML = `
${msg}
`; + return; + } + + const renderRow = (title, color, isExcluded) => `
+
+ + ${esc(title)} +
+ +
`; + + const visibleHtml = visibleTitled + .map((g) => renderRow(g.title, GROUP_COLOR_HEX[g.color] || "#888", excluded.includes(g.title))) + .join(""); + const orphanHtml = orphans.map((t) => renderRow(t, "#444", true)).join(""); + + groupListEl.innerHTML = visibleHtml + orphanHtml; + + groupListEl.querySelectorAll('input[type="checkbox"][data-title]').forEach((cb) => { + cb.addEventListener("change", () => { + const title = cb.dataset.title; + chrome.storage.local.get(["excludedGroupTitles"], (data) => { + let list = data.excludedGroupTitles || []; + if (cb.checked) { + if (!list.includes(title)) list = [...list, title]; + } else { + list = list.filter((t) => t !== title); + } + chrome.storage.local.set({ excludedGroupTitles: list }, () => renderGroupList(list)); + }); + }); + }); +} + // ── Closed Tabs History ── const closedSection = document.getElementById("closedSection");