Initial commit — basic tab grouping works
This commit is contained in:
commit
f0f3a7e4ea
6 changed files with 137 additions and 0 deletions
21
README.md
Normal file
21
README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# tab-tidy
|
||||||
|
|
||||||
|
A small browser extension that groups and sorts your open tabs by domain.
|
||||||
|
Built for Firefox. Side project — scratching my own itch.
|
||||||
|
|
||||||
|
## Install (dev)
|
||||||
|
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
# Load dist/ as a temporary extension in about:debugging
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Groups tabs by domain
|
||||||
|
- Sorts groups alphabetically or by last-used
|
||||||
|
- One-click close for all tabs in a group
|
||||||
|
- Keyboard shortcut: Alt+T to open the popup
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Works for me™. Not on the extension store yet.
|
||||||
21
manifest.json
Normal file
21
manifest.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Tab Tidy",
|
||||||
|
"version": "0.3.1",
|
||||||
|
"description": "Group and sort your tabs by domain",
|
||||||
|
"permissions": ["tabs"],
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"_execute_action": {
|
||||||
|
"suggested_key": {
|
||||||
|
"default": "Alt+T"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
package.json
Normal file
16
package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "tab-tidy",
|
||||||
|
"version": "0.3.1",
|
||||||
|
"description": "Group and sort browser tabs by domain",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config webpack.config.js",
|
||||||
|
"dev": "webpack --config webpack.config.js --watch",
|
||||||
|
"lint": "eslint src/"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.91.0",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"copy-webpack-plugin": "^12.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
popup.html
Normal file
18
popup.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tab Tidy</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui; width: 320px; padding: 8px; }
|
||||||
|
h2 { font-size: 0.85rem; margin: 8px 0 4px; color: #555; display: flex; justify-content: space-between; }
|
||||||
|
button { font-size: 0.7rem; cursor: pointer; border: none; background: #eee; padding: 2px 6px; border-radius: 3px; }
|
||||||
|
.tab-item { font-size: 0.8rem; padding: 3px 6px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.tab-item:hover { background: #f0f0f0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="groups"></div>
|
||||||
|
<script src="src/popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
12
src/background.js
Normal file
12
src/background.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Background service worker
|
||||||
|
// Listens for tab updates and triggers a re-group
|
||||||
|
|
||||||
|
chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
|
||||||
|
if (changeInfo.status === 'complete') {
|
||||||
|
chrome.runtime.sendMessage({ type: 'TABS_CHANGED' }).catch(() => {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.tabs.onRemoved.addListener(() => {
|
||||||
|
chrome.runtime.sendMessage({ type: 'TABS_CHANGED' }).catch(() => {});
|
||||||
|
});
|
||||||
49
src/popup.js
Normal file
49
src/popup.js
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
const groupByDomain = (tabs) => {
|
||||||
|
return tabs.reduce((groups, tab) => {
|
||||||
|
try {
|
||||||
|
const domain = new URL(tab.url).hostname;
|
||||||
|
if (!groups[domain]) groups[domain] = [];
|
||||||
|
groups[domain].push(tab);
|
||||||
|
} catch (_) {}
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = async () => {
|
||||||
|
const tabs = await chrome.tabs.query({ currentWindow: true });
|
||||||
|
const groups = groupByDomain(tabs);
|
||||||
|
const sorted = Object.entries(groups).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
|
||||||
|
const container = document.getElementById('groups');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
for (const [domain, domainTabs] of sorted) {
|
||||||
|
const section = document.createElement('section');
|
||||||
|
const header = document.createElement('h2');
|
||||||
|
header.textContent = `${domain} (${domainTabs.length})`;
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.textContent = 'close all';
|
||||||
|
closeBtn.onclick = () => {
|
||||||
|
chrome.tabs.remove(domainTabs.map(t => t.id)).then(render);
|
||||||
|
};
|
||||||
|
|
||||||
|
header.appendChild(closeBtn);
|
||||||
|
section.appendChild(header);
|
||||||
|
|
||||||
|
for (const tab of domainTabs) {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'tab-item';
|
||||||
|
item.textContent = tab.title || tab.url;
|
||||||
|
item.title = tab.url;
|
||||||
|
item.onclick = () => chrome.tabs.update(tab.id, { active: true });
|
||||||
|
section.appendChild(item);
|
||||||
|
}
|
||||||
|
container.appendChild(section);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', render);
|
||||||
|
chrome.runtime.onMessage.addListener((msg) => {
|
||||||
|
if (msg.type === 'TABS_CHANGED') render();
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue