diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch new file mode 100644 index 000000000..04aa6f0ca --- /dev/null +++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs +index 157c55ab24a418b56690d2e26320582909b919e4..14755f57dc450583e69eee94eb11f16980d5e5cb 100644 +--- a/browser/components/sessionstore/SessionFile.sys.mjs ++++ b/browser/components/sessionstore/SessionFile.sys.mjs +@@ -22,6 +22,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + RunState: "resource:///modules/sessionstore/RunState.sys.mjs", + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", + SessionWriter: "resource:///modules/sessionstore/SessionWriter.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const PREF_UPGRADE_BACKUP = "browser.sessionstore.upgradeBackup.latestBuildID"; +@@ -364,7 +365,7 @@ var SessionFileInternal = { + this._readOrigin = result.origin; + + result.noFilesFound = noFilesFound; +- ++ await lazy.ZenSessionStore.readFile(); + return result; + }, + diff --git a/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch new file mode 100644 index 000000000..b106193cf --- /dev/null +++ b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch @@ -0,0 +1,21 @@ +diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs +index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644 +--- a/browser/components/sessionstore/SessionStartup.sys.mjs ++++ b/browser/components/sessionstore/SessionStartup.sys.mjs +@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + StartupPerformance: + "resource:///modules/sessionstore/StartupPerformance.sys.mjs", + sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + const STATE_RUNNING_STR = "running"; +@@ -179,6 +180,8 @@ export var SessionStartup = { + this._initialState = parsed; + } + ++ lazy.ZenSessionStore.onFileRead(this._initialState); ++ + if (this._initialState == null) { + // No valid session found. + this._sessionType = this.NO_SESSION; diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 5cf576c24..1a0ebb806 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0eeba2381c 100644 +index eb62ff3e733e43fdaa299babddea3ba0125abb06..09567fe1be2af56429b60cbcbb36aa477fa68794 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -11,7 +11,15 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e ]; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -@@ -1911,6 +1913,8 @@ var SessionStoreInternal = { +@@ -195,6 +197,7 @@ ChromeUtils.defineESModuleGetters(lazy, { + TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", + TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", ++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs", + }); + + ChromeUtils.defineLazyGetter(lazy, "blankURI", () => { +@@ -1904,6 +1907,8 @@ var SessionStoreInternal = { case "TabPinned": case "TabUnpinned": case "SwapDocShells": @@ -20,7 +28,18 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e this.saveStateDelayed(win); break; case "TabGroupCreate": -@@ -2151,7 +2155,6 @@ var SessionStoreInternal = { +@@ -2041,6 +2046,10 @@ var SessionStoreInternal = { + // A regular window is not a private window, taskbar tab window, or popup window + let isRegularWindow = + !isPrivateWindow && !isTaskbarTab && aWindow.toolbar.visible; ++ if (!aInitialState && isRegularWindow) { ++ aInitialState = ZenSessionStore.getNewWindowData(this._windows); ++ this.restoreWindows(aWindow, aInitialState, {}); ++ } + + // perform additional initialization when the first window is loading + if (lazy.RunState.isStopped) { +@@ -2139,7 +2148,6 @@ var SessionStoreInternal = { if (closedWindowState) { let newWindowState; if ( @@ -28,7 +47,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e !lazy.SessionStartup.willRestore() ) { // We want to split the window up into pinned tabs and unpinned tabs. -@@ -2384,11 +2387,9 @@ var SessionStoreInternal = { +@@ -2372,11 +2380,9 @@ var SessionStoreInternal = { tabbrowser.selectedTab.label; } @@ -40,7 +59,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e // Store the window's close date to figure out when each individual tab // was closed. This timestamp should allow re-arranging data based on how -@@ -3373,7 +3374,7 @@ var SessionStoreInternal = { +@@ -3361,7 +3367,7 @@ var SessionStoreInternal = { if (!isPrivateWindow && tabState.isPrivate) { return; } @@ -49,7 +68,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e return; } -@@ -4089,6 +4090,12 @@ var SessionStoreInternal = { +@@ -4073,6 +4079,11 @@ var SessionStoreInternal = { Math.min(tabState.index, tabState.entries.length) ); tabState.pinned = false; @@ -62,7 +81,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e if (inBackground === false) { aWindow.gBrowser.selectedTab = newTab; -@@ -4525,6 +4532,7 @@ var SessionStoreInternal = { +@@ -4509,6 +4520,7 @@ var SessionStoreInternal = { // Append the tab if we're opening into a different window, tabIndex: aSource == aTargetWindow ? pos : Infinity, pinned: state.pinned, @@ -70,7 +89,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e userContextId: state.userContextId, skipLoad: true, preferredRemoteType, -@@ -5374,7 +5382,7 @@ var SessionStoreInternal = { +@@ -5358,7 +5370,7 @@ var SessionStoreInternal = { for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) { let tab = tabbrowser.tabs[i]; @@ -79,7 +98,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e removableTabs.push(tab); } } -@@ -5434,7 +5442,7 @@ var SessionStoreInternal = { +@@ -5418,7 +5430,7 @@ var SessionStoreInternal = { } let workspaceID = aWindow.getWorkspaceID(); @@ -88,7 +107,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e winData.workspaceID = workspaceID; } }, -@@ -5625,11 +5633,12 @@ var SessionStoreInternal = { +@@ -5609,11 +5621,12 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -102,7 +121,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5640,6 +5649,7 @@ var SessionStoreInternal = { +@@ -5624,6 +5637,7 @@ var SessionStoreInternal = { tabsData.push(tabData); } @@ -110,7 +129,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e // update tab group state for this window winData.groups = []; for (let tabGroup of aWindow.gBrowser.tabGroups) { -@@ -5652,7 +5662,7 @@ var SessionStoreInternal = { +@@ -5636,7 +5650,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -119,7 +138,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5764,8 +5774,8 @@ var SessionStoreInternal = { +@@ -5748,8 +5762,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -130,7 +149,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5808,6 +5818,8 @@ var SessionStoreInternal = { +@@ -5792,6 +5806,8 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); @@ -139,7 +158,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6371,6 +6383,25 @@ var SessionStoreInternal = { +@@ -6348,6 +6364,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -165,7 +184,7 @@ index 1cdbc0f41bf5b55dfbbd850cb618c6d870f7a261..4eac2fed26d779908107ef60f1c2bd0e if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7289,7 +7320,7 @@ var SessionStoreInternal = { +@@ -7263,7 +7298,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest index 74c0232b8..0f38828a1 100644 --- a/src/zen/ZenComponents.manifest +++ b/src/zen/ZenComponents.manifest @@ -13,3 +13,4 @@ category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} #include common/Components.manifest +#include sessionstore/SessionComponents.manifest diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index 2209e6713..16746cd26 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -157,6 +157,17 @@ class ZenFolder extends MozTabbrowserTabGroup { } } } + async unpackTabs() { + this.collapsed = false; + for (let tab of this.allItems.reverse()) { + tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; + if (tab.hasAttribute('zen-empty-tab')) { + gBrowser.removeTab(tab); + } else { + gBrowser.ungroupTab(tab); + } + } + } async delete() { for (const tab of this.allItemsRecursive) { @@ -169,6 +180,16 @@ class ZenFolder extends MozTabbrowserTabGroup { } await gBrowser.removeTabGroup(this, { isUserTriggered: true }); } + async delete() { + for (const tab of this.allItemsRecursive) { + if (tab.hasAttribute('zen-empty-tab')) { + // Manually remove the empty tabs as removeTabs() inside removeTabGroup + // does ignore them. + gBrowser.removeTab(tab); + } + } + await gBrowser.removeTabGroup(this, { isUserTriggered: true }); + } get allItemsRecursive() { const items = []; diff --git a/src/zen/moz.build b/src/zen/moz.build index 21915a69a..eb681597f 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -13,4 +13,5 @@ DIRS += [ "tests", "urlbar", "toolkit", + "sessionstore", ] diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest new file mode 100644 index 000000000..f8f08d1d7 --- /dev/null +++ b/src/zen/sessionstore/SessionComponents.manifest @@ -0,0 +1,6 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# Browser global components initializing before UI startup +category browser-before-ui-startup resource:///modules/zen/ZenSessionManager.sys.mjs ZenSessionStore.init diff --git a/src/zen/sessionstore/ZenSessionFile.sys.mjs b/src/zen/sessionstore/ZenSessionFile.sys.mjs index c50960ae1..526f7ab04 100644 --- a/src/zen/sessionstore/ZenSessionFile.sys.mjs +++ b/src/zen/sessionstore/ZenSessionFile.sys.mjs @@ -2,26 +2,39 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -const FILE_NAME = 'zen-sessions.jsonlz4'; +// Note that changing this hidden pref will make the previous session file +// unused, causing a new session file to be created on next write. +const SHOULD_COMPRESS_FILE = Services.prefs.getBoolPref('zen.session-store.compress-file', true); + +const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json'; export class nsZenSessionFile { - #path; - - #windows; - - constructor() { - this.#path = PathUtils.join(profileDir, FILE_NAME); - } + #path = PathUtils.join(PathUtils.profileDir, FILE_NAME); + #sidebar = []; async read() { try { - return await IOUtils.readJSON(this.#path, { compress: true }); - } catch (e) { - return {}; + const data = await IOUtils.readJSON(this.#path, { compress: SHOULD_COMPRESS_FILE }); + this.#sidebar = data.sidebar || []; + } catch { + // File doesn't exist yet, that's fine. } } - async write(data) { - await IOUtils.writeJSON(this.#path, data, { compress: true }); + get sidebar() { + return this.#sidebar; + } + + set sidebar(data) { + this.#sidebar = data; + } + + async #write(data) { + await IOUtils.writeJSON(this.#path, data, { compress: SHOULD_COMPRESS_FILE }); + } + + async store() { + const data = { sidebar: this.#sidebar }; + await this.#write(data); } } diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index 68aa829e4..5120319bc 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -12,23 +12,74 @@ import { const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - ZenSessionFile: 'resource://gre/modules/ZenSessionFile.sys.mjs', + nsZenSessionFile: 'resource:///modules/zen/ZenSessionFile.sys.mjs', PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs', - RunState: 'resource:///modules/sessionstore/RunState.sys.mjs', + BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs', + TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs', + SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs', }); +const TAB_CUSTOM_VALUES = new WeakMap(); +const LAZY_COLLECT_THRESHOLD = 5 * 60 * 1000; // 5 minutes +const OBSERVING = ['sessionstore-state-write-complete', 'browser-window-before-show']; + class nsZenSessionManager { #file; constructor() { - this.#file = null; + this.#file = new lazy.nsZenSessionFile(); } - get file() { - if (!this.#file) { - this.#file = lazy.ZenSessionFile; + // Called from SessionComponents.manifest on app-startup + init() { + this.#initObservers(); + } + + async readFile() { + await this.#file.read(); + } + + onFileRead(initialState) { + for (const winData of initialState.windows || []) { + this.restoreWindowData(winData); } - return this.#file; + } + + #initObservers() { + for (let topic of OBSERVING) { + Services.obs.addObserver(this, topic); + } + } + + get #sidebar() { + return this.#file.sidebar; + } + + set #sidebar(data) { + this.#file.sidebar = data; + } + + observe(aSubject, aTopic) { + switch (aTopic) { + case 'sessionstore-state-write-complete': { + this.#saveState(true); + break; + } + case 'browser-window-before-show': // catch new windows + this.#onBeforeBrowserWindowShown(aSubject); + break; + default: + break; + } + } + + /** Handles the browser-window-before-show observer notification. */ + #onBeforeBrowserWindowShown(aWindow) { + // TODO: Initialize new window + } + + get #topMostWindow() { + return lazy.BrowserWindowTracker.getTopWindow(); } /** @@ -38,12 +89,82 @@ class nsZenSessionManager { * Forces us to recollect data for all windows and will bypass and * update the corresponding caches. */ - saveState(forceUpdateAllWindows = false) { + async #saveState(forceUpdateAllWindows = false) { if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) { // Don't save (or even collect) anything in permanent private // browsing mode return Promise.resolve(); } + // Collect an initial snapshot of window data before we do the flush. + const window = this.#topMostWindow; + // We don't have any normal windows or no windows at all + if (!window) { + return; + } + this.#collectWindowData(this.#topMostWindow, forceUpdateAllWindows); + this.#file.store(); + } + + /** + * Collects session data for a given window. + * + * @param window + * The window to collect data for. + * @param forceUpdate + * Forces us to recollect data and will bypass and update the + * corresponding caches. + * @param zIndex + * The z-index of the window. + */ + #collectWindowData(window, forceUpdate = false, zIndex = 0) { + let sidebarData = this.#sidebar; + if (!sidebarData || forceUpdate) { + sidebarData = {}; + } + + // If it hasn't changed, don't update. + if ( + !forceUpdate && + sidebarData.lastCollected && + Date.now() - sidebarData.lastCollected < LAZY_COLLECT_THRESHOLD + ) { + return; + } + sidebarData.lastCollected = Date.now(); + this.#collectTabsData(window, sidebarData); + this.#sidebar = sidebarData; + } + + /** + * Collects session data for all tabs in a given window. + * + * @param aWindow + * The window to collect tab data for. + * @param winData + * The window data object to populate. + */ + #collectTabsData(aWindow, sidebarData) { + const winData = lazy.SessionStore.getWindowState(aWindow).windows[0]; + if (!winData) return; + sidebarData.tabs = winData.tabs; + sidebarData.folders = winData.folders; + sidebarData.splitViewData = winData.splitViewData; + sidebarData.groups = winData.groups; + } + + restoreWindowData(aWindowData) { + const sidebar = this.#file.sidebar; + if (!sidebar) { + return; + } + aWindowData.tabs = sidebar.tabs || []; + aWindowData.splitViewData = sidebar.splitViewData; + aWindowData.folders = sidebar.folders; + aWindowData.groups = sidebar.groups; + } + + getNewWindowData(aWindows) { + return { windows: [Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {})] }; } } diff --git a/src/zen/sessionstore/ZenSessionWindow.sys.mjs b/src/zen/sessionstore/ZenSessionWindow.sys.mjs deleted file mode 100644 index 460070234..000000000 --- a/src/zen/sessionstore/ZenSessionWindow.sys.mjs +++ /dev/null @@ -1,35 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -export class ZenSessionWindow { - #id; - #selectedWorkspace; - #selectedTab; - - constructor(id) { - this.#id = id; - this.#selectedWorkspace = null; - this.#selectedTab = null; - } - - get id() { - return this.#id; - } - - get selectedWorkspace() { - return this.#selectedWorkspace; - } - - set selectedWorkspace(workspace) { - this.#selectedWorkspace = workspace; - } - - get selectedTab() { - return this.#selectedTab; - } - - set selectedTab(tab) { - this.#selectedTab = tab; - } -} diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build new file mode 100644 index 000000000..af5a7dd3b --- /dev/null +++ b/src/zen/sessionstore/moz.build @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES.zen += [ + "ZenSessionFile.sys.mjs", + "ZenSessionManager.sys.mjs", +] diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 2fbfdc347..62350e00d 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -212,199 +212,10 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { this.hasInitializedPins = true; } - async #initializePinnedTabs(init = false) { - const pins = this._pinsCache; - if (!pins?.length || !init) { + async #initializePinnedTabs(init = false) { this.#finishedInitializingPins(); - return; } - const pinnedTabsByUUID = new Map(); - const pinsToCreate = new Set(pins.map((p) => p.uuid)); - - // First pass: identify existing tabs and remove those without pins - for (let tab of gZenWorkspaces.allStoredTabs) { - const pinId = tab.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - - if (pinsToCreate.has(pinId)) { - // This is a valid pinned tab that matches a pin - pinnedTabsByUUID.set(pinId, tab); - pinsToCreate.delete(pinId); - - if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) { - this._resetTabToStoredState(tab); - } - } else { - // This is a pinned tab that no longer has a corresponding pin - gBrowser.removeTab(tab); - } - } - - for (const group of gZenWorkspaces.allTabGroups) { - const pinId = group.getAttribute('zen-pin-id'); - if (!pinId) { - continue; - } - if (pinsToCreate.has(pinId)) { - // This is a valid pinned group that matches a pin - pinsToCreate.delete(pinId); - } - } - - // Second pass: For every existing tab, update its label - // and set 'zen-has-static-label' attribute if it's been edited - for (let pin of pins) { - const tab = pinnedTabsByUUID.get(pin.uuid); - if (!tab) { - continue; - } - - tab.removeAttribute('zen-has-static-label'); // So we can set it again - if (pin.title && pin.editedTitle) { - gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true }); - tab.setAttribute('zen-has-static-label', 'true'); - } - } - - const groups = new Map(); - const pendingTabsInsideGroups = {}; - - // Third pass: create new tabs for pins that don't have tabs - for (let pin of pins) { - try { - if (!pinsToCreate.has(pin.uuid)) { - continue; // Skip pins that already have tabs - } - - if (pin.isGroup) { - const tabs = []; - // If there's already existing tabs, let's use them - for (const [uuid, existingTab] of pinnedTabsByUUID) { - const pinObject = this._pinsCache.find((p) => p.uuid === uuid); - if (pinObject && pinObject.parentUuid === pin.uuid) { - tabs.push(existingTab); - } - } - // We still need to iterate through pending tabs since the database - // query doesn't guarantee the order of insertion - for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) { - if (parentUuid === pin.uuid) { - tabs.push(...folderTabs); - } - } - const group = gZenFolders.createFolder(tabs, { - label: pin.title, - collapsed: pin.isFolderCollapsed, - initialPinId: pin.uuid, - workspaceId: pin.workspaceUuid, - insertAfter: - groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null, - }); - gZenFolders.setFolderUserIcon(group, pin.folderIcon); - groups.set(pin.uuid, group); - continue; - } - - let params = { - skipAnimation: true, - allowInheritPrincipal: false, - skipBackgroundNotify: true, - userContextId: pin.containerTabId || 0, - createLazyBrowser: true, - skipLoad: true, - noInitialLabel: false, - }; - - // Create and initialize the tab - let newTab = gBrowser.addTrustedTab(pin.url, params); - newTab.setAttribute('zenDefaultUserContextId', true); - - // Set initial label/title - if (pin.title) { - gBrowser.setInitialTabTitle(newTab, pin.title); - } - - // Set the icon if we have it cached - if (pin.iconUrl) { - gBrowser.setIcon(newTab, pin.iconUrl); - } - - newTab.setAttribute('zen-pin-id', pin.uuid); - - if (pin.workspaceUuid) { - newTab.setAttribute('zen-workspace-id', pin.workspaceUuid); - } - - if (pin.isEssential) { - newTab.setAttribute('zen-essential', 'true'); - } - - if (pin.editedTitle) { - newTab.setAttribute('zen-has-static-label', 'true'); - } - - // Initialize browser state if needed - if (!newTab.linkedBrowser._remoteAutoRemoved) { - let state = { - entries: [ - { - url: pin.url, - title: pin.title, - triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL, - }, - ], - userContextId: pin.containerTabId || 0, - image: pin.iconUrl, - }; - - SessionStore.setTabState(newTab, state); - } - - this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`); - gBrowser.pinTab(newTab); - - if (pin.parentUuid) { - const parentGroup = groups.get(pin.parentUuid); - if (parentGroup) { - parentGroup.querySelector('.tab-group-container').appendChild(newTab); - } else { - if (pendingTabsInsideGroups[pin.parentUuid]) { - pendingTabsInsideGroups[pin.parentUuid].push(newTab); - } else { - pendingTabsInsideGroups[pin.parentUuid] = [newTab]; - } - } - } else { - if (!pin.isEssential) { - const container = gZenWorkspaces.workspaceElement( - pin.workspaceUuid - )?.pinnedTabsContainer; - if (container) { - container.insertBefore(newTab, container.lastChild); - } - } else { - gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab); - } - } - - gBrowser.tabContainer._invalidateCachedTabs(); - newTab.initialize(); - } catch (ex) { - console.error('Failed to initialize pinned tabs:', ex); - } - } - - setTimeout(() => { - this.#finishedInitializingPins(); - }, 0); - - gBrowser._updateTabBarForPinnedTabs(); - gZenUIManager.updateTabsToolbar(); - } - _onPinnedTabEvent(action, event) { if (!this.enabled) return; const tab = event.target; diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js index 197089441..06e07eaa4 100644 --- a/src/zen/zen.globals.js +++ b/src/zen/zen.globals.js @@ -26,7 +26,6 @@ export default [ 'ZEN_KEYSET_ID', 'gZenPinnedTabManager', - 'ZenPinnedTabsStorage', 'gZenEmojiPicker', 'gZenSessionStore',