Skip to content

Commit

Permalink
Release v1.2.0 (#12)
Browse files Browse the repository at this point in the history
* Show the number of tabs in each container

* Refactor tabCount

* Add tabs counter to context menu

* Remove parentheses from tab count html

* Suggest results

* Switch to suggested tab

* Add multi-window search

* Add styling for tab counter

* Increase version

* Update readme

* Add separator before new workpsace option in context menu

* Tidy up the code

* Refactor workspace to know its window id

* Tokenize search query

* Fix linter warings
  • Loading branch information
fonse authored Oct 7, 2017
1 parent 32da639 commit 259705c
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 55 deletions.
7 changes: 1 addition & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This extension aims to be an alternative to [Tab Groups](https://addons.mozilla.
- If you have multiple windows open, each one has its own set of workspaces.
- Send a specific tab to another workspace from the right-click menu.
- Press Ctrl+E to open the list of workspaces, then press 1-9 to switch between using keyboard shortcuts.
- Search through your tabs in the address bar. Type "ws [text]" to begin searching. Choose a result to switch to that tab.

## Notice
There is no way to "hide" a tab with the WebExtensions API, so when switching between workspaces the tabs are actually closed and reopened.
Expand All @@ -20,12 +21,6 @@ This has the side effect of not maintaining the tabs' history, as well as stoppi

If you know any better way to hide the tabs, please let me know.

# Future Improvements
I'm planning to add the following features in the near future:

- UI improvements
- Suggest tabs from other workspaces in the awesomebar

## Acknowledgements
This extension was inspired by [Multi-Account Containers](https://addons.mozilla.org/en-US/firefox/addon/multi-account-containers/), which also served as a reference for some of the functionality.

Expand Down
78 changes: 72 additions & 6 deletions background/backgroundLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const BackgroundLogic = {
BackgroundLogic.updateContextMenu();
}
});

browser.tabs.onCreated.addListener(BackgroundLogic.updateContextMenu);
browser.tabs.onRemoved.addListener(BackgroundLogic.updateContextMenu);

browser.omnibox.onInputChanged.addListener(BackgroundLogic.handleAwesomebarSearch);
browser.omnibox.onInputEntered.addListener(BackgroundLogic.handleAwesomebarSelection);
},

