Files
desktop/src/browser/base/zen-components/ZenTabUnloader.mjs
Slowlife 4ecccd67bd fix: change const to let
Signed-off-by: Slowlife <slowlife1165@gmail.com>
2025-04-06 16:12:14 +07:00

303 lines
8.8 KiB
JavaScript

{
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenTabUnloaderEnabled', 'zen.tab-unloader.enabled', false);
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenTabUnloaderTimeout', 'zen.tab-unloader.timeout-minutes', 20);
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenTabUnloaderExcludedUrls', 'zen.tab-unloader.excluded-urls', '');
const ZEN_TAB_UNLOADER_DEFAULT_EXCLUDED_URLS = [
'^about:',
'^chrome:',
'^devtools:',
'^file:',
'^resource:',
'^view-source:',
'^view-image:',
];
class ZenTabsObserver {
static ALL_EVENTS = [
'TabAttrModified',
'TabPinned',
'TabUnpinned',
'TabShow',
'TabHide',
'TabOpen',
'TabClose',
'TabSelect',
'TabMultiSelect',
];
#listeners = [];
constructor() {
this.#listenAllEvents();
}
#listenAllEvents() {
const eventListener = this.#eventListener.bind(this);
for (const event of ZenTabsObserver.ALL_EVENTS) {
window.addEventListener(event, eventListener);
}
window.addEventListener('unload', () => {
for (const event of ZenTabsObserver.ALL_EVENTS) {
window.removeEventListener(event, eventListener);
}
});
}
#eventListener(event) {
for (const listener of this.#listeners) {
listener(event.type, event);
}
}
addTabsListener(listener) {
this.#listeners.push(listener);
}
}
class ZenTabsIntervalUnloader {
static INTERVAL = 1000 * 60; // 1 minute
interval = null;
/** @type {ZenTabUnloader} */
unloader = null;
constructor(unloader) {
this.unloader = unloader;
this.interval = setInterval(this.intervalListener.bind(this), ZenTabsIntervalUnloader.INTERVAL);
}
intervalListener() {
if (!lazy.zenTabUnloaderEnabled) {
return;
}
const currentTimestamp = Date.now();
const tabs = ZenWorkspaces.allStoredTabs;
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
if (this.unloader.canUnloadTab(tab, currentTimestamp)) {
this.unloader.unload(tab);
}
}
}
}
class ZenTabUnloader extends ZenDOMOperatedFeature {
static ACTIVITY_MODIFIERS = ['muted', 'soundplaying', 'label', 'attention'];
#excludedUrls = [];
#compiledExcludedUrls = [];
#lastCheckedUrlTimestamp = 0;
constructor() {
super();
this.#excludedUrls = this.lazyExcludeUrls;
if (!lazy.zenTabUnloaderEnabled) {
return;
}
this.intervalUnloader = new ZenTabsIntervalUnloader(this);
}
init() {
if (!lazy.zenTabUnloaderEnabled) {
return;
}
this.insertIntoContextMenu();
this.observer = new ZenTabsObserver();
this.observer.addTabsListener(this.onTabEvent.bind(this));
}
onTabEvent(action, event) {
const tab = event.target;
switch (action) {
case 'TabPinned':
case 'TabUnpinned':
case 'TabShow':
case 'TabHide':
break;
case 'TabAttrModified':
this.handleTabAttrModified(tab, event);
break;
case 'TabOpen':
this.handleTabOpen(tab);
break;
case 'TabClose':
this.handleTabClose(tab);
break;
case 'TabSelect':
case 'TabMultiSelect':
this.updateTabActivity(tab);
break;
default:
console.warn('ZenTabUnloader: Unhandled tab event', action);
break;
}
}
onLocationChange(browser) {
const tab = browser.ownerGlobal.gBrowser.getTabForBrowser(browser);
this.updateTabActivity(tab);
}
handleTabClose(tab) {
tab.lastActivity = null;
}
handleTabOpen(tab) {
this.updateTabActivity(tab);
}
handleTabAttrModified(tab, event) {
for (const modifier of ZenTabUnloader.ACTIVITY_MODIFIERS) {
if (event.detail.changed.includes(modifier)) {
this.updateTabActivity(tab);
break;
}
}
}
updateTabActivity(tab) {
const currentTimestamp = Date.now();
tab.lastActivity = currentTimestamp;
}
insertIntoContextMenu() {
const element = window.MozXULElement.parseXULToFragment(`
<menuseparator/>
<menuitem id="context_zenUnloadTab"
data-lazy-l10n-id="tab-zen-unload"
command="cmd_zenUnloadTab"/>
<menu data-lazy-l10n-id="zen-tabs-unloader-tab-actions" id="context_zenTabActions">
<menupopup>
<menuitem id="context_zenPreventUnloadTab"
data-lazy-l10n-id="tab-zen-prevent-unload"
command="cmd_zenPreventUnloadTab"/>
<menuitem id="context_zenIgnoreUnloadTab"
data-lazy-l10n-id="tab-zen-ignore-unload"
command="cmd_zenIgnoreUnloadTab"/>
</menupopup>
</menu>
`);
document.getElementById('context_closeDuplicateTabs').parentNode.appendChild(element);
}
get lazyExcludeUrls() {
return [
...ZEN_TAB_UNLOADER_DEFAULT_EXCLUDED_URLS,
...lazy.zenTabUnloaderExcludedUrls.split(',').map((url) => url.trim()),
];
}
arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
const currentTimestamp = Date.now();
if (currentTimestamp - this.#lastCheckedUrlTimestamp < 5 * 1000) {
return true;
}
this.#lastCheckedUrlTimestamp = currentTimestamp;
// If you don't care about the order of the elements inside
// the array, you should sort both arrays here.
// Please note that calling sort on an array will modify that array.
// you might want to clone your array first.
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
get excludedUrls() {
// Check if excludedrls is the same as the pref value
const excludedUrls = this.lazyExcludeUrls;
if (!this.arraysEqual(this.#excludedUrls, excludedUrls) || !this.#compiledExcludedUrls.length) {
this.#excludedUrls = excludedUrls;
this.#compiledExcludedUrls = excludedUrls.map((url) => new RegExp(url));
}
return this.#compiledExcludedUrls;
}
unload(tab, skipPermitUnload = false) {
gBrowser.explicitUnloadTabs([tab], skipPermitUnload);
tab.removeAttribute('linkedpanel');
}
unloadTab() {
const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
this.explicitUnloadTabs(tabs);
}
explicitUnloadTabs(tabs, extraArgs = {}) {
for (let i = 0; i < tabs.length; i++) {
if (this.canUnloadTab(tabs[i], Date.now(), true, extraArgs)) {
this.unload(tabs[i], true);
}
}
}
preventUnloadTab() {
const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
tab.zenIgnoreUnload = true;
}
}
ignoreUnloadTab() {
const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
tab.zenIgnoreUnload = false;
}
}
canUnloadTab(tab, currentTimestamp, ignoreTimestamp = false, extraArgs = {}) {
if (
(tab.pinned && !ignoreTimestamp) ||
tab.selected ||
(tab.multiselected && !ignoreTimestamp) ||
(tab.hasAttribute('busy') && !ignoreTimestamp) ||
!tab.linkedPanel ||
tab.splitView ||
tab.group?.hasAttribute('split-view-group') ||
tab.attention ||
tab.hasAttribute('glance-id') ||
tab.linkedBrowser?.zenModeActive ||
(tab.pictureinpicture && !ignoreTimestamp) ||
(tab.soundPlaying && !ignoreTimestamp) ||
(tab.zenIgnoreUnload && !ignoreTimestamp) ||
(this.excludedUrls.some((url) => url.test(tab.linkedBrowser?.currentURI.spec)) &&
tab.linkedBrowser?.currentURI.spec !== 'about:blank')
) {
return false;
}
if (ignoreTimestamp) {
return this._tabPermitsUnload(tab, extraArgs);
}
const lastActivity = tab.lastActivity;
if (!lastActivity) {
return false;
}
const diff = currentTimestamp - lastActivity;
// Check if the tab has been inactive for more than the timeout
return diff > lazy.zenTabUnloaderTimeout * 60 * 1000 && this._tabPermitsUnload(tab, extraArgs);
}
_tabPermitsUnload(tab, extraArgs) {
return typeof extraArgs.permitUnload === 'undefined'
? tab.linkedBrowser?.permitUnload()?.permitUnload
: extraArgs.permitUnload;
}
}
window.gZenTabUnloader = new ZenTabUnloader();
}