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