async getWorkspacesForCurrentWindow(){
Expand Down Expand Up @@ -67,9 +73,9 @@ const BackgroundLogic = {

// Since we're gonna be closing all open tabs, we need to show the new ones first.
// However, we first need to prepare the old one, so it can tell which tabs were the original ones and which were opened by the new workspace.
await oldWorkspace.prepareToHide(windowId);
await newWorkspace.show(windowId);
await oldWorkspace.hide(windowId);
await oldWorkspace.prepareToHide();
await newWorkspace.show();
await oldWorkspace.hide();
},

async renameWorkspace(workspaceId, workspaceName) {
Expand All @@ -91,7 +97,7 @@ const BackgroundLogic = {
await BackgroundLogic.switchToWorkspace(nextWorkspaceId);
}

await workspaceToDelete.delete(windowId);
await workspaceToDelete.delete();

// Re-render context menu
BackgroundLogic.updateContextMenu();
Expand Down Expand Up @@ -141,16 +147,22 @@ const BackgroundLogic = {
});

const workspaces = await BackgroundLogic.getWorkspacesForCurrentWindow();
workspaces.forEach(workspace => {
const workspaceObjects = await Promise.all(workspaces.map(workspace => workspace.toObject()));
workspaceObjects.forEach(workspace => {
browser.menus.create({
title: workspace.name,
title: `${workspace.name} (${workspace.tabCount} tabs)`,
parentId: menuId,
id: workspace.id,
enabled: !workspace.active,
onclick: BackgroundLogic.handleContextMenuClick
});
});

browser.menus.create({
parentId: menuId,
type: "separator"
});

browser.menus.create({
title: "Create new workspace",
parentId: menuId,
Expand All @@ -174,6 +186,60 @@ const BackgroundLogic = {
}

await BackgroundLogic.moveTabToWorkspace(tab, destinationWorkspace);
},

async handleAwesomebarSearch(text, suggest){
suggest(await BackgroundLogic.searchTabs(text));
},

async handleAwesomebarSelection(content, disposition){
let windowId, workspaceId, tabIndex;
[windowId, workspaceId, tabIndex] = content.split(':');

await browser.windows.update(parseInt(windowId), {focused: true});

const workspace = await Workspace.find(workspaceId);
await BackgroundLogic.switchToWorkspace(workspace.id);

const matchedTabs = await browser.tabs.query({
windowId: parseInt(windowId),
index: parseInt(tabIndex)
});

if (matchedTabs.length > 0){
await browser.tabs.update(matchedTabs[0].id, {active: true});
}
},

async searchTabs(text){
if (text.length < 3){
return [];
}

const windows = await browser.windows.getAll({windowTypes: ['normal']})
const promises = windows.map(windowInfo => BackgroundLogic.searchTabsInWindow(text, windowInfo.id));

return Util.flattenArray(await Promise.all(promises));
},

async searchTabsInWindow(text, windowId){
const suggestions = [];

const workspaces = await BackgroundLogic.getWorkspacesForWindow(windowId);
const promises = workspaces.map(async workspace => {
const tabs = await workspace.getTabs();
tabs.forEach(tab => {
if (Util.matchesQuery(tab.title, text)) {
suggestions.push({
content: `${windowId}:${workspace.id}:${tab.index}`,
description: tab.title
});
}
});
});

await Promise.all(promises);
return suggestions;
}

};
Expand Down
7 changes: 6 additions & 1 deletion background/messageHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ browser.runtime.onMessage.addListener(async m => {

switch (m.method) {
case "getWorkspacesForCurrentWindow":
response = BackgroundLogic.getWorkspacesForCurrentWindow();
const workspaces = await BackgroundLogic.getWorkspacesForCurrentWindow();
response = await Promise.all(workspaces.map(workspace => workspace.toObject()));
break;

case "switchToWorkspace":
await BackgroundLogic.switchToWorkspace(m.workspaceId);
break;

case "createNewWorkspaceAndSwitch":
await BackgroundLogic.createNewWorkspaceAndSwitch();
break;

case "renameWorkspace":
await BackgroundLogic.renameWorkspace(m.workspaceId, m.workspaceName);
break;

case "deleteWorkspace":
await BackgroundLogic.deleteWorkspace(m.workspaceId);
break;
Expand Down
13 changes: 13 additions & 0 deletions background/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ const Util = {
)
},

matchesQuery(subject, query){
return query.split(" ")
.filter(token => token)
.every(token => subject.toLowerCase().indexOf(token.toLowerCase()) != -1);
},

flattenArray(arr) {
return arr.reduce(
(acc, cur) => acc.concat(cur),
[]
);
},

// From https://gist.github.com/nmsdvid/8807205
debounce(func, wait, immediate) {
var timeout;
Expand Down
86 changes: 62 additions & 24 deletions background/workspace.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
class Workspace {
constructor(id, name, active, hiddenTabs) {
constructor(id, state) {
this.id = id;
this.name = name;
this.active = active;
this.hiddenTabs = hiddenTabs;

if (state){
this.name = state.name;
this.active = state.active;
this.hiddenTabs = state.hiddenTabs;
this.windowId = state.windowId;
}
}

static async create(windowId, name, active) {
const workspace = new Workspace(Util.generateUUID(), name, active || false, []);
const workspace = new Workspace(Util.generateUUID(), {
name: name,
active: active || false,
hiddenTabs: [],
windowId: windowId
});

await workspace.storeState();
await WorkspaceStorage.registerWorkspaceToWindow(windowId, workspace.id);

Expand All @@ -26,30 +36,50 @@ class Workspace {
await this.storeState();
}

async getTabs() {
if (this.active){
// Not counting pinned tabs. Should we?
const tabs = await browser.tabs.query({
pinned: false,
windowId: this.windowId
});

return tabs;
} else {
return this.hiddenTabs;
}
}

async toObject() {
const obj = Object.assign({}, this);
obj.tabCount = (await this.getTabs()).length;

return obj;
}

// Store hidden tabs in storage
async prepareToHide(windowId) {
async prepareToHide() {
const tabs = await browser.tabs.query({
windowId: windowId,
windowId: this.windowId,
pinned: false
});

tabs.forEach(tab => {
const tabObject = Object.assign({}, tab);
this.hiddenTabs.push(tabObject);
this.hiddenTabs.push(tab);
})
}

// Then remove the tabs from the window
async hide(windowId) {
async hide() {
this.active = false;
await this.storeState();

const tabIds = this.hiddenTabs.map(tab => tab.id);
browser.tabs.remove(tabIds);
await browser.tabs.remove(tabIds);
}

async show(windowId) {
const tabs = this.hiddenTabs.filter(tabObject => Util.isPermissibleURL(tabObject.url));
async show() {
const tabs = this.hiddenTabs.filter(tab => Util.isPermissibleURL(tab.url));

if (tabs.length == 0){
tabs.push({
Expand All @@ -58,12 +88,12 @@ class Workspace {
});
}

const promises = tabs.map(tabObject => {
const promises = tabs.map(tab => {
return browser.tabs.create({
url: tabObject.url,
active: tabObject.active,
cookieStoreId: tabObject.cookieStoreId,
windowId: windowId
url: tab.url,
active: tab.active,
cookieStoreId: tab.cookieStoreId,
windowId: this.windowId
});
});

Expand All @@ -75,14 +105,13 @@ class Workspace {
}

// Then remove the tabs from the window
async delete(windowId) {
async delete() {
await WorkspaceStorage.deleteWorkspaceState(this.id);
await WorkspaceStorage.unregisterWorkspaceToWindow(windowId, this.id);
await WorkspaceStorage.unregisterWorkspaceToWindow(this.windowId, this.id);
}

async attachTab(tab) {
const tabObject = Object.assign({}, tab);
this.hiddenTabs.push(tabObject);
this.hiddenTabs.push(tab);

await this.storeState();
}
Expand All @@ -96,7 +125,7 @@ class Workspace {
await browser.tabs.remove(tab.id);
} else {
// Otherwise, forget it from hiddenTabs
const index = this.hiddenTabs.findIndex(tabObject => tabObject.id == tab.id);
const index = this.hiddenTabs.findIndex(hiddenTab => hiddenTab.id == tab.id);
if (index > -1){
this.hiddenTabs.splice(index, 1);
await this.storeState();
Expand All @@ -110,13 +139,22 @@ class Workspace {
this.name = state.name;
this.active = state.active;
this.hiddenTabs = state.hiddenTabs;
this.windowId = state.windowId;

// For backwards compatibility
if (!this.windowId){
console.log("Backwards compatibility for",this.name);
this.windowId = (await browser.windows.getCurrent()).id;
await this.storeState();
}
}

async storeState() {
await WorkspaceStorage.storeWorkspaceState(this.id, {
name: this.name,
active: this.active,
hiddenTabs: this.hiddenTabs
hiddenTabs: this.hiddenTabs,
windowId: this.windowId
});
}
}
16 changes: 2 additions & 14 deletions background/workspaceStorage.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
const WorkspaceStorage = {

// Deprecated
async fetchWorkspace(workspaceId) {
const key = `workspaces@${workspaceId}`;
const results = await browser.storage.local.get(key);

if (results[key]){
const state = results[key];
return new Workspace(workspaceId, state.name, state.active, state.hiddenTabs);
} else {
return null;
}
},

async fetchWorkspaceState(workspaceId) {
const key = `workspaces@${workspaceId}`;
const results = await browser.storage.local.get(key);
Expand Down Expand Up @@ -51,7 +38,8 @@ const WorkspaceStorage = {

const workspaceIds = results[key] || [];
const promises = workspaceIds.map(async workspaceId => {
return await WorkspaceStorage.fetchWorkspace(workspaceId);
const state = await WorkspaceStorage.fetchWorkspaceState(workspaceId);
return new Workspace(workspaceId, state);
});

return await Promise.all(promises);
Expand Down
6 changes: 5 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Tab Workspaces",
"version": "1.1.1",
"version": "1.2.0",

"description": "Organize your tabs into workspaces. Switch between workspaces to change which tabs are displayed at the moment.",
"icons": {
Expand Down Expand Up @@ -42,5 +42,9 @@
"background/workspaceStorage.js",
"background/messageHandler.js"
]
},

"omnibox": {
"keyword": "ws"
}
}
Loading

0 comments on commit 259705c

Please sign in to comment.