├── icons ├── 128.png ├── 16.png └── 48.png ├── popup.css ├── popup.html ├── options.html ├── popup.js ├── manifest.json ├── README.md ├── LICENSE ├── options.js └── background.js /icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fthdgn/chrome_organize_windows_tabs/HEAD/icons/128.png -------------------------------------------------------------------------------- /icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fthdgn/chrome_organize_windows_tabs/HEAD/icons/16.png -------------------------------------------------------------------------------- /icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fthdgn/chrome_organize_windows_tabs/HEAD/icons/48.png -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | @media (prefers-color-scheme: dark) { 2 | body { 3 | background-color: rgb(32, 33, 36); 4 | color: rgb(232, 234, 237); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |Don't ask again.
19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |Ignore pinned tabs for merge and sort actions
13 |Ignore popup windows for merge and sort actions
14 |Ignore app windows for merge and sort actions
15 |Show default action popup
16 |Default action for extension icon:
17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const okButton = document.getElementById("okButton") 2 | const dontAskAgainInput = document.getElementById("dontAskAgainInput") 3 | const defaultActionSpan = document.getElementById("defaultActionSpan") 4 | 5 | getOptions().then(options => { 6 | let action = ALL_ACTIONS.find(action => action.id == options.defaultAction) 7 | defaultActionSpan.innerHTML = action.title.toLowerCase() 8 | }) 9 | 10 | 11 | okButton.onclick = async () => { 12 | await chrome.storage.sync.set({ showDefaultActionPopup: !dontAskAgainInput.checked }) 13 | 14 | let options = await getOptions() 15 | baseAction(options.defaultAction) 16 | 17 | if (dontAskAgainInput.checked) { 18 | chrome.action.setPopup({ popup: "" }) 19 | } 20 | 21 | chrome.extension.getViews().find(v => v.location.pathname === "/popup.html").close() 22 | } 23 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "service_worker": "background.js" 4 | }, 5 | "action": { 6 | "default_icon": { 7 | "128": "icons/128.png", 8 | "48": "icons/48.png", 9 | "16": "icons/16.png" 10 | }, 11 | "default_title": "Organize Windows and Tabs" 12 | }, 13 | "homepage_url": "https://github.com/fthdgn/chrome_organize_windows_tabs/", 14 | "description": "Organize Windows and Tabs", 15 | "icons": { 16 | "128": "icons/128.png", 17 | "48": "icons/48.png", 18 | "16": "icons/16.png" 19 | }, 20 | "manifest_version": 3, 21 | "name": "Organize Windows and Tabs", 22 | "offline_enabled": true, 23 | "options_page": "options.html", 24 | "permissions": [ 25 | "tabs", 26 | "contextMenus", 27 | "storage", 28 | "tabGroups" 29 | ], 30 | "version": "1.4.1" 31 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Organize Windows and Tabs Extension for Google Chrome 2 | 3 | This extension adds these actions: 4 | * Merge windows and sort tabs: 5 | Merge all windows on target window, and sorts all tabs according to tab url alphabetically. 6 | * Merge windows: 7 | Merge all windows on target window. 8 | * Sort tabs: 9 | Sort tabs on target windows according to tab url alphabetically. 10 | * Close tabs from this domain: 11 | Closes all tabs with same domain. 12 | * Move tabs from this domain to this window 13 | Moves tabs from same domain with the selected tab's domain to current window. 14 | * Group tabs from this domain 15 | Groups tabs from same domain with the selected tab's domain. 16 | 17 | Default extension icon action is "Merge windows and sort tabs". This can be changed from options page. You can select "do nothing" option to disable default action. 18 | 19 | By default the extension ignores pinned tabs, popup and app windows for merge and sort actions. These can be changed from options page. 20 | 21 | This extension does not cause tab groups to be ungrouped. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fatih Doğan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | const defaultActionInput = document.getElementById("defaultActionInput") 2 | const ignorePinnedTabsInput = document.getElementById("ignorePinnedTabsInput") 3 | const ignorePopupWindowsInput = document.getElementById("ignorePopupWindowsInput") 4 | const ignoreAppWindowsInput = document.getElementById("ignoreAppWindowsInput") 5 | const showDefaultActionPopupInput = document.getElementById("showDefaultActionPopupInput") 6 | 7 | ALL_ACTIONS.forEach(action => { 8 | var opt = document.createElement("option"); 9 | opt.value = action.id; 10 | opt.innerHTML = action.title; 11 | defaultActionInput.appendChild(opt); 12 | }) 13 | 14 | getOptions().then(options => { 15 | defaultActionInput.value = options.defaultAction 16 | ignorePinnedTabsInput.checked = options.ignorePinnedTabs 17 | ignorePopupWindowsInput.checked = options.ignorePopupWindows 18 | ignoreAppWindowsInput.checked = options.ignoreAppWindows 19 | showDefaultActionPopupInput.checked = options.showDefaultActionPopup 20 | }) 21 | 22 | defaultActionInput.addEventListener('change', () => { 23 | chrome.storage.sync.set({ defaultAction: defaultActionInput.value }) 24 | }); 25 | 26 | ignorePinnedTabsInput.addEventListener('change', () => { 27 | chrome.storage.sync.set({ ignorePinnedTabs: ignorePinnedTabsInput.checked }) 28 | }); 29 | 30 | ignorePopupWindowsInput.addEventListener('change', () => { 31 | chrome.storage.sync.set({ ignorePopupWindows: ignorePopupWindowsInput.checked }) 32 | }); 33 | 34 | ignoreAppWindowsInput.addEventListener('change', () => { 35 | chrome.storage.sync.set({ ignoreAppWindows: ignoreAppWindowsInput.checked }) 36 | }); 37 | 38 | showDefaultActionPopupInput.addEventListener('change', () => { 39 | chrome.storage.sync.set({ showDefaultActionPopup: showDefaultActionPopupInput.checked }) 40 | chrome.action.setPopup({ popup: showDefaultActionPopupInput.checked ? "popup.html" : "" }) 41 | }); -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | const DO_NOTHING = { 2 | title: "Do nothing", 3 | id: "do_nothing" 4 | } 5 | 6 | const MERGE_AND_SORT_ACTION = { 7 | title: "Merge windows and sort tabs", 8 | id: "merge_and_sort" 9 | } 10 | 11 | const MERGE_ACTION = { 12 | title: "Merge windows", 13 | id: "merge" 14 | } 15 | 16 | const SORT_ACTION = { 17 | title: "Sort tabs", 18 | id: "sort" 19 | } 20 | 21 | const CLOSE_TABS_FROM_THIS_DOMAIN_ACTION = { 22 | title: "Close tabs from this domain", 23 | id: "close_tabs_from_this_domain" 24 | } 25 | 26 | const MOVE_TABS_FROM_THIS_DOMAIN_ACTION = { 27 | title: "Move tabs from this domain to this window", 28 | id: "move_tabs_from_this_domain" 29 | } 30 | 31 | const GROUP_TABS_FROM_THIS_DOMAON_ACTION = { 32 | title: "Group tabs from this domain", 33 | id: "group_tabs_from_this_domain" 34 | } 35 | 36 | const ALL_ACTIONS = [DO_NOTHING, MERGE_AND_SORT_ACTION, MERGE_ACTION, SORT_ACTION, CLOSE_TABS_FROM_THIS_DOMAIN_ACTION, MOVE_TABS_FROM_THIS_DOMAIN_ACTION, GROUP_TABS_FROM_THIS_DOMAON_ACTION] 37 | 38 | async function getOptions() { 39 | return await chrome.storage.sync.get({ 40 | defaultAction: MERGE_AND_SORT_ACTION.id, 41 | ignorePinnedTabs: true, 42 | ignorePopupWindows: true, 43 | ignoreAppWindows: true, 44 | showDefaultActionPopup: true, 45 | }) 46 | } 47 | 48 | async function getTabsFromDomain(url) { 49 | let options = await getOptions() 50 | let tabs = await chrome.tabs.query({ 51 | groupId: chrome.tabGroups.TAB_GROUP_ID_NONE, 52 | url: url.protocol + "//" + url.host + "/*", 53 | pinned: options.ignorePinnedTabs ? false : undefined, 54 | }) 55 | return tabs 56 | } 57 | 58 | function baseAction(actionId) { 59 | if (actionId == DO_NOTHING.id) { 60 | //DO NOTHING 61 | } else if (actionId == MERGE_AND_SORT_ACTION.id) { 62 | mergeWindowsAndSortTabsAction() 63 | } else if (actionId == MERGE_ACTION.id) { 64 | mergeWindowsAction() 65 | } else if (actionId == SORT_ACTION.id) { 66 | sortTabsAction() 67 | } else if (actionId == CLOSE_TABS_FROM_THIS_DOMAIN_ACTION.id) { 68 | closeTabsFromCurrentDomainAction() 69 | } else if (actionId == MOVE_TABS_FROM_THIS_DOMAIN_ACTION.id) { 70 | moveTabsFromCurrentDomainAction() 71 | } else if (actionId == GROUP_TABS_FROM_THIS_DOMAON_ACTION.id) { 72 | groupTabsFromCurrentDomainAction() 73 | } 74 | } 75 | 76 | async function mergeWindowsAndSortTabsAction() { 77 | await mergeWindows() 78 | await sortTabs() 79 | } 80 | 81 | async function mergeWindowsAction() { 82 | await mergeWindows() 83 | } 84 | 85 | async function sortTabsAction() { 86 | await sortTabs() 87 | } 88 | 89 | async function closeTabsFromCurrentDomainAction() { 90 | let selectedTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0] 91 | let url = new URL(selectedTab.url) 92 | let tabs = await getTabsFromDomain(url) 93 | for (let tab of tabs) { 94 | await chrome.tabs.remove(tab.id) 95 | } 96 | } 97 | 98 | async function moveTabsFromCurrentDomainAction() { 99 | let selectedTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0] 100 | let url = new URL(selectedTab.url) 101 | let tabs = await getTabsFromDomain(url) 102 | for (let tab of tabs) { 103 | await chrome.tabs.move(tab.id, { windowId: selectedTab.windowId, index: -1 }) 104 | if (tab.pinned == true) { 105 | await chrome.tabs.update(tab.id, { pinned: true }) 106 | } 107 | } 108 | } 109 | 110 | async function groupTabsFromCurrentDomainAction() { 111 | let selectedTab = (await chrome.tabs.query({ active: true, currentWindow: true }))[0] 112 | let url = new URL(selectedTab.url) 113 | let tabs = await getTabsFromDomain(url) 114 | let tabIds = tabs.map(tab => tab.id) 115 | let existingGroups = await chrome.tabGroups.query({ title: url.host }) 116 | if (existingGroups.length > 0) { 117 | let existingGroupId = existingGroups[0].id 118 | await chrome.tabs.group({ groupId: existingGroupId, tabIds: tabIds }) 119 | } else { 120 | let groupId = await chrome.tabs.group({ tabIds: tabIds }) 121 | await chrome.tabGroups.update(groupId, { title: url.host }) 122 | } 123 | } 124 | 125 | async function mergeWindows() { 126 | let options = await getOptions() 127 | let currentWindow = await chrome.windows.getCurrent() 128 | let windows = await chrome.windows.getAll({ populate: true }) 129 | 130 | for (let window of windows) { 131 | if (window.id === currentWindow.id) { 132 | continue; 133 | } 134 | if (options.ignoreAppWindows && window.type === "app") { 135 | continue; 136 | } 137 | if (options.ignorePopupWindows && window.type === "popup") { 138 | continue; 139 | } 140 | 141 | for (let tab of window.tabs) { 142 | if (options.ignorePinnedTabs && tab.pinned) { 143 | continue 144 | } 145 | 146 | if (tab.groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE) { 147 | await chrome.tabGroups.move(tab.groupId, { windowId: currentWindow.id, index: -1 }) 148 | } else { 149 | await chrome.tabs.move(tab.id, { windowId: currentWindow.id, index: -1 }) 150 | } 151 | 152 | if (tab.pinned == true) { 153 | await chrome.tabs.update(tab.id, { pinned: true }) 154 | } 155 | } 156 | } 157 | } 158 | 159 | async function sortTabs() { 160 | let options = await getOptions() 161 | let currentWindow = await chrome.windows.getCurrent({ populate: true }) 162 | let tabs = currentWindow.tabs 163 | if (options.ignorePinnedTabs) { 164 | tabs = tabs.filter(tab => !tab.pinned) 165 | } 166 | 167 | tabs.sort(function (a, b) { 168 | if (a.groupId < b.groupId) { 169 | return -1 170 | } else if (a.groupId > b.groupId) { 171 | return 1 172 | } else { 173 | if (a.url < b.url) { 174 | return -1 175 | } else if (a.url > b.url) { 176 | return 1 177 | } else { 178 | return 0 179 | } 180 | } 181 | }) 182 | 183 | for (let tab of tabs) { 184 | if (tab.groupId !== chrome.tabGroups.TAB_GROUP_ID_NONE) { 185 | await chrome.tabGroups.move(tab.groupId, { windowId: currentWindow.id, index: -1 }) 186 | } else { 187 | await chrome.tabs.move(tab.id, { windowId: currentWindow.id, index: -1 }) 188 | } 189 | 190 | if (tab.pinned == true) { 191 | await chrome.tabs.update(tab.id, { pinned: true }) 192 | } 193 | } 194 | } 195 | 196 | chrome.contextMenus.onClicked.addListener(event => { 197 | baseAction(event.menuItemId) 198 | }); 199 | 200 | chrome.action.onClicked.addListener(async event => { 201 | let options = await getOptions() 202 | baseAction(options.defaultAction) 203 | }) 204 | 205 | chrome.runtime.onInstalled.addListener(() => { 206 | for (action of ALL_ACTIONS) { 207 | if (action === DO_NOTHING) { 208 | continue 209 | } 210 | chrome.contextMenus.create({ 211 | "title": action.title, 212 | "id": action.id, 213 | contexts: ["action"], 214 | }); 215 | } 216 | 217 | getOptions().then(options => { 218 | chrome.action.setPopup({ popup: options.showDefaultActionPopup ? "popup.html" : "" }) 219 | }) 220 | }) 221 | --------------------------------------------------------------------------------