├── .gitignore ├── .web-extension-id ├── icon.png ├── panel ├── default-favicon.png ├── index.js ├── index.html ├── zh_CN │ └── panel.html ├── en_US │ └── panel.html ├── de_DE │ └── panel.html ├── panel.css └── panel.js ├── package.json ├── settings ├── index.js ├── index.html ├── settings.css ├── zh_CN │ └── settings.html ├── settings.js ├── en_US │ └── settings.html └── de_DE │ └── settings.html ├── README.md ├── manifest.json ├── _locales ├── zh_CN │ └── messages.json ├── en_US │ └── messages.json └── de_DE │ └── messages.json ├── background.js ├── lib ├── browseraction.js ├── state.js └── autoclose.js └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.web-extension-id: -------------------------------------------------------------------------------- 1 | jid1-i37bkuPx3kABMw@jetpack 2 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qzb/dustman/HEAD/icon.png -------------------------------------------------------------------------------- /panel/default-favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qzb/dustman/HEAD/panel/default-favicon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": [ 3 | "jsdoc", 4 | "standard", 5 | "web-ext" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /panel/index.js: -------------------------------------------------------------------------------- 1 | window.location.href = browser.runtime.getURL('panel/' + browser.i18n.getMessage('locale') + '/panel.html') 2 | -------------------------------------------------------------------------------- /settings/index.js: -------------------------------------------------------------------------------- 1 | window.location.href = browser.runtime.getURL('settings/' + browser.i18n.getMessage('locale') + '/settings.html') 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dustman 2 | 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 4 | 5 | Firefox alternative for Tab Wrangler. Automatically closes old unused tabs. 6 | -------------------------------------------------------------------------------- /panel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /settings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /settings/settings.css: -------------------------------------------------------------------------------- 1 | .form-group { 2 | margin-top: 2em; 3 | margin-bottom: 2em; 4 | } 5 | 6 | label { 7 | float: left; 8 | width: 300px; 9 | } 10 | 11 | .form-group p { 12 | display: block; 13 | color: rgb(70, 70, 70); 14 | margin-left: 2em; 15 | margin-top: 0.5em; 16 | } 17 | 18 | input[type='number'] { 19 | width: 100px; 20 | } 21 | -------------------------------------------------------------------------------- /panel/zh_CN/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 暂停自动关闭 16 | 继续自动关闭 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /panel/en_US/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | Pause auto-closing 16 | Resume auto-closing 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /panel/de_DE/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | Automatisches Schließen pausieren 16 | Automatisches Schließen fortsetzen 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "version": "2.1.0", 5 | 6 | "description": "__MSG_extensionDescription__", 7 | 8 | "icons": { 9 | "64": "icon.png" 10 | }, 11 | 12 | "background": { 13 | "scripts": [ 14 | "lib/state.js", 15 | "lib/autoclose.js", 16 | "lib/browseraction.js", 17 | "background.js" 18 | ] 19 | }, 20 | 21 | "options_ui": { 22 | "page": "settings/index.html" 23 | }, 24 | 25 | "browser_action": { 26 | "browser_style": true, 27 | "default_popup": "panel/index.html", 28 | "default_title": "__MSG_buttonTooltip__", 29 | "default_icon": { 30 | "64": "icon.png" 31 | } 32 | }, 33 | 34 | "permissions": ["storage", "tabs"], 35 | 36 | "default_locale": "en_US" 37 | } 38 | -------------------------------------------------------------------------------- /_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": { 3 | "message": "zh_CN", 4 | "description": "A variant of the IETF language tag of this locale, e.g. 'en_US' for American English (not 'en-US', i.e. with an underscore _ instead of a minus -)." 5 | }, 6 | "extensionName": { 7 | "message": "Dustman", 8 | "description": "The name of the extension." 9 | }, 10 | "extensionDescription": { 11 | "message": "自动关闭未使用的标签页", 12 | "description": "The description of the extension, e.g. on the addon page." 13 | }, 14 | "buttonTooltip": { 15 | "message": "Dustman", 16 | "description": "The tooltip of the dustman button when auto-closing is not paused." 17 | }, 18 | "buttonTooltipPaused": { 19 | "message": "Dustman (已暂停)", 20 | "description": "The tooltip of the dustman button when auto-closing is paused." 21 | }, 22 | "buttonBadgePaused": { 23 | "message": "🚫", 24 | "description": "The symbol over the dustman button when auto-closing is paused." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": { 3 | "message": "en_US", 4 | "description": "A variant of the IETF language tag of this locale, e.g. 'en_US' for American English (not 'en-US', i.e. with an underscore _ instead of a minus -)." 5 | }, 6 | "extensionName": { 7 | "message": "Dustman", 8 | "description": "The name of the extension." 9 | }, 10 | "extensionDescription": { 11 | "message": "Auto-closes unused tabs", 12 | "description": "The description of the extension, e.g. on the addon page." 13 | }, 14 | "buttonTooltip": { 15 | "message": "Dustman", 16 | "description": "The tooltip of the dustman button when auto-closing is not paused." 17 | }, 18 | "buttonTooltipPaused": { 19 | "message": "Dustman (paused)", 20 | "description": "The tooltip of the dustman button when auto-closing is paused." 21 | }, 22 | "buttonBadgePaused": { 23 | "message": "🚫", 24 | "description": "The symbol over the dustman button when auto-closing is paused." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /_locales/de_DE/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "locale": { 3 | "message": "de_DE", 4 | "description": "A variant of the IETF language tag of this locale, e.g. 'en_US' for American English (not 'en-US', i.e. with an underscore _ instead of a minus -)." 5 | }, 6 | "extensionName": { 7 | "message": "Dustman", 8 | "description": "The name of the extension." 9 | }, 10 | "extensionDescription": { 11 | "message": "Schließt automatisch unbenutzte Tabs.", 12 | "description": "The description of the extension, e.g. on the addon page." 13 | }, 14 | "buttonTooltip": { 15 | "message": "Dustman", 16 | "description": "The tooltip of the dustman button when auto-closing is not paused." 17 | }, 18 | "buttonTooltipPaused": { 19 | "message": "Dustman (pausiert)", 20 | "description": "The tooltip of the dustman button when auto-closing is paused." 21 | }, 22 | "buttonBadgePaused": { 23 | "message": "🚫", 24 | "description": "The symbol over the dustman button when auto-closing is paused." 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | loadState().then(state => { 4 | // make the state available via the window of the background page 5 | window.state = state 6 | 7 | browser.browserAction.onClicked.addListener(() => { 8 | state.paused = !state.paused 9 | updateBrowserAction(state) 10 | autoclose(state) 11 | }) 12 | updateBrowserAction(state) 13 | 14 | browser.tabs.onCreated.addListener(tab => { 15 | autoclose(state) 16 | }) 17 | browser.tabs.onAttached.addListener(() => autoclose(state)) 18 | browser.tabs.onUpdated.addListener(changeInfo => { 19 | if (changeInfo.pinned === false || changeInfo.audible === false) { 20 | autoclose(state) 21 | } 22 | }) 23 | 24 | browser.storage.onChanged.addListener(changes => { 25 | if ('settings' in changes) { 26 | state.settings = changes.settings.newValue 27 | 28 | updateBrowserAction(state) 29 | 30 | state.history = state.history.slice(0, state.settings.maxHistorySize) 31 | persistHistory(state) 32 | 33 | autoclose(state) 34 | } 35 | }) 36 | 37 | return autoclose(state) 38 | }) 39 | -------------------------------------------------------------------------------- /lib/browseraction.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function updateBrowserAction (state) { 4 | if ( 5 | browser.browserAction.setBadgeText == null || 6 | browser.browserAction.setBadgeBackgroundColor == null || 7 | browser.browserAction.setTitle == null || 8 | browser.browserAction.setPopup == null 9 | ) { 10 | return 11 | } 12 | 13 | if (state.paused === true) { 14 | browser.browserAction.setBadgeText({text: browser.i18n.getMessage('buttonBadgePaused')}) //🚫'}) 15 | browser.browserAction.setBadgeBackgroundColor({color: [0, 0, 0, 0]}) 16 | browser.browserAction.setTitle({title: browser.i18n.getMessage('buttonTooltipPaused')}) 17 | } else { 18 | browser.browserAction.setBadgeText({text: ''}) 19 | browser.browserAction.setTitle({title: browser.i18n.getMessage('buttonTooltip')}) 20 | } 21 | 22 | if (state.settings.maxHistorySize > 0) { 23 | browser.browserAction.setPopup( 24 | {popup: browser.extension.getURL(browser.runtime.getManifest().browser_action.default_popup)} 25 | ) 26 | } else { 27 | browser.browserAction.setPopup({popup: ''}) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Martin Bidlingmaier 2 | 3 | Copyright for portions of Dustman are held by Józef Sokołowski as part of the 4 | project with the same name. All other copyright for Dustman are held by Martin 5 | Bidlingmaier, 2017. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 21 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 23 | OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /panel/panel.css: -------------------------------------------------------------------------------- 1 | .recently-closed { 2 | overflow-x: hidden; 3 | overflow-y: auto; 4 | padding: 4px; 5 | } 6 | 7 | .recently-closed:empty::before { 8 | color: #999; 9 | content: attr(placeholder); 10 | display: block; 11 | font-style: italic; 12 | margin-top: 8px; 13 | margin-bottom: 8px; 14 | text-align: center; 15 | width: 100%; 16 | } 17 | 18 | .recently-closed a { 19 | border: 1px solid transparent; 20 | box-sizing: border-box; 21 | color: #444; 22 | cursor: pointer; 23 | display: block; 24 | height: 26px; 25 | line-height: 16px; 26 | overflow: hidden; 27 | padding: 4px; 28 | text-decoration: none; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | display: block; 32 | width: 100%; 33 | } 34 | 35 | .recently-closed a:hover { 36 | background-color: #ddd; 37 | border-radius: 3px; 38 | border: 1px solid #ccc; 39 | } 40 | 41 | .recently-closed a img { 42 | float: left; 43 | height: 16px; 44 | margin-right: 8px; 45 | width: 16px; 46 | } 47 | 48 | .button { 49 | display: block; 50 | cursor: pointer; 51 | background-color: #e9e9e9; 52 | border: none; 53 | border-top: 1px solid #ccc; 54 | bottom: 0; 55 | text-align: center; 56 | padding-top: 8px; 57 | padding-bottom: 8px; 58 | white-space: nowrap; 59 | width: 100%; 60 | min-width: 250px; 61 | outline: none; 62 | } 63 | 64 | .button:hover { 65 | background-color: #ddd; 66 | } 67 | 68 | .button:active { 69 | background-color: #ccc; 70 | } 71 | 72 | .button.paused .pause { 73 | display: none; 74 | } 75 | 76 | .button:not(.paused) .resume { 77 | display: none; 78 | } 79 | -------------------------------------------------------------------------------- /settings/zh_CN/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dustman 设置 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 17 |

18 | Dustman 仅关闭在后台存留多于这一时长的标签页。 19 |

20 |
21 | 22 |
23 | 26 | 27 |

28 | Dustman 仅处理多于这一数量的标签页的窗口。 29 | 固定的标签页不计数,不会自动关闭。 30 | 非固定正在播放声音的标签页会计数,但是不会自动关闭。 31 |

32 |
33 | 34 |
35 | 38 | 39 |

40 | Dustman 可以保存已关闭的标签页。 41 | 通过工具栏按钮访问保存的标签页列表。 42 | 设置为 0 即禁用保存列表。 43 |

44 |
45 | 46 |
47 | 50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /settings/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function initializeMinInactiveMinutes (settings) { 4 | const input = document.getElementById('min-inactive-minutes') 5 | input.value = settings.minInactiveMilliseconds / (1000 * 60) 6 | input.addEventListener('change', () => { 7 | const s = parseFloat(input.value) 8 | if (isNaN(s) || s < 0) { 9 | input.setAttribute('aria-invalid', true) 10 | } else { 11 | input.setAttribute('aria-invalid', false) 12 | settings.minInactiveMilliseconds = s * 1000 * 60 13 | persistSettings(settings) 14 | } 15 | }) 16 | } 17 | 18 | function initializeMinTabsCount (settings) { 19 | const input = document.getElementById('min-tabs-count') 20 | input.value = settings.minTabsCount 21 | input.addEventListener('change', () => { 22 | const c = parseInt(input.value) 23 | if (isNaN(c) || c <= 0) { 24 | input.setAttribute('aria-invalid', true) 25 | } else { 26 | input.setAttribute('aria-invalid', false) 27 | settings.minTabsCount = c 28 | persistSettings(settings) 29 | } 30 | }) 31 | } 32 | 33 | function initializeMaxHistorySize (settings) { 34 | const input = document.getElementById('max-history-size') 35 | input.value = settings.maxHistorySize 36 | input.addEventListener('change', () => { 37 | const c = parseInt(input.value) 38 | if (isNaN(c) || c < 0) { 39 | input.setAttribute('aria-invalid', true) 40 | } else { 41 | input.setAttribute('aria-invalid', false) 42 | settings.maxHistorySize = c 43 | persistSettings(settings) 44 | } 45 | }) 46 | } 47 | 48 | function initializeClearHistoryOnExit (settings) { 49 | const input = document.getElementById('clear-history-on-exit') 50 | input.checked = settings.clearHistoryOnExit 51 | input.addEventListener('change', () => { 52 | settings.clearHistoryOnExit = input.checked 53 | persistSettings(settings) 54 | }) 55 | } 56 | 57 | function initializeSettingsUi (settings) { 58 | initializeMinInactiveMinutes(settings) 59 | initializeMinTabsCount(settings) 60 | initializeMaxHistorySize(settings) 61 | initializeClearHistoryOnExit(settings) 62 | } 63 | 64 | loadState().then(state => initializeSettingsUi(state.settings)) 65 | -------------------------------------------------------------------------------- /panel/panel.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // get the background page 4 | const bp = browser.extension.getBackgroundPage() 5 | 6 | /** 7 | * Create a link node from a closed page info 8 | * @param {ClosedPageInfo} 9 | * @return {HTMLElement} 10 | */ 11 | function makeLink ({url, title, favIconUrl}) { 12 | const link = document.createElement('a') 13 | link.setAttribute('href', url) 14 | 15 | const img = document.createElement('img') 16 | img.setAttribute('height', '1em') 17 | img.setAttribute('width', '1em') 18 | img.setAttribute('src', favIconUrl || 'default-favicon.png') 19 | link.appendChild(img) 20 | 21 | const titleNode = document.createTextNode(title) 22 | link.appendChild(titleNode) 23 | 24 | return link 25 | } 26 | /** 27 | * Populate the list of closed pages. 28 | * @param {State} state 29 | */ 30 | function populateLinkList (state) { 31 | const linkList = document.getElementById('link-list') 32 | 33 | while (linkList.hasChildNodes()) { 34 | linkList.removeChild(linkList.lastChild) 35 | } 36 | 37 | for (let closedPage of state.history) { 38 | const link = makeLink(closedPage) 39 | link.addEventListener('click', event => { 40 | const i = state.history.indexOf(closedPage) 41 | state.history.splice(i, 1) 42 | if (state.settings.clearHistoryOnExit === false) { 43 | persistHistory(state) 44 | } 45 | linkList.removeChild(link) 46 | }) 47 | linkList.appendChild(link) 48 | } 49 | } 50 | 51 | /** 52 | * Set up actions for the pause toggle button. 53 | * @param {State} state 54 | */ 55 | function initializePauseButton (state) { 56 | const button = document.getElementById('pause-toggle-button') 57 | if (state.paused) { 58 | button.classList.add('paused') 59 | } else { 60 | button.classList.remove('paused') 61 | } 62 | 63 | button.addEventListener('click', () => { 64 | state.paused = !state.paused 65 | bp.updateBrowserAction(state) 66 | bp.autoclose(state).then(() => { 67 | populateLinkList(state) 68 | if (state.paused) { 69 | button.classList.add('paused') 70 | } else { 71 | button.classList.remove('paused') 72 | } 73 | }) 74 | }) 75 | } 76 | 77 | populateLinkList(bp.state) 78 | initializePauseButton(bp.state) 79 | -------------------------------------------------------------------------------- /settings/en_US/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dustman Settings 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 17 |

18 | Dustman only closes tabs which have been in the background for this long. 19 |

20 |
21 | 22 |
23 | 26 | 27 |

28 | Dustman closes tabs only in windows that have more than this many tabs. 29 | Pinned tabs are not counted towards this limit, and never auto-closed. 30 | While unpinned Tabs that play sound are counted towards this limit, they are also never auto-closed. 31 |

32 |
33 | 34 |
35 | 38 | 39 |

40 | Dustman can save a number of tabs it has closed. 41 | The list can be accessed via the garbage bin button in the toolbar. 42 | Set to 0 to disable saving tabs altogether. 43 |

44 |
45 | 46 |
47 | 50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /settings/de_DE/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dustman Einstellungen 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 17 |

18 | Dustman schließt Tabs nur, wenn sie mindestens so lange im Hintergrund waren. 19 |

20 |
21 | 22 |
23 | 26 | 27 |

28 | Dustman schließt Tabs nur in Fenstern, in denen mehr als so viele Tabs geöffnet sind. 29 | Angeheftete Tabs werden nicht mitgezählt, und auch nicht automatisch geschlossen. 30 | Nicht angeheftete Tabs, die Ton abspielen, werden zwar mitgezählt, aber ebenfalls nicht automatisch geschlossen. 31 |

32 |
33 | 34 |
35 | 38 | 39 |

40 | Dustman kann sich automatisch geschlossene Tabs merken. 41 | Die Liste ist über den Abfalleimer in der Symbolleiste erreichbar. 42 | Auf 0 setzen deaktiviert das Speichern von geschlossenen Tabs. 43 |

44 |
45 | 46 |
47 | 50 | 51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /lib/state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @typedef Settings 5 | * @type {object} 6 | * @property {number} maxInactiveMilliseconds 7 | * @property {integer} minTabsCount 8 | * @property {integer} maxHistorySize 9 | * @property {boolean} clearHistoryOnExit 10 | */ 11 | 12 | const defaultSettings = { 13 | minInactiveMilliseconds: 20 * 60 * 1000, 14 | minTabsCount: 5, 15 | maxHistorySize: 1000, 16 | clearHistoryOnExit: true 17 | } 18 | 19 | /** 20 | * @typedef ClosedPageInfo 21 | * @type {object} 22 | * @property {String} url 23 | * @property {String} title 24 | * @property {String} favIconUrl 25 | */ 26 | 27 | /** 28 | * @typedef State 29 | * @type {object} 30 | * @property {Settings} settings 31 | * @property {integer} autocloseTimeoutId 32 | * @property {boolean} paused 33 | * @property {Array.} history 34 | */ 35 | 36 | /** 37 | * Load the persistent state from storage if possible, and otherwise set 38 | * everything to defaults 39 | * @return {Promise.} 40 | */ 41 | function loadState () { 42 | return browser.storage.local.get().then(state => state, err => { 43 | console.log(err) 44 | return {settings: defaultSettings} 45 | }).then(state => { 46 | if (state.settings == null) { 47 | state.settings = defaultSettings 48 | } 49 | if (state.history == null) { 50 | state.history = [] 51 | } 52 | state.autocloseTimeoutId = 0 53 | state.paused = false 54 | 55 | // handle settings from previous versions of dustman 56 | const settings = state.settings 57 | 58 | if (settings.saveClosedPages != null) { 59 | if (settings.saveClosedPages=== true) { 60 | settings.maxHistorySize = defaultSettings.maxHistorySize 61 | } else { 62 | settings.maxHistorySize = 0 63 | } 64 | delete settings.saveClosedPages 65 | } 66 | 67 | if (settings.clearHistoryOnExit == null) { 68 | settings.clearHistoryOnExit = defaultSettings.clearHistoryOnExit 69 | } 70 | return state 71 | }) 72 | } 73 | 74 | /** 75 | * Save settings to storage. 76 | * @param {Settings} settings 77 | * @return {Promise.<()>} 78 | */ 79 | function persistSettings (settings) { 80 | return browser.storage.local.set({settings: settings}) 81 | } 82 | 83 | /** 84 | * Save history to disk, or delete history on disk (depending on settings). 85 | * @param {State} state 86 | * @return {Promise.<()>} 87 | */ 88 | function persistHistory (state) { 89 | if (state.settings.clearHistoryOnExit === false) { 90 | return browser.storage.local.set({history: state.history}) 91 | } else { 92 | return browser.storage.local.remove('history') 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /lib/autoclose.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @typedef TabInfo 5 | * @type {object} 6 | * @property {integer} tabId 7 | * @property {integer} windowId 8 | * @property {number} inactiveMilliseconds - milliseconds since last activity in the tab 9 | * @property {boolean} pinned - whether the tab is pinned 10 | * @property {boolean} seen - whether the tab has been seen by the user at least once 11 | */ 12 | 13 | /** 14 | * @typedef CloseInfo 15 | * @type {object} 16 | * @property {Array.} tabIds 17 | * @property {number} millisecondsUntilNextCheck - potentially Infinity if no tab can be closed at some point in the future 18 | */ 19 | 20 | /** 21 | * Get a list of tabs that should be closed, as well as the time when another 22 | * tab can be closed (if any). 23 | * @param {number} now - milliseconds since the epoch 24 | * @param {Settings} settings 25 | * @param {Array.} tabs 26 | * @return {CloseInfo} 27 | */ 28 | function tabsToClose (now, settings, tabs) { 29 | const windowIds = Array.from(new Set(tabs.map(tab => tab.windowId))) 30 | const tabsByWindow = windowIds.map(windowId => tabs.filter(tab => tab.windowId === windowId)) 31 | 32 | const perWindowResults = tabsByWindow.map(tabs => { 33 | const unpinnedTabs = tabs.filter(tab => !tab.pinned) 34 | const numTabsToClose = unpinnedTabs.length - settings.minTabsCount 35 | if (numTabsToClose <= 0) { 36 | return {tabsToClose: [], nextCheck: Infinity} 37 | } 38 | 39 | // closeable tabs (now or in the future), sorted from longest to shortest inactivity 40 | const closeableTabs = 41 | unpinnedTabs.filter(tab => tab.audible === false && tab.lastAccessed < Infinity) 42 | .sort((t1, t2) => t1.lastAccessed > t2.lastAccessed) 43 | 44 | const nowCloseableTabs = 45 | closeableTabs.filter(tab => tab.lastAccessed + settings.minInactiveMilliseconds < now) 46 | const onlyLaterCloseableTabs = 47 | closeableTabs.filter(tab => tab.lastAccessed + settings.minInactiveMilliseconds >= now) 48 | 49 | const tabsToClose = nowCloseableTabs.slice(0, numTabsToClose) 50 | var nextCheck 51 | if (tabsToClose.length === numTabsToClose || onlyLaterCloseableTabs.length === 0) { 52 | nextCheck = Infinity 53 | } else { 54 | nextCheck = onlyLaterCloseableTabs[0].lastAccessed + settings.minInactiveMilliseconds 55 | } 56 | 57 | return {tabsToClose, nextCheck} 58 | }) 59 | 60 | const tabsToClose = 61 | Array.prototype.concat.apply([], perWindowResults.map(res => res.tabsToClose)) 62 | 63 | const nextCheck = 64 | Math.min.apply(null, perWindowResults.map(res => res.nextCheck)) 65 | 66 | return {tabsToClose, nextCheck} 67 | } 68 | 69 | /** 70 | * Whether a tab can be saved to the panel. 71 | * @param {browser.tabs.Tab} tab 72 | * @return {boolean} 73 | */ 74 | function saveableTab (tab) { 75 | if (tab.title == null || tab.url == null) { 76 | return false 77 | } 78 | 79 | const protocol = new URL(tab.url).protocol 80 | if (['chrome:', 'javascript:', 'data:', 'file:', 'about:'].indexOf(protocol) >= 0) { 81 | return false 82 | } 83 | 84 | if (tab.incognito === true) { 85 | return false 86 | } 87 | 88 | return true 89 | } 90 | 91 | /** 92 | * Auto-close old tabs. Also clears the timeout and sets a new one for the next 93 | * auto-close if appropriate. 94 | * @param {State} state 95 | * @return {Promise.<()>} 96 | */ 97 | function autoclose (state) { 98 | clearTimeout(state.autocloseTimeoutId) 99 | 100 | if (state.paused) { 101 | return Promise.resolve() 102 | } 103 | 104 | return browser.tabs.query({windowType: 'normal'}).then(tabs => { 105 | const now = new Date().getTime() 106 | const {tabsToClose: tabsToClose_, nextCheck} = tabsToClose(now, state.settings, tabs) 107 | 108 | if (nextCheck < Infinity) { 109 | // check again at nextCheck + some tolerance 110 | state.autocloseTimeoutId = setTimeout(() => autoclose(state), nextCheck - now + 1000) 111 | } 112 | 113 | return browser.tabs.remove(tabsToClose_.map(tab => tab.id)).then(() => { 114 | if (state.settings.maxHistorySize > 0) { 115 | const history = 116 | tabsToClose_ 117 | .filter(saveableTab) 118 | .map(tab => ({title: tab.title, url: tab.url, favIconUrl: tab.favIconUrl})) 119 | state.history = 120 | history.concat(state.history).slice(0, state.settings.maxHistorySize) 121 | if (state.settings.clearHistoryOnExit === false) { 122 | return persistHistory(state) 123 | } 124 | } 125 | }) 126 | }) 127 | } 128 | --------------------------------------------------------------------------------