Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions background.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,43 @@ 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;

const now = Date.now();
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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
});
);
});
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ["<all_urls>"],
"background": {
"service_worker": "background.js"
Expand Down
30 changes: 30 additions & 0 deletions popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -766,6 +787,15 @@ <h2>Excluded Hosts</h2>
<button id="addBtn">Add</button>
</div>
<div class="exc-list" id="list"></div>
<h2>Excluded Tab Groups</h2>
<div class="toggle-row">
<label>Exclude all tab groups</label>
<label class="switch">
<input type="checkbox" id="excludeAllGroups">
<span class="slider"></span>
</label>
</div>
<div class="exc-list group-list" id="groupList"></div>
<div class="closed-section" id="closedSection"></div>
</div>
</div>
Expand Down
97 changes: 97 additions & 0 deletions popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<div class="empty">${msg}</div>`;
return;
}

const renderRow = (title, color, isExcluded) => `<div class="item group-item">
<div class="group-info">
<span class="group-color" style="background:${color}"></span>
<span class="group-title">${esc(title)}</span>
</div>
<label class="switch">
<input type="checkbox" data-title="${escA(title)}"${isExcluded ? " checked" : ""}>
<span class="slider"></span>
</label>
</div>`;

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");

Expand Down