├── .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 | [](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 |
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 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/settings/de_DE/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Dustman Einstellungen
7 |
8 |
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------