mirror of
				https://github.com/zen-browser/desktop.git
				synced 2025-11-04 01:34:35 +00:00 
			
		
		
		
	Compare commits
	
		
			18 Commits
		
	
	
		
			1.17.4b
			...
			window-syn
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					bf1b0dcd48 | ||
| 
						 | 
					76acc8b0e4 | ||
| 
						 | 
					1b83b77cad | ||
| 
						 | 
					d005b97042 | ||
| 
						 | 
					79ff574978 | ||
| 
						 | 
					af20a65fa1 | ||
| 
						 | 
					4a7f8fc9c0 | ||
| 
						 | 
					a738a829de | ||
| 
						 | 
					240a031e38 | ||
| 
						 | 
					9bc7b9ce4e | ||
| 
						 | 
					86006c8891 | ||
| 
						 | 
					a55b1c7495 | ||
| 
						 | 
					6e6337a95b | ||
| 
						 | 
					6b12153c8a | ||
| 
						 | 
					f6922ef2ba | ||
| 
						 | 
					91f5d58fbc | ||
| 
						 | 
					7a4cdaa45c | ||
| 
						 | 
					81e854a89f | 
@@ -48,7 +48,6 @@
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenFolders.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenMods.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenCompactMode.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenPinnedTabsStorage.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspacesStorage.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenPinnedTabManager.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenGradientGenerator.mjs"></script>
 | 
			
		||||
@@ -58,3 +57,4 @@
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenDownloadAnimation.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenEmojiPicker.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspaceCreation.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWindowSyncing.mjs"></script>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
        content/browser/ZenPreloadedScripts.js                                  (../../zen/common/ZenPreloadedScripts.js)
 | 
			
		||||
        content/browser/zen-sets.js                                             (../../zen/common/zen-sets.js)
 | 
			
		||||
        content/browser/ZenUIManager.mjs                                        (../../zen/common/ZenUIManager.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenActorsManager.mjs                     (../../zen/common/ZenActorsManager.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenCommonUtils.mjs                       (../../zen/common/ZenCommonUtils.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenSessionStore.mjs                      (../../zen/common/ZenSessionStore.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenEmojisData.min.mjs                    (../../zen/common/emojis/ZenEmojisData.min.mjs)
 | 
			
		||||
@@ -42,6 +41,7 @@
 | 
			
		||||
        content/browser/zen-components/ZenWorkspaceIcons.mjs                    (../../zen/workspaces/ZenWorkspaceIcons.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenWorkspace.mjs                         (../../zen/workspaces/ZenWorkspace.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenWorkspaces.mjs                        (../../zen/workspaces/ZenWorkspaces.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenWindowSyncing.mjs                     (../../zen/workspaces/ZenWindowSyncing.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenWorkspaceCreation.mjs                 (../../zen/workspaces/ZenWorkspaceCreation.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenWorkspacesStorage.mjs                 (../../zen/workspaces/ZenWorkspacesStorage.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenWorkspacesSync.mjs                    (../../zen/workspaces/ZenWorkspacesSync.mjs)
 | 
			
		||||
@@ -51,7 +51,6 @@
 | 
			
		||||
 | 
			
		||||
        content/browser/zen-components/ZenKeyboardShortcuts.mjs                 (../../zen/kbs/ZenKeyboardShortcuts.mjs)
 | 
			
		||||
 | 
			
		||||
        content/browser/zen-components/ZenPinnedTabsStorage.mjs                 (../../zen/tabs/ZenPinnedTabsStorage.mjs)
 | 
			
		||||
        content/browser/zen-components/ZenPinnedTabManager.mjs                  (../../zen/tabs/ZenPinnedTabManager.mjs)
 | 
			
		||||
*       content/browser/zen-styles/zen-tabs.css                                 (../../zen/tabs/zen-tabs.css)
 | 
			
		||||
        content/browser/zen-styles/zen-tabs/vertical-tabs.css                   (../../zen/tabs/zen-tabs/vertical-tabs.css)
 | 
			
		||||
 
 | 
			
		||||
@@ -13,5 +13,4 @@
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspace.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspaces.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenWorkspacesSync.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenActorsManager.mjs"></script>
 | 
			
		||||
<script type="text/javascript" src="chrome://browser/content/zen-components/ZenSessionStore.mjs"></script>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
   },
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
 | 
			
		||||
index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d89fb95494 100644
 | 
			
		||||
index eb62ff3e733e43fdaa299babddea3ba0125abb06..6a73ee56c067cba2347a552b4152cd03cda47b6f 100644
 | 
			
		||||
--- a/browser/components/sessionstore/SessionStore.sys.mjs
 | 
			
		||||
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
 | 
			
		||||
@@ -126,6 +126,8 @@ const TAB_EVENTS = [
 | 
			
		||||
@@ -11,7 +11,15 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
 ];
 | 
			
		||||
 
 | 
			
		||||
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 | 
			
		||||
@@ -1904,6 +1906,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,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
         this.saveStateDelayed(win);
 | 
			
		||||
         break;
 | 
			
		||||
       case "TabGroupCreate":
 | 
			
		||||
@@ -2139,7 +2143,6 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -2139,7 +2144,6 @@ var SessionStoreInternal = {
 | 
			
		||||
       if (closedWindowState) {
 | 
			
		||||
         let newWindowState;
 | 
			
		||||
         if (
 | 
			
		||||
@@ -28,7 +36,18 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
           !lazy.SessionStartup.willRestore()
 | 
			
		||||
         ) {
 | 
			
		||||
           // We want to split the window up into pinned tabs and unpinned tabs.
 | 
			
		||||
@@ -2372,11 +2375,9 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -2203,6 +2207,10 @@ var SessionStoreInternal = {
 | 
			
		||||
       });
 | 
			
		||||
       this._shouldRestoreLastSession = false;
 | 
			
		||||
     }
 | 
			
		||||
+    else if (!aInitialState && isRegularWindow) {
 | 
			
		||||
+      aInitialState = lazy.ZenSessionStore.getNewWindowData(this._windows);
 | 
			
		||||
+      this.restoreWindows(aWindow, aInitialState, {});
 | 
			
		||||
+    }
 | 
			
		||||
 
 | 
			
		||||
     if (this._restoreLastWindow && aWindow.toolbar.visible) {
 | 
			
		||||
       // always reset (if not a popup window)
 | 
			
		||||
@@ -2372,11 +2380,9 @@ var SessionStoreInternal = {
 | 
			
		||||
           tabbrowser.selectedTab.label;
 | 
			
		||||
       }
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +59,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
 
 | 
			
		||||
       // 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
 | 
			
		||||
@@ -3361,7 +3362,7 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -3361,7 +3367,7 @@ var SessionStoreInternal = {
 | 
			
		||||
     if (!isPrivateWindow && tabState.isPrivate) {
 | 
			
		||||
       return;
 | 
			
		||||
     }
 | 
			
		||||
@@ -49,7 +68,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
       return;
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
@@ -4073,6 +4074,11 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -4073,6 +4079,11 @@ var SessionStoreInternal = {
 | 
			
		||||
         Math.min(tabState.index, tabState.entries.length)
 | 
			
		||||
       );
 | 
			
		||||
       tabState.pinned = false;
 | 
			
		||||
@@ -61,7 +80,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
 
 | 
			
		||||
       if (inBackground === false) {
 | 
			
		||||
         aWindow.gBrowser.selectedTab = newTab;
 | 
			
		||||
@@ -4509,6 +4515,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,
 | 
			
		||||
@@ -69,7 +88,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
       userContextId: state.userContextId,
 | 
			
		||||
       skipLoad: true,
 | 
			
		||||
       preferredRemoteType,
 | 
			
		||||
@@ -5358,7 +5365,7 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -5358,7 +5370,7 @@ var SessionStoreInternal = {
 | 
			
		||||
 
 | 
			
		||||
     for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
 | 
			
		||||
       let tab = tabbrowser.tabs[i];
 | 
			
		||||
@@ -78,7 +97,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
         removableTabs.push(tab);
 | 
			
		||||
       }
 | 
			
		||||
     }
 | 
			
		||||
@@ -5418,7 +5425,7 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -5418,7 +5430,7 @@ var SessionStoreInternal = {
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
     let workspaceID = aWindow.getWorkspaceID();
 | 
			
		||||
@@ -87,7 +106,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
       winData.workspaceID = workspaceID;
 | 
			
		||||
     }
 | 
			
		||||
   },
 | 
			
		||||
@@ -5609,11 +5616,12 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -5609,11 +5621,12 @@ var SessionStoreInternal = {
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
     let tabbrowser = aWindow.gBrowser;
 | 
			
		||||
@@ -101,7 +120,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
     // update the internal state data for this window
 | 
			
		||||
     for (let tab of tabs) {
 | 
			
		||||
       if (tab == aWindow.FirefoxViewHandler.tab) {
 | 
			
		||||
@@ -5624,6 +5632,7 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -5624,6 +5637,7 @@ var SessionStoreInternal = {
 | 
			
		||||
       tabsData.push(tabData);
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
@@ -109,7 +128,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
     // update tab group state for this window
 | 
			
		||||
     winData.groups = [];
 | 
			
		||||
     for (let tabGroup of aWindow.gBrowser.tabGroups) {
 | 
			
		||||
@@ -5636,7 +5645,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) {
 | 
			
		||||
@@ -118,7 +137,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
       winData.title = tabbrowser.tabs[0].label;
 | 
			
		||||
     }
 | 
			
		||||
     winData.selected = selectedIndex;
 | 
			
		||||
@@ -5748,8 +5757,8 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -5748,8 +5762,8 @@ var SessionStoreInternal = {
 | 
			
		||||
     // selectTab represents.
 | 
			
		||||
     let selectTab = 0;
 | 
			
		||||
     if (overwriteTabs) {
 | 
			
		||||
@@ -129,7 +148,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
       selectTab = Math.min(selectTab, winData.tabs.length);
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
@@ -5792,6 +5801,8 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -5792,6 +5806,8 @@ var SessionStoreInternal = {
 | 
			
		||||
         winData.tabs,
 | 
			
		||||
         winData.groups ?? []
 | 
			
		||||
       );
 | 
			
		||||
@@ -138,7 +157,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
       this._log.debug(
 | 
			
		||||
         `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
 | 
			
		||||
       );
 | 
			
		||||
@@ -6348,6 +6359,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.
 | 
			
		||||
@@ -152,8 +171,8 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
+    if (tabData.zenHasStaticLabel) {
 | 
			
		||||
+      tab.setAttribute("zen-has-static-label", "true");
 | 
			
		||||
+    }
 | 
			
		||||
+    if (tabData.zenPinnedId) {
 | 
			
		||||
+      tab.setAttribute("zen-pin-id", tabData.zenPinnedId);
 | 
			
		||||
+    if (tabData.zenSyncId) {
 | 
			
		||||
+      tab.setAttribute("zen-sync-id", tabData.zenSyncId);
 | 
			
		||||
+    }
 | 
			
		||||
+    if (tabData.zenDefaultUserContextId) {
 | 
			
		||||
+      tab.setAttribute("zenDefaultUserContextId", true);
 | 
			
		||||
@@ -164,7 +183,7 @@ index eb62ff3e733e43fdaa299babddea3ba0125abb06..8f20ba50b06f5b75d7de08eb4d1b27d8
 | 
			
		||||
 
 | 
			
		||||
     if (tabData.pinned) {
 | 
			
		||||
       tabbrowser.pinTab(tab);
 | 
			
		||||
@@ -7263,7 +7293,7 @@ var SessionStoreInternal = {
 | 
			
		||||
@@ -7263,7 +7298,7 @@ var SessionStoreInternal = {
 | 
			
		||||
 
 | 
			
		||||
       let groupsToSave = new Map();
 | 
			
		||||
       for (let tIndex = 0; tIndex < window.tabs.length; ) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
 | 
			
		||||
index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644
 | 
			
		||||
index 82721356d191055bec0d4b0ca49e481221988801..d1323fe17c995611ebdfe2869b0ccd2d45bcfa11 100644
 | 
			
		||||
--- a/browser/components/sessionstore/TabState.sys.mjs
 | 
			
		||||
+++ b/browser/components/sessionstore/TabState.sys.mjs
 | 
			
		||||
@@ -85,7 +85,22 @@ class _TabState {
 | 
			
		||||
@@ -7,7 +7,7 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
+    tabData.zenWorkspace = tab.getAttribute("zen-workspace-id");
 | 
			
		||||
+    tabData.zenPinnedId = tab.getAttribute("zen-pin-id");
 | 
			
		||||
+    tabData.zenSyncId = tab.getAttribute("zen-sync-id");
 | 
			
		||||
+    tabData.zenEssential = tab.getAttribute("zen-essential");
 | 
			
		||||
+    tabData.pinned = tabData.pinned || tabData.zenEssential;
 | 
			
		||||
+    tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId");
 | 
			
		||||
 
 | 
			
		||||
@@ -121,14 +121,6 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362ef
 | 
			
		||||
     on_click(event) {
 | 
			
		||||
       if (event.button != 0) {
 | 
			
		||||
         return;
 | 
			
		||||
@@ -570,6 +592,7 @@
 | 
			
		||||
             )
 | 
			
		||||
           );
 | 
			
		||||
         } else {
 | 
			
		||||
+          gZenPinnedTabManager._removePinnedAttributes(this, true);
 | 
			
		||||
           gBrowser.removeTab(this, {
 | 
			
		||||
             animate: true,
 | 
			
		||||
             triggeringEvent: event,
 | 
			
		||||
@@ -582,6 +605,14 @@
 | 
			
		||||
         // (see tabbrowser-tabs 'click' handler).
 | 
			
		||||
         gBrowser.tabContainer._blockDblClick = true;
 | 
			
		||||
 
 | 
			
		||||
@@ -477,16 +477,6 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394
 | 
			
		||||
 
 | 
			
		||||
       TabBarVisibility.update();
 | 
			
		||||
     }
 | 
			
		||||
@@ -4553,6 +4680,9 @@
 | 
			
		||||
         return;
 | 
			
		||||
       }
 | 
			
		||||
 
 | 
			
		||||
+      for (let tab of selectedTabs) {
 | 
			
		||||
+        gZenPinnedTabManager._removePinnedAttributes(tab, true);
 | 
			
		||||
+      }
 | 
			
		||||
       this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource });
 | 
			
		||||
     }
 | 
			
		||||
 
 | 
			
		||||
@@ -4814,6 +4944,7 @@
 | 
			
		||||
         telemetrySource,
 | 
			
		||||
       } = {}
 | 
			
		||||
@@ -838,11 +828,3 @@ index c099e8646b9341a3ff55bf394037c8fc2769969b..0d524a0519bbbdf304a594d1fb56c394
 | 
			
		||||
     // Build Ask Chat items
 | 
			
		||||
     TabContextMenu.GenAI.buildTabMenu(
 | 
			
		||||
       document.getElementById("context_askChat"),
 | 
			
		||||
@@ -9763,6 +9944,7 @@ var TabContextMenu = {
 | 
			
		||||
         )
 | 
			
		||||
       );
 | 
			
		||||
     } else {
 | 
			
		||||
+      gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true);
 | 
			
		||||
       gBrowser.removeTab(this.contextTab, {
 | 
			
		||||
         animate: true,
 | 
			
		||||
         ...gBrowser.TabMetrics.userTriggeredContext(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								src/zen/ZenComponents.manifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/zen/ZenComponents.manifest
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# 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/.
 | 
			
		||||
 | 
			
		||||
# nsBrowserGlue.js
 | 
			
		||||
 | 
			
		||||
# This component must restrict its registration for the app-startup category
 | 
			
		||||
# to the specific list of apps that use it so it doesn't get loaded in xpcshell.
 | 
			
		||||
# Thus we restrict it to these apps:
 | 
			
		||||
#
 | 
			
		||||
#   browser:        {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
 | 
			
		||||
 | 
			
		||||
category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
 | 
			
		||||
 | 
			
		||||
#include common/Components.manifest
 | 
			
		||||
#include sessionstore/SessionComponents.manifest
 | 
			
		||||
							
								
								
									
										5
									
								
								src/zen/common/Components.manifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/zen/common/Components.manifest
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
# 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/.
 | 
			
		||||
 | 
			
		||||
category browser-before-ui-startup resource:///modules/ZenActorsManager.sys.mjs gZenActorsManager.init
 | 
			
		||||
@@ -1,34 +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/.
 | 
			
		||||
// Utility to register JSWindowActors
 | 
			
		||||
 | 
			
		||||
window.gZenActorsManager = {
 | 
			
		||||
  _actors: new Set(),
 | 
			
		||||
  _lazy: {},
 | 
			
		||||
 | 
			
		||||
  init() {
 | 
			
		||||
    ChromeUtils.defineESModuleGetters(this._lazy, {
 | 
			
		||||
      ActorManagerParent: 'resource://gre/modules/ActorManagerParent.sys.mjs',
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  addJSWindowActor(name, data) {
 | 
			
		||||
    if (!this._lazy.ActorManagerParent) {
 | 
			
		||||
      this.init();
 | 
			
		||||
    }
 | 
			
		||||
    if (this._actors.has(name)) {
 | 
			
		||||
      // Actor already registered, nothing to do
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const decl = {};
 | 
			
		||||
    decl[name] = data;
 | 
			
		||||
    try {
 | 
			
		||||
      this._lazy.ActorManagerParent.addJSWindowActors(decl);
 | 
			
		||||
      this._actors.add(name);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.warn(`Failed to register JSWindowActor: ${e}`);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										61
									
								
								src/zen/common/ZenActorsManager.sys.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/zen/common/ZenActorsManager.sys.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
// 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/.
 | 
			
		||||
// Utility to register JSWindowActors
 | 
			
		||||
 | 
			
		||||
import { ActorManagerParent } from 'resource://gre/modules/ActorManagerParent.sys.mjs';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fission-compatible JSProcess implementations.
 | 
			
		||||
 * Each actor options object takes the form of a ProcessActorOptions dictionary.
 | 
			
		||||
 * Detailed documentation of these options is in dom/docs/ipc/jsactors.rst,
 | 
			
		||||
 * available at https://firefox-source-docs.mozilla.org/dom/ipc/jsactors.html
 | 
			
		||||
 */
 | 
			
		||||
let JSPROCESSACTORS = {};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Fission-compatible JSWindowActor implementations.
 | 
			
		||||
 * Detailed documentation of these options is in dom/docs/ipc/jsactors.rst,
 | 
			
		||||
 * available at https://firefox-source-docs.mozilla.org/dom/ipc/jsactors.html
 | 
			
		||||
 */
 | 
			
		||||
let JSWINDOWACTORS = {
 | 
			
		||||
  ZenModsMarketplace: {
 | 
			
		||||
    parent: {
 | 
			
		||||
      esModuleURI: 'resource:///actors/ZenModsMarketplaceParent.sys.mjs',
 | 
			
		||||
    },
 | 
			
		||||
    child: {
 | 
			
		||||
      esModuleURI: 'resource:///actors/ZenModsMarketplaceChild.sys.mjs',
 | 
			
		||||
      events: {
 | 
			
		||||
        DOMContentLoaded: {},
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    matches: [
 | 
			
		||||
      ...Services.prefs.getStringPref('zen.injections.match-urls').split(','),
 | 
			
		||||
      'about:preferences',
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  ZenGlance: {
 | 
			
		||||
    parent: {
 | 
			
		||||
      esModuleURI: 'resource:///actors/ZenGlanceParent.sys.mjs',
 | 
			
		||||
    },
 | 
			
		||||
    child: {
 | 
			
		||||
      esModuleURI: 'resource:///actors/ZenGlanceChild.sys.mjs',
 | 
			
		||||
      events: {
 | 
			
		||||
        DOMContentLoaded: {},
 | 
			
		||||
        keydown: {
 | 
			
		||||
          capture: true,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    allFrames: true,
 | 
			
		||||
    matches: ['*://*/*'],
 | 
			
		||||
    enablePreference: 'zen.glance.enabled',
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export let gZenActorsManager = {
 | 
			
		||||
  init() {
 | 
			
		||||
    ActorManagerParent.addJSProcessActors(JSPROCESSACTORS);
 | 
			
		||||
    ActorManagerParent.addJSWindowActors(JSWINDOWACTORS);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
@@ -15,8 +15,8 @@
 | 
			
		||||
      if (tabData.zenWorkspace) {
 | 
			
		||||
        tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
 | 
			
		||||
      }
 | 
			
		||||
      if (tabData.zenPinnedId) {
 | 
			
		||||
        tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
 | 
			
		||||
      if (tabData.zenSyncId) {
 | 
			
		||||
        tab.setAttribute('zen-sync-id', tabData.zenSyncId);
 | 
			
		||||
      }
 | 
			
		||||
      if (tabData.zenHasStaticLabel) {
 | 
			
		||||
        tab.setAttribute('zen-has-static-label', 'true');
 | 
			
		||||
 
 | 
			
		||||
@@ -1249,14 +1249,6 @@ var gZenVerticalTabsManager = {
 | 
			
		||||
        } else {
 | 
			
		||||
          gBrowser.setTabTitle(this._tabEdited);
 | 
			
		||||
        }
 | 
			
		||||
        if (this._tabEdited.getAttribute('zen-pin-id')) {
 | 
			
		||||
          // Update pin title in storage
 | 
			
		||||
          await gZenPinnedTabManager.updatePinTitle(
 | 
			
		||||
            this._tabEdited,
 | 
			
		||||
            this._tabEdited.label,
 | 
			
		||||
            !!newName
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Maybe add some confetti here?!?
 | 
			
		||||
        gZenUIManager.motion.animate(
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
 | 
			
		||||
 | 
			
		||||
EXTRA_JS_MODULES += [
 | 
			
		||||
  "ZenActorsManager.sys.mjs",
 | 
			
		||||
  "ZenCustomizableUI.sys.mjs",
 | 
			
		||||
  "ZenUIMigration.sys.mjs",
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
@@ -150,7 +150,6 @@
 | 
			
		||||
      for (let tab of this.allItems.reverse()) {
 | 
			
		||||
        tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab;
 | 
			
		||||
        if (tab.hasAttribute('zen-empty-tab')) {
 | 
			
		||||
          await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
 | 
			
		||||
          gBrowser.removeTab(tab);
 | 
			
		||||
        } else {
 | 
			
		||||
          gBrowser.ungroupTab(tab);
 | 
			
		||||
@@ -160,7 +159,6 @@
 | 
			
		||||
 | 
			
		||||
    async delete() {
 | 
			
		||||
      for (const tab of this.allItemsRecursive) {
 | 
			
		||||
        await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
 | 
			
		||||
        if (tab.hasAttribute('zen-empty-tab')) {
 | 
			
		||||
          // Manually remove the empty tabs as removeTabs() inside removeTabGroup
 | 
			
		||||
          // does ignore them.
 | 
			
		||||
 
 | 
			
		||||
@@ -508,9 +508,6 @@
 | 
			
		||||
      tabs = [emptyTab, ...filteredTabs];
 | 
			
		||||
 | 
			
		||||
      const folder = this._createFolderNode(options);
 | 
			
		||||
      if (options.initialPinId) {
 | 
			
		||||
        folder.setAttribute('zen-pin-id', options.initialPinId);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (options.insertAfter) {
 | 
			
		||||
        options.insertAfter.after(folder);
 | 
			
		||||
@@ -940,7 +937,7 @@
 | 
			
		||||
        if (!parentFolder && folder.hasAttribute('split-view-group')) continue;
 | 
			
		||||
        const emptyFolderTabs = folder.tabs
 | 
			
		||||
          .filter((tab) => tab.hasAttribute('zen-empty-tab'))
 | 
			
		||||
          .map((tab) => tab.getAttribute('zen-pin-id'));
 | 
			
		||||
          .map((tab) => tab.getAttribute('zen-sync-id'));
 | 
			
		||||
 | 
			
		||||
        let prevSiblingInfo = null;
 | 
			
		||||
        const prevSibling = folder.previousElementSibling;
 | 
			
		||||
@@ -949,8 +946,8 @@
 | 
			
		||||
        if (prevSibling) {
 | 
			
		||||
          if (gBrowser.isTabGroup(prevSibling)) {
 | 
			
		||||
            prevSiblingInfo = { type: 'group', id: prevSibling.id };
 | 
			
		||||
          } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) {
 | 
			
		||||
            const zenPinId = prevSibling.getAttribute('zen-pin-id');
 | 
			
		||||
          } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-sync-id')) {
 | 
			
		||||
            const zenPinId = prevSibling.getAttribute('zen-sync-id');
 | 
			
		||||
            prevSiblingInfo = { type: 'tab', id: zenPinId };
 | 
			
		||||
          } else {
 | 
			
		||||
            prevSiblingInfo = { type: 'start', id: null };
 | 
			
		||||
@@ -969,7 +966,7 @@
 | 
			
		||||
          prevSiblingInfo: prevSiblingInfo,
 | 
			
		||||
          emptyTabIds: emptyFolderTabs,
 | 
			
		||||
          userIcon: userIcon?.getAttribute('href'),
 | 
			
		||||
          pinId: folder.getAttribute('zen-pin-id'),
 | 
			
		||||
          syncId: folder.getAttribute('zen-sync-id'),
 | 
			
		||||
          // note: We shouldn't be using the workspace-id anywhere, we are just
 | 
			
		||||
          //  remembering it for the pinned tabs manager to use it later.
 | 
			
		||||
          workspaceId: folder.getAttribute('zen-workspace-id'),
 | 
			
		||||
@@ -996,9 +993,9 @@
 | 
			
		||||
        tabFolderWorkingData.set(folderData.id, workingData);
 | 
			
		||||
 | 
			
		||||
        const oldGroup = document.getElementById(folderData.id);
 | 
			
		||||
        folderData.emptyTabIds.forEach((zenPinId) => {
 | 
			
		||||
        folderData.emptyTabIds.forEach((zenSyncId) => {
 | 
			
		||||
          oldGroup
 | 
			
		||||
            ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
 | 
			
		||||
            ?.querySelector(`tab[zen-sync-id="${zenSyncId}"]`)
 | 
			
		||||
            ?.setAttribute('zen-empty-tab', true);
 | 
			
		||||
        });
 | 
			
		||||
        if (oldGroup) {
 | 
			
		||||
@@ -1011,7 +1008,7 @@
 | 
			
		||||
              saveOnWindowClose: folderData.saveOnWindowClose,
 | 
			
		||||
              workspaceId: folderData.workspaceId,
 | 
			
		||||
            });
 | 
			
		||||
            folder.setAttribute('zen-pin-id', folderData.pinId);
 | 
			
		||||
            folder.setAttribute('zen-sync-id', folderData.syncId);
 | 
			
		||||
            workingData.node = folder;
 | 
			
		||||
            oldGroup.before(folder);
 | 
			
		||||
          } else {
 | 
			
		||||
@@ -1044,7 +1041,7 @@
 | 
			
		||||
            switch (stateData?.prevSiblingInfo?.type) {
 | 
			
		||||
              case 'tab': {
 | 
			
		||||
                const tab = parentWorkingData.node.querySelector(
 | 
			
		||||
                  `[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
 | 
			
		||||
                  `[zen-sync-id="${stateData.prevSiblingInfo.id}"]`
 | 
			
		||||
                );
 | 
			
		||||
                tab.after(node);
 | 
			
		||||
                break;
 | 
			
		||||
 
 | 
			
		||||
@@ -1716,29 +1716,4 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  window.gZenGlanceManager = new nsZenGlanceManager();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Register window actors for glance functionality
 | 
			
		||||
   */
 | 
			
		||||
  function registerWindowActors() {
 | 
			
		||||
    gZenActorsManager.addJSWindowActor('ZenGlance', {
 | 
			
		||||
      parent: {
 | 
			
		||||
        esModuleURI: 'resource:///actors/ZenGlanceParent.sys.mjs',
 | 
			
		||||
      },
 | 
			
		||||
      child: {
 | 
			
		||||
        esModuleURI: 'resource:///actors/ZenGlanceChild.sys.mjs',
 | 
			
		||||
        events: {
 | 
			
		||||
          DOMContentLoaded: {},
 | 
			
		||||
          keydown: {
 | 
			
		||||
            capture: true,
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      allFrames: true,
 | 
			
		||||
      matches: ['*://*/*'],
 | 
			
		||||
      enablePreference: 'zen.glance.enabled',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  registerWindowActors();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -667,20 +667,4 @@
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  window.gZenMods = new nsZenMods();
 | 
			
		||||
 | 
			
		||||
  gZenActorsManager.addJSWindowActor('ZenModsMarketplace', {
 | 
			
		||||
    parent: {
 | 
			
		||||
      esModuleURI: 'resource:///actors/ZenModsMarketplaceParent.sys.mjs',
 | 
			
		||||
    },
 | 
			
		||||
    child: {
 | 
			
		||||
      esModuleURI: 'resource:///actors/ZenModsMarketplaceChild.sys.mjs',
 | 
			
		||||
      events: {
 | 
			
		||||
        DOMContentLoaded: {},
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    matches: [
 | 
			
		||||
      ...Services.prefs.getStringPref('zen.injections.match-urls').split(','),
 | 
			
		||||
      'about:preferences',
 | 
			
		||||
    ],
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,10 @@
 | 
			
		||||
# 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_PP_COMPONENTS += [
 | 
			
		||||
  "ZenComponents.manifest",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
DIRS += [
 | 
			
		||||
    "common",
 | 
			
		||||
    "glance",
 | 
			
		||||
@@ -9,4 +13,5 @@ DIRS += [
 | 
			
		||||
    "tests",
 | 
			
		||||
    "urlbar",
 | 
			
		||||
    "toolkit",
 | 
			
		||||
    "sessionstore",
 | 
			
		||||
]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								src/zen/sessionstore/SessionComponents.manifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/zen/sessionstore/SessionComponents.manifest
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
							
								
								
									
										40
									
								
								src/zen/sessionstore/ZenSessionFile.sys.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/zen/sessionstore/ZenSessionFile.sys.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
// 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/.
 | 
			
		||||
 | 
			
		||||
// 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 = PathUtils.join(PathUtils.profileDir, FILE_NAME);
 | 
			
		||||
  #sidebar = [];
 | 
			
		||||
 | 
			
		||||
  async read() {
 | 
			
		||||
    try {
 | 
			
		||||
      const data = await IOUtils.readJSON(this.#path, { compress: SHOULD_COMPRESS_FILE });
 | 
			
		||||
      this.#sidebar = data.sidebar || [];
 | 
			
		||||
    } catch {
 | 
			
		||||
      // File doesn't exist yet, that's fine.
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										163
									
								
								src/zen/sessionstore/ZenSessionManager.sys.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/zen/sessionstore/ZenSessionManager.sys.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
// 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/.
 | 
			
		||||
 | 
			
		||||
const lazy = {};
 | 
			
		||||
 | 
			
		||||
ChromeUtils.defineESModuleGetters(lazy, {
 | 
			
		||||
  nsZenSessionFile: 'resource:///modules/zen/ZenSessionFile.sys.mjs',
 | 
			
		||||
  PrivateBrowsingUtils: 'resource://gre/modules/PrivateBrowsingUtils.sys.mjs',
 | 
			
		||||
  BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
 | 
			
		||||
  TabGroupState: 'resource:///modules/sessionstore/TabGroupState.sys.mjs',
 | 
			
		||||
  SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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 = new lazy.nsZenSessionFile();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #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
 | 
			
		||||
    void aWindow;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get #topMostWindow() {
 | 
			
		||||
    return lazy.BrowserWindowTracker.getTopWindow();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Saves the current session state. Collects data and writes to disk.
 | 
			
		||||
   *
 | 
			
		||||
   * @param forceUpdateAllWindows (optional)
 | 
			
		||||
   *        Forces us to recollect data for all windows and will bypass and
 | 
			
		||||
   *        update the corresponding caches.
 | 
			
		||||
   */
 | 
			
		||||
  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.
 | 
			
		||||
   */
 | 
			
		||||
  #collectWindowData(window, forceUpdate = false) {
 | 
			
		||||
    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) {
 | 
			
		||||
    let newWindow = { ...Cu.cloneInto(aWindows[Object.keys(aWindows)[0]], {}), ...this.#sidebar };
 | 
			
		||||
    return { windows: [newWindow] };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ZenSessionStore = new nsZenSessionManager();
 | 
			
		||||
							
								
								
									
										8
									
								
								src/zen/sessionstore/moz.build
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/zen/sessionstore/moz.build
									
									
									
									
									
										Normal file
									
								
							@@ -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",
 | 
			
		||||
]
 | 
			
		||||
@@ -5,23 +5,7 @@
 | 
			
		||||
  const lazy = {};
 | 
			
		||||
 | 
			
		||||
  class ZenPinnedTabsObserver {
 | 
			
		||||
    static ALL_EVENTS = [
 | 
			
		||||
      'TabPinned',
 | 
			
		||||
      'TabUnpinned',
 | 
			
		||||
      'TabMove',
 | 
			
		||||
      'TabGroupCreate',
 | 
			
		||||
      'TabGroupRemoved',
 | 
			
		||||
      'TabGroupMoved',
 | 
			
		||||
      'ZenFolderRenamed',
 | 
			
		||||
      'ZenFolderIconChanged',
 | 
			
		||||
      'TabGroupCollapse',
 | 
			
		||||
      'TabGroupExpand',
 | 
			
		||||
      'TabGrouped',
 | 
			
		||||
      'TabUngrouped',
 | 
			
		||||
      'ZenFolderChangedWorkspace',
 | 
			
		||||
      'TabAddedToEssentials',
 | 
			
		||||
      'TabRemovedFromEssentials',
 | 
			
		||||
    ];
 | 
			
		||||
    static ALL_EVENTS = ['TabPinned', 'TabUnpinned'];
 | 
			
		||||
 | 
			
		||||
    #listeners = [];
 | 
			
		||||
 | 
			
		||||
@@ -101,22 +85,10 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onTabIconChanged(tab, url = null) {
 | 
			
		||||
      tab.dispatchEvent(new CustomEvent('ZenTabIconChanged', { bubbles: true, detail: { tab } }));
 | 
			
		||||
      const iconUrl = url ?? tab.iconImage.src;
 | 
			
		||||
      if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
 | 
			
		||||
        try {
 | 
			
		||||
          setTimeout(async () => {
 | 
			
		||||
            const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI);
 | 
			
		||||
            if (favicon) {
 | 
			
		||||
              gBrowser.setIcon(tab, favicon);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        } catch {
 | 
			
		||||
          // Handle error
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        if (tab.hasAttribute('zen-essential')) {
 | 
			
		||||
          tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
 | 
			
		||||
        }
 | 
			
		||||
      if (tab.hasAttribute('zen-essential')) {
 | 
			
		||||
        tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -148,261 +120,6 @@
 | 
			
		||||
      return lazy.zenTabsEssentialsMax;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshPinnedTabs({ init = false } = {}) {
 | 
			
		||||
      if (!this.enabled) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      await ZenPinnedTabsStorage.promiseInitialized;
 | 
			
		||||
      await this.#initializePinsCache();
 | 
			
		||||
      setTimeout(async () => {
 | 
			
		||||
        // Execute in a separate task to avoid blocking the main thread
 | 
			
		||||
        await SessionStore.promiseAllWindowsRestored;
 | 
			
		||||
        await gZenWorkspaces.promiseInitialized;
 | 
			
		||||
        await this.#initializePinnedTabs(init);
 | 
			
		||||
        if (init) {
 | 
			
		||||
          this._hasFinishedLoading = true;
 | 
			
		||||
        }
 | 
			
		||||
      }, 10);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #initializePinsCache() {
 | 
			
		||||
      try {
 | 
			
		||||
        // Get pin data
 | 
			
		||||
        const pins = await ZenPinnedTabsStorage.getPins();
 | 
			
		||||
 | 
			
		||||
        // Enhance pins with favicons
 | 
			
		||||
        this._pinsCache = await Promise.all(
 | 
			
		||||
          pins.map(async (pin) => {
 | 
			
		||||
            try {
 | 
			
		||||
              if (pin.isGroup) {
 | 
			
		||||
                return pin; // Skip groups for now
 | 
			
		||||
              }
 | 
			
		||||
              const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url));
 | 
			
		||||
              return {
 | 
			
		||||
                ...pin,
 | 
			
		||||
                iconUrl: image || null,
 | 
			
		||||
              };
 | 
			
		||||
            } catch {
 | 
			
		||||
              // If favicon fetch fails, continue without icon
 | 
			
		||||
              return {
 | 
			
		||||
                ...pin,
 | 
			
		||||
                iconUrl: null,
 | 
			
		||||
              };
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      } catch (ex) {
 | 
			
		||||
        console.error('Failed to initialize pins cache:', ex);
 | 
			
		||||
        this._pinsCache = [];
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.log(`Initialized pins cache with ${this._pinsCache.length} pins`);
 | 
			
		||||
      return this._pinsCache;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #finishedInitializingPins() {
 | 
			
		||||
      if (this.hasInitializedPins) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this._resolvePinnedInitializedInternal();
 | 
			
		||||
      delete this._resolvePinnedInitializedInternal;
 | 
			
		||||
      this.hasInitializedPins = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #initializePinnedTabs(init = false) {
 | 
			
		||||
      const pins = this._pinsCache;
 | 
			
		||||
      if (!pins?.length || !init) {
 | 
			
		||||
        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;
 | 
			
		||||
@@ -412,230 +129,22 @@
 | 
			
		||||
      }
 | 
			
		||||
      switch (action) {
 | 
			
		||||
        case 'TabPinned':
 | 
			
		||||
        case 'TabAddedToEssentials':
 | 
			
		||||
          tab._zenClickEventListener = this._zenClickEventListener;
 | 
			
		||||
          tab.addEventListener('click', tab._zenClickEventListener);
 | 
			
		||||
          this._setPinnedAttributes(tab);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabRemovedFromEssentials':
 | 
			
		||||
          if (tab.pinned) {
 | 
			
		||||
            this.#onTabMove(tab);
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        // [Fall through]
 | 
			
		||||
        case 'TabUnpinned':
 | 
			
		||||
          this._removePinnedAttributes(tab);
 | 
			
		||||
          if (tab._zenClickEventListener) {
 | 
			
		||||
            tab.removeEventListener('click', tab._zenClickEventListener);
 | 
			
		||||
            delete tab._zenClickEventListener;
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabMove':
 | 
			
		||||
          this.#onTabMove(tab);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabGroupCreate':
 | 
			
		||||
          this.#onTabGroupCreate(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabGroupRemoved':
 | 
			
		||||
          this.#onTabGroupRemoved(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabGroupMoved':
 | 
			
		||||
          this.#onTabGroupMoved(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'ZenFolderRenamed':
 | 
			
		||||
        case 'ZenFolderIconChanged':
 | 
			
		||||
        case 'TabGroupCollapse':
 | 
			
		||||
        case 'TabGroupExpand':
 | 
			
		||||
        case 'ZenFolderChangedWorkspace':
 | 
			
		||||
          this.#updateGroupInfo(event.originalTarget);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabGrouped':
 | 
			
		||||
          this.#onTabGrouped(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabUngrouped':
 | 
			
		||||
          this.#onTabUngrouped(event);
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          console.warn('ZenPinnedTabManager: Unhandled tab event', action);
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabGroupCreate(event) {
 | 
			
		||||
      const group = event.originalTarget;
 | 
			
		||||
      if (!group.isZenFolder) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (group.hasAttribute('zen-pin-id')) {
 | 
			
		||||
        return; // Group already exists in storage
 | 
			
		||||
      }
 | 
			
		||||
      const workspaceId = group.getAttribute('zen-workspace-id');
 | 
			
		||||
      let id = await ZenPinnedTabsStorage.createGroup(
 | 
			
		||||
        group.name,
 | 
			
		||||
        group.iconURL,
 | 
			
		||||
        group.collapsed,
 | 
			
		||||
        workspaceId,
 | 
			
		||||
        group.getAttribute('zen-pin-id'),
 | 
			
		||||
        group._pPos
 | 
			
		||||
      );
 | 
			
		||||
      group.setAttribute('zen-pin-id', id);
 | 
			
		||||
      for (const tab of group.tabs) {
 | 
			
		||||
        // Only add it if the tab is directly under the group
 | 
			
		||||
        if (
 | 
			
		||||
          tab.pinned &&
 | 
			
		||||
          tab.hasAttribute('zen-pin-id') &&
 | 
			
		||||
          tab.group === group &&
 | 
			
		||||
          this.hasInitializedPins
 | 
			
		||||
        ) {
 | 
			
		||||
          const tabPinId = tab.getAttribute('zen-pin-id');
 | 
			
		||||
          await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      await this.refreshPinnedTabs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabGrouped(event) {
 | 
			
		||||
      const tab = event.detail;
 | 
			
		||||
      const group = tab.group;
 | 
			
		||||
      if (!group.isZenFolder) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const pinId = group.getAttribute('zen-pin-id');
 | 
			
		||||
      const tabPinId = tab.getAttribute('zen-pin-id');
 | 
			
		||||
      const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
 | 
			
		||||
      if (!tabPin || !tabPin.group) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabUngrouped(event) {
 | 
			
		||||
      const tab = event.detail;
 | 
			
		||||
      const group = tab.group;
 | 
			
		||||
      if (!group?.isZenFolder) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const tabPinId = tab.getAttribute('zen-pin-id');
 | 
			
		||||
      const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
 | 
			
		||||
      if (!tabPin) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #updateGroupInfo(group) {
 | 
			
		||||
      if (!group?.isZenFolder) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const pinId = group.getAttribute('zen-pin-id');
 | 
			
		||||
      const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
 | 
			
		||||
      if (groupPin) {
 | 
			
		||||
        groupPin.title = group.name;
 | 
			
		||||
        groupPin.folderIcon = group.iconURL;
 | 
			
		||||
        groupPin.isFolderCollapsed = group.collapsed;
 | 
			
		||||
        groupPin.position = group._pPos;
 | 
			
		||||
        groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null;
 | 
			
		||||
        groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null;
 | 
			
		||||
        await this.savePin(groupPin);
 | 
			
		||||
        for (const item of group.allItems) {
 | 
			
		||||
          if (gBrowser.isTabGroup(item)) {
 | 
			
		||||
            await this.#updateGroupInfo(item);
 | 
			
		||||
          } else {
 | 
			
		||||
            await this.#onTabMove(item);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabGroupRemoved(event) {
 | 
			
		||||
      const group = event.originalTarget;
 | 
			
		||||
      if (!group.isZenFolder) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id'));
 | 
			
		||||
      group.removeAttribute('zen-pin-id');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabGroupMoved(event) {
 | 
			
		||||
      const group = event.originalTarget;
 | 
			
		||||
      if (!group.isZenFolder) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const newIndex = group._pPos;
 | 
			
		||||
      const pinId = group.getAttribute('zen-pin-id');
 | 
			
		||||
      if (!pinId) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      for (const tab of group.allItemsRecursive) {
 | 
			
		||||
        if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) {
 | 
			
		||||
          const pin = this._pinsCache.find((p) => p.uuid === pinId);
 | 
			
		||||
          if (pin) {
 | 
			
		||||
            pin.position = tab._pPos;
 | 
			
		||||
            pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
 | 
			
		||||
            pin.workspaceUuid = group.getAttribute('zen-workspace-id');
 | 
			
		||||
            await this.savePin(pin, false);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
 | 
			
		||||
      if (groupPin) {
 | 
			
		||||
        groupPin.position = newIndex;
 | 
			
		||||
        groupPin.parentUuid = group.group?.getAttribute('zen-pin-id');
 | 
			
		||||
        groupPin.workspaceUuid = group.getAttribute('zen-workspace-id');
 | 
			
		||||
        await this.savePin(groupPin);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabMove(tab) {
 | 
			
		||||
      if (!tab.pinned || !this._pinsCache) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups];
 | 
			
		||||
      for (let i = 0; i < allTabs.length; i++) {
 | 
			
		||||
        const otherTab = allTabs[i];
 | 
			
		||||
        if (
 | 
			
		||||
          otherTab.pinned &&
 | 
			
		||||
          otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')
 | 
			
		||||
        ) {
 | 
			
		||||
          const actualPin = this._pinsCache.find(
 | 
			
		||||
            (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id')
 | 
			
		||||
          );
 | 
			
		||||
          if (!actualPin) {
 | 
			
		||||
            continue;
 | 
			
		||||
          }
 | 
			
		||||
          actualPin.position = otherTab._pPos;
 | 
			
		||||
          actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id');
 | 
			
		||||
          actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null;
 | 
			
		||||
          await this.savePin(actualPin, false);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
 | 
			
		||||
 | 
			
		||||
      if (!actualPin) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      actualPin.position = tab._pPos;
 | 
			
		||||
      actualPin.isEssential = tab.hasAttribute('zen-essential');
 | 
			
		||||
      actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
 | 
			
		||||
      actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null;
 | 
			
		||||
 | 
			
		||||
      // There was a bug where the title and hasStaticLabel attribute were not being set
 | 
			
		||||
      // This is a workaround to fix that
 | 
			
		||||
      if (tab.hasAttribute('zen-has-static-label')) {
 | 
			
		||||
        actualPin.editedTitle = true;
 | 
			
		||||
        actualPin.title = tab.label;
 | 
			
		||||
      }
 | 
			
		||||
      await this.savePin(actualPin);
 | 
			
		||||
      tab.dispatchEvent(
 | 
			
		||||
        new CustomEvent('ZenPinnedTabMoved', {
 | 
			
		||||
          detail: { tab },
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _onTabClick(e) {
 | 
			
		||||
      const tab = e.target?.closest('tab');
 | 
			
		||||
      if (e.button === 1 && tab) {
 | 
			
		||||
@@ -665,106 +174,10 @@
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const browser = tab.linkedBrowser;
 | 
			
		||||
 | 
			
		||||
      const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
 | 
			
		||||
 | 
			
		||||
      if (!pin) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const userContextId = tab.getAttribute('usercontextid');
 | 
			
		||||
 | 
			
		||||
      pin.title = tab.label || browser.contentTitle;
 | 
			
		||||
      pin.url = browser.currentURI.spec;
 | 
			
		||||
      pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
 | 
			
		||||
      pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
 | 
			
		||||
 | 
			
		||||
      await this.savePin(pin);
 | 
			
		||||
      this.resetPinChangedUrl(tab);
 | 
			
		||||
      await this.refreshPinnedTabs();
 | 
			
		||||
      gZenUIManager.showToast('zen-pinned-tab-replaced');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _setPinnedAttributes(tab) {
 | 
			
		||||
      if (
 | 
			
		||||
        tab.hasAttribute('zen-pin-id') ||
 | 
			
		||||
        !this._hasFinishedLoading ||
 | 
			
		||||
        tab.hasAttribute('zen-empty-tab')
 | 
			
		||||
      ) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
 | 
			
		||||
      const browser = tab.linkedBrowser;
 | 
			
		||||
 | 
			
		||||
      const uuid = gZenUIManager.generateUuidv4();
 | 
			
		||||
      const userContextId = tab.getAttribute('usercontextid');
 | 
			
		||||
 | 
			
		||||
      let entry = null;
 | 
			
		||||
 | 
			
		||||
      if (tab.getAttribute('zen-pinned-entry')) {
 | 
			
		||||
        entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await this.savePin({
 | 
			
		||||
        uuid,
 | 
			
		||||
        title: entry?.title || tab.label || browser.contentTitle,
 | 
			
		||||
        url: entry?.url || browser.currentURI.spec,
 | 
			
		||||
        containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
 | 
			
		||||
        workspaceUuid: tab.getAttribute('zen-workspace-id'),
 | 
			
		||||
        isEssential: tab.getAttribute('zen-essential') === 'true',
 | 
			
		||||
        parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
 | 
			
		||||
        position: tab._pPos,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      tab.setAttribute('zen-pin-id', uuid);
 | 
			
		||||
      tab.dispatchEvent(
 | 
			
		||||
        new CustomEvent('ZenPinnedTabCreated', {
 | 
			
		||||
          detail: { tab },
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // This is used while migrating old pins to new system - we don't want to refresh when migrating
 | 
			
		||||
      if (tab.getAttribute('zen-pinned-entry')) {
 | 
			
		||||
        tab.removeAttribute('zen-pinned-entry');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.onLocationChange(browser);
 | 
			
		||||
      await this.refreshPinnedTabs();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async _removePinnedAttributes(tab, isClosing = false) {
 | 
			
		||||
      tab.removeAttribute('zen-has-static-label');
 | 
			
		||||
      if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (Services.startup.shuttingDown || window.skipNextCanClose) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
 | 
			
		||||
      await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
 | 
			
		||||
      this.resetPinChangedUrl(tab);
 | 
			
		||||
 | 
			
		||||
      if (!isClosing) {
 | 
			
		||||
        tab.removeAttribute('zen-pin-id');
 | 
			
		||||
        tab.removeAttribute('zen-essential'); // Just in case
 | 
			
		||||
 | 
			
		||||
        if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
 | 
			
		||||
          const workspace = await gZenWorkspaces.getActiveWorkspace();
 | 
			
		||||
          tab.setAttribute('zen-workspace-id', workspace.uuid);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      await this.refreshPinnedTabs();
 | 
			
		||||
      tab.dispatchEvent(
 | 
			
		||||
        new CustomEvent('ZenPinnedTabRemoved', {
 | 
			
		||||
          detail: { tab },
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _initClosePinnedTabShortcut() {
 | 
			
		||||
      let cmdClose = document.getElementById('cmd_close');
 | 
			
		||||
 | 
			
		||||
@@ -773,21 +186,6 @@
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async savePin(pin, notifyObservers = true) {
 | 
			
		||||
      if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid);
 | 
			
		||||
      if (existingPin) {
 | 
			
		||||
        Object.assign(existingPin, pin);
 | 
			
		||||
      } else {
 | 
			
		||||
        // We shouldn't need it, but just in case there's
 | 
			
		||||
        // a race condition while making new pinned tabs.
 | 
			
		||||
        this._pinsCache.push(pin);
 | 
			
		||||
      }
 | 
			
		||||
      await ZenPinnedTabsStorage.savePin(pin, notifyObservers);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async onCloseTabShortcut(
 | 
			
		||||
      event,
 | 
			
		||||
      selectedTab = gBrowser.selectedTab,
 | 
			
		||||
@@ -1010,12 +408,6 @@
 | 
			
		||||
          tab.removeAttribute('zen-workspace-id');
 | 
			
		||||
        }
 | 
			
		||||
        if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
 | 
			
		||||
          const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
 | 
			
		||||
          if (pin) {
 | 
			
		||||
            pin.isEssential = true;
 | 
			
		||||
            pin.workspaceUuid = null;
 | 
			
		||||
            this.savePin(pin);
 | 
			
		||||
          }
 | 
			
		||||
          gBrowser.zenHandleTabMove(tab, () => {
 | 
			
		||||
            if (tab.ownerGlobal !== window) {
 | 
			
		||||
              tab = gBrowser.adoptTab(tab, {
 | 
			
		||||
@@ -1406,11 +798,8 @@
 | 
			
		||||
      return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
 | 
			
		||||
    async updatePinTitle(tab, newTitle, isEdited = true) {
 | 
			
		||||
      const uuid = tab.getAttribute('zen-pin-id');
 | 
			
		||||
      await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
 | 
			
		||||
 | 
			
		||||
      await this.refreshPinnedTabs();
 | 
			
		||||
 | 
			
		||||
      const browsers = Services.wm.getEnumerator('navigator:browser');
 | 
			
		||||
 | 
			
		||||
@@ -1555,6 +944,7 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async onTabLabelChanged(tab) {
 | 
			
		||||
      tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { detail: { tab } }));
 | 
			
		||||
      if (!this._pinsCache) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,635 +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/.
 | 
			
		||||
var ZenPinnedTabsStorage = {
 | 
			
		||||
  async init() {
 | 
			
		||||
    await this._ensureTable();
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async _ensureTable() {
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => {
 | 
			
		||||
      // Create the pins table if it doesn't exist
 | 
			
		||||
      await db.execute(`
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS zen_pins (
 | 
			
		||||
      id INTEGER PRIMARY KEY,
 | 
			
		||||
      uuid TEXT UNIQUE NOT NULL,
 | 
			
		||||
      title TEXT NOT NULL,
 | 
			
		||||
      url TEXT,
 | 
			
		||||
      container_id INTEGER,
 | 
			
		||||
      workspace_uuid TEXT,
 | 
			
		||||
      position INTEGER NOT NULL DEFAULT 0,
 | 
			
		||||
      is_essential BOOLEAN NOT NULL DEFAULT 0,
 | 
			
		||||
      is_group BOOLEAN NOT NULL DEFAULT 0,
 | 
			
		||||
      created_at INTEGER NOT NULL,
 | 
			
		||||
      updated_at INTEGER NOT NULL
 | 
			
		||||
          )
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      const columns = await db.execute(`PRAGMA table_info(zen_pins)`);
 | 
			
		||||
      const columnNames = columns.map((row) => row.getResultByName('name'));
 | 
			
		||||
 | 
			
		||||
      // Helper function to add column if it doesn't exist
 | 
			
		||||
      const addColumnIfNotExists = async (columnName, definition) => {
 | 
			
		||||
        if (!columnNames.includes(columnName)) {
 | 
			
		||||
          await db.execute(`ALTER TABLE zen_pins ADD COLUMN ${columnName} ${definition}`);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      await addColumnIfNotExists('edited_title', 'BOOLEAN NOT NULL DEFAULT 0');
 | 
			
		||||
      await addColumnIfNotExists('is_folder_collapsed', 'BOOLEAN NOT NULL DEFAULT 0');
 | 
			
		||||
      await addColumnIfNotExists('folder_icon', 'TEXT DEFAULT NULL');
 | 
			
		||||
      await addColumnIfNotExists('folder_parent_uuid', 'TEXT DEFAULT NULL');
 | 
			
		||||
 | 
			
		||||
      await db.execute(`
 | 
			
		||||
        CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid)
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      await db.execute(`
 | 
			
		||||
        CREATE TABLE IF NOT EXISTS zen_pins_changes (
 | 
			
		||||
          uuid TEXT PRIMARY KEY,
 | 
			
		||||
          timestamp INTEGER NOT NULL
 | 
			
		||||
        )
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      await db.execute(`
 | 
			
		||||
        CREATE INDEX IF NOT EXISTS idx_zen_pins_changes_uuid ON zen_pins_changes(uuid)
 | 
			
		||||
      `);
 | 
			
		||||
 | 
			
		||||
      this._resolveInitialized();
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Private helper method to notify observers with a list of changed UUIDs.
 | 
			
		||||
   * @param {string} event - The observer event name.
 | 
			
		||||
   * @param {Array<string>} uuids - Array of changed workspace UUIDs.
 | 
			
		||||
   */
 | 
			
		||||
  _notifyPinsChanged(event, uuids) {
 | 
			
		||||
    if (uuids.length === 0) return; // No changes to notify
 | 
			
		||||
 | 
			
		||||
    // Convert the array of UUIDs to a JSON string
 | 
			
		||||
    const data = JSON.stringify(uuids);
 | 
			
		||||
 | 
			
		||||
    Services.obs.notifyObservers(null, event, data);
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async savePin(pin, notifyObservers = true) {
 | 
			
		||||
    const changedUUIDs = new Set();
 | 
			
		||||
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => {
 | 
			
		||||
      await db.executeTransaction(async () => {
 | 
			
		||||
        const now = Date.now();
 | 
			
		||||
 | 
			
		||||
        let newPosition;
 | 
			
		||||
        if ('position' in pin && Number.isFinite(pin.position)) {
 | 
			
		||||
          newPosition = pin.position;
 | 
			
		||||
        } else {
 | 
			
		||||
          // Get the maximum position within the same parent group (or null for root level)
 | 
			
		||||
          const maxPositionResult = await db.execute(
 | 
			
		||||
            `
 | 
			
		||||
            SELECT MAX("position") as max_position
 | 
			
		||||
            FROM zen_pins
 | 
			
		||||
            WHERE COALESCE(folder_parent_uuid, '') = COALESCE(:folder_parent_uuid, '')
 | 
			
		||||
          `,
 | 
			
		||||
            { folder_parent_uuid: pin.parentUuid || null }
 | 
			
		||||
          );
 | 
			
		||||
          const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
 | 
			
		||||
          newPosition = maxPosition + 1000;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Insert or replace the pin
 | 
			
		||||
        await db.executeCached(
 | 
			
		||||
          `
 | 
			
		||||
          INSERT OR REPLACE INTO zen_pins (
 | 
			
		||||
            uuid, title, url, container_id, workspace_uuid, position,
 | 
			
		||||
            is_essential, is_group, folder_parent_uuid, edited_title, created_at,
 | 
			
		||||
            updated_at, is_folder_collapsed, folder_icon
 | 
			
		||||
          ) VALUES (
 | 
			
		||||
            :uuid, :title, :url, :container_id, :workspace_uuid, :position,
 | 
			
		||||
            :is_essential, :is_group, :folder_parent_uuid, :edited_title,
 | 
			
		||||
            COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now),
 | 
			
		||||
            :now, :is_folder_collapsed, :folder_icon
 | 
			
		||||
          )
 | 
			
		||||
        `,
 | 
			
		||||
          {
 | 
			
		||||
            uuid: pin.uuid,
 | 
			
		||||
            title: pin.title,
 | 
			
		||||
            url: pin.isGroup ? '' : pin.url,
 | 
			
		||||
            container_id: pin.containerTabId || null,
 | 
			
		||||
            workspace_uuid: pin.workspaceUuid || null,
 | 
			
		||||
            position: newPosition,
 | 
			
		||||
            is_essential: pin.isEssential || false,
 | 
			
		||||
            is_group: pin.isGroup || false,
 | 
			
		||||
            folder_parent_uuid: pin.parentUuid || null,
 | 
			
		||||
            edited_title: pin.editedTitle || false,
 | 
			
		||||
            now,
 | 
			
		||||
            folder_icon: pin.folderIcon || null,
 | 
			
		||||
            is_folder_collapsed: pin.isFolderCollapsed || false,
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await db.execute(
 | 
			
		||||
          `
 | 
			
		||||
          INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
          VALUES (:uuid, :timestamp)
 | 
			
		||||
        `,
 | 
			
		||||
          {
 | 
			
		||||
            uuid: pin.uuid,
 | 
			
		||||
            timestamp: Math.floor(now / 1000),
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        changedUUIDs.add(pin.uuid);
 | 
			
		||||
        await this.updateLastChangeTimestamp(db);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (notifyObservers) {
 | 
			
		||||
      this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async getPins() {
 | 
			
		||||
    const db = await PlacesUtils.promiseDBConnection();
 | 
			
		||||
    const rows = await db.executeCached(`
 | 
			
		||||
      SELECT * FROM zen_pins
 | 
			
		||||
      ORDER BY position ASC
 | 
			
		||||
    `);
 | 
			
		||||
    return rows.map((row) => ({
 | 
			
		||||
      uuid: row.getResultByName('uuid'),
 | 
			
		||||
      title: row.getResultByName('title'),
 | 
			
		||||
      url: row.getResultByName('url'),
 | 
			
		||||
      containerTabId: row.getResultByName('container_id'),
 | 
			
		||||
      workspaceUuid: row.getResultByName('workspace_uuid'),
 | 
			
		||||
      position: row.getResultByName('position'),
 | 
			
		||||
      isEssential: Boolean(row.getResultByName('is_essential')),
 | 
			
		||||
      isGroup: Boolean(row.getResultByName('is_group')),
 | 
			
		||||
      parentUuid: row.getResultByName('folder_parent_uuid'),
 | 
			
		||||
      editedTitle: Boolean(row.getResultByName('edited_title')),
 | 
			
		||||
      folderIcon: row.getResultByName('folder_icon'),
 | 
			
		||||
      isFolderCollapsed: Boolean(row.getResultByName('is_folder_collapsed')),
 | 
			
		||||
    }));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a new group
 | 
			
		||||
   * @param {string} title - The title of the group
 | 
			
		||||
   * @param {string} workspaceUuid - The workspace UUID (optional)
 | 
			
		||||
   * @param {string} parentUuid - The parent group UUID (optional, null for root level)
 | 
			
		||||
   * @param {number} position - The position of the group (optional, will auto-calculate if not provided)
 | 
			
		||||
   * @param {boolean} notifyObservers - Whether to notify observers (default: true)
 | 
			
		||||
   * @returns {Promise<string>} The UUID of the created group
 | 
			
		||||
   */
 | 
			
		||||
  async createGroup(
 | 
			
		||||
    title,
 | 
			
		||||
    icon = null,
 | 
			
		||||
    isCollapsed = false,
 | 
			
		||||
    workspaceUuid = null,
 | 
			
		||||
    parentUuid = null,
 | 
			
		||||
    position = null,
 | 
			
		||||
    notifyObservers = true
 | 
			
		||||
  ) {
 | 
			
		||||
    if (!title || typeof title !== 'string') {
 | 
			
		||||
      throw new Error('Group title is required and must be a string');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const groupUuid = gZenUIManager.generateUuidv4();
 | 
			
		||||
 | 
			
		||||
    const groupPin = {
 | 
			
		||||
      uuid: groupUuid,
 | 
			
		||||
      title,
 | 
			
		||||
      folderIcon: icon || null,
 | 
			
		||||
      isFolderCollapsed: isCollapsed || false,
 | 
			
		||||
      workspaceUuid,
 | 
			
		||||
      parentUuid,
 | 
			
		||||
      position,
 | 
			
		||||
      isGroup: true,
 | 
			
		||||
      isEssential: false,
 | 
			
		||||
      editedTitle: true, // Group titles are always considered edited
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    await this.savePin(groupPin, notifyObservers);
 | 
			
		||||
    return groupUuid;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add an existing tab/pin to a group
 | 
			
		||||
   * @param {string} tabUuid - The UUID of the tab to add to the group
 | 
			
		||||
   * @param {string} groupUuid - The UUID of the target group
 | 
			
		||||
   * @param {number} position - The position within the group (optional, will append if not provided)
 | 
			
		||||
   * @param {boolean} notifyObservers - Whether to notify observers (default: true)
 | 
			
		||||
   */
 | 
			
		||||
  async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) {
 | 
			
		||||
    if (!tabUuid || !groupUuid) {
 | 
			
		||||
      throw new Error('Both tabUuid and groupUuid are required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const changedUUIDs = new Set();
 | 
			
		||||
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => {
 | 
			
		||||
      await db.executeTransaction(async () => {
 | 
			
		||||
        // Verify the group exists and is actually a group
 | 
			
		||||
        const groupCheck = await db.execute(
 | 
			
		||||
          `SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`,
 | 
			
		||||
          { groupUuid }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (groupCheck.length === 0) {
 | 
			
		||||
          throw new Error(`Group with UUID ${groupUuid} does not exist`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!groupCheck[0].getResultByName('is_group')) {
 | 
			
		||||
          throw new Error(`Pin with UUID ${groupUuid} is not a group`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, {
 | 
			
		||||
          tabUuid,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (tabCheck.length === 0) {
 | 
			
		||||
          throw new Error(`Tab with UUID ${tabUuid} does not exist`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const now = Date.now();
 | 
			
		||||
        let newPosition;
 | 
			
		||||
 | 
			
		||||
        if (position !== null && Number.isFinite(position)) {
 | 
			
		||||
          newPosition = position;
 | 
			
		||||
        } else {
 | 
			
		||||
          // Get the maximum position within the group
 | 
			
		||||
          const maxPositionResult = await db.execute(
 | 
			
		||||
            `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`,
 | 
			
		||||
            { groupUuid }
 | 
			
		||||
          );
 | 
			
		||||
          const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
 | 
			
		||||
          newPosition = maxPosition + 1000;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await db.execute(
 | 
			
		||||
          `
 | 
			
		||||
          UPDATE zen_pins
 | 
			
		||||
          SET folder_parent_uuid = :groupUuid,
 | 
			
		||||
              position = :newPosition,
 | 
			
		||||
              updated_at = :now
 | 
			
		||||
          WHERE uuid = :tabUuid
 | 
			
		||||
          `,
 | 
			
		||||
          {
 | 
			
		||||
            tabUuid,
 | 
			
		||||
            groupUuid,
 | 
			
		||||
            newPosition,
 | 
			
		||||
            now,
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        changedUUIDs.add(tabUuid);
 | 
			
		||||
 | 
			
		||||
        await db.execute(
 | 
			
		||||
          `
 | 
			
		||||
          INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
          VALUES (:uuid, :timestamp)
 | 
			
		||||
          `,
 | 
			
		||||
          {
 | 
			
		||||
            uuid: tabUuid,
 | 
			
		||||
            timestamp: Math.floor(now / 1000),
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await this.updateLastChangeTimestamp(db);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (notifyObservers) {
 | 
			
		||||
      this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove a tab from its group (move to root level)
 | 
			
		||||
   * @param {string} tabUuid - The UUID of the tab to remove from its group
 | 
			
		||||
   * @param {number} newPosition - The new position at root level (optional, will append if not provided)
 | 
			
		||||
   * @param {boolean} notifyObservers - Whether to notify observers (default: true)
 | 
			
		||||
   */
 | 
			
		||||
  async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) {
 | 
			
		||||
    if (!tabUuid) {
 | 
			
		||||
      throw new Error('tabUuid is required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const changedUUIDs = new Set();
 | 
			
		||||
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper(
 | 
			
		||||
      'ZenPinnedTabsStorage.removeTabFromGroup',
 | 
			
		||||
      async (db) => {
 | 
			
		||||
        await db.executeTransaction(async () => {
 | 
			
		||||
          // Verify the tab exists and is in a group
 | 
			
		||||
          const tabCheck = await db.execute(
 | 
			
		||||
            `SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`,
 | 
			
		||||
            { tabUuid }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if (tabCheck.length === 0) {
 | 
			
		||||
            throw new Error(`Tab with UUID ${tabUuid} does not exist`);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (!tabCheck[0].getResultByName('folder_parent_uuid')) {
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const now = Date.now();
 | 
			
		||||
          let finalPosition;
 | 
			
		||||
 | 
			
		||||
          if (newPosition !== null && Number.isFinite(newPosition)) {
 | 
			
		||||
            finalPosition = newPosition;
 | 
			
		||||
          } else {
 | 
			
		||||
            // Get the maximum position at root level (where folder_parent_uuid is null)
 | 
			
		||||
            const maxPositionResult = await db.execute(
 | 
			
		||||
              `SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL`
 | 
			
		||||
            );
 | 
			
		||||
            const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
 | 
			
		||||
            finalPosition = maxPosition + 1000;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Update the tab to be at root level
 | 
			
		||||
          await db.execute(
 | 
			
		||||
            `
 | 
			
		||||
          UPDATE zen_pins
 | 
			
		||||
          SET folder_parent_uuid = NULL,
 | 
			
		||||
              position = :newPosition,
 | 
			
		||||
              updated_at = :now
 | 
			
		||||
          WHERE uuid = :tabUuid
 | 
			
		||||
          `,
 | 
			
		||||
            {
 | 
			
		||||
              tabUuid,
 | 
			
		||||
              newPosition: finalPosition,
 | 
			
		||||
              now,
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          changedUUIDs.add(tabUuid);
 | 
			
		||||
 | 
			
		||||
          // Record the change
 | 
			
		||||
          await db.execute(
 | 
			
		||||
            `
 | 
			
		||||
          INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
          VALUES (:uuid, :timestamp)
 | 
			
		||||
          `,
 | 
			
		||||
            {
 | 
			
		||||
              uuid: tabUuid,
 | 
			
		||||
              timestamp: Math.floor(now / 1000),
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await this.updateLastChangeTimestamp(db);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (notifyObservers) {
 | 
			
		||||
      this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async removePin(uuid, notifyObservers = true) {
 | 
			
		||||
    const changedUUIDs = [uuid];
 | 
			
		||||
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => {
 | 
			
		||||
      await db.executeTransaction(async () => {
 | 
			
		||||
        // Get all child UUIDs first for change tracking
 | 
			
		||||
        const children = await db.execute(
 | 
			
		||||
          `SELECT uuid FROM zen_pins WHERE folder_parent_uuid = :uuid`,
 | 
			
		||||
          {
 | 
			
		||||
            uuid,
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Add child UUIDs to changedUUIDs array
 | 
			
		||||
        for (const child of children) {
 | 
			
		||||
          changedUUIDs.push(child.getResultByName('uuid'));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Delete the pin/group itself
 | 
			
		||||
        await db.execute(`DELETE FROM zen_pins WHERE uuid = :uuid`, { uuid });
 | 
			
		||||
 | 
			
		||||
        // Record the changes
 | 
			
		||||
        const now = Math.floor(Date.now() / 1000);
 | 
			
		||||
        for (const changedUuid of changedUUIDs) {
 | 
			
		||||
          await db.execute(
 | 
			
		||||
            `
 | 
			
		||||
            INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
            VALUES (:uuid, :timestamp)
 | 
			
		||||
          `,
 | 
			
		||||
            {
 | 
			
		||||
              uuid: changedUuid,
 | 
			
		||||
              timestamp: now,
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await this.updateLastChangeTimestamp(db);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (notifyObservers) {
 | 
			
		||||
      this._notifyPinsChanged('zen-pin-removed', changedUUIDs);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async wipeAllPins() {
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => {
 | 
			
		||||
      await db.execute(`DELETE FROM zen_pins`);
 | 
			
		||||
      await db.execute(`DELETE FROM zen_pins_changes`);
 | 
			
		||||
      await this.updateLastChangeTimestamp(db);
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async markChanged(uuid) {
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => {
 | 
			
		||||
      const now = Date.now();
 | 
			
		||||
      await db.execute(
 | 
			
		||||
        `
 | 
			
		||||
        INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
        VALUES (:uuid, :timestamp)
 | 
			
		||||
      `,
 | 
			
		||||
        {
 | 
			
		||||
          uuid,
 | 
			
		||||
          timestamp: Math.floor(now / 1000),
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async getChangedIDs() {
 | 
			
		||||
    const db = await PlacesUtils.promiseDBConnection();
 | 
			
		||||
    const rows = await db.execute(`
 | 
			
		||||
      SELECT uuid, timestamp FROM zen_pins_changes
 | 
			
		||||
    `);
 | 
			
		||||
    const changes = {};
 | 
			
		||||
    for (const row of rows) {
 | 
			
		||||
      changes[row.getResultByName('uuid')] = row.getResultByName('timestamp');
 | 
			
		||||
    }
 | 
			
		||||
    return changes;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async clearChangedIDs() {
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => {
 | 
			
		||||
      await db.execute(`DELETE FROM zen_pins_changes`);
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  shouldReorderPins(before, current, after) {
 | 
			
		||||
    const minGap = 1; // Minimum allowed gap between positions
 | 
			
		||||
    return (
 | 
			
		||||
      (before !== null && current - before < minGap) || (after !== null && after - current < minGap)
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async reorderAllPins(db, changedUUIDs) {
 | 
			
		||||
    const pins = await db.execute(`
 | 
			
		||||
      SELECT uuid
 | 
			
		||||
      FROM zen_pins
 | 
			
		||||
      ORDER BY position ASC
 | 
			
		||||
    `);
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < pins.length; i++) {
 | 
			
		||||
      const newPosition = (i + 1) * 1000; // Use large increments
 | 
			
		||||
      await db.execute(
 | 
			
		||||
        `
 | 
			
		||||
        UPDATE zen_pins
 | 
			
		||||
        SET position = :newPosition
 | 
			
		||||
        WHERE uuid = :uuid
 | 
			
		||||
      `,
 | 
			
		||||
        { newPosition, uuid: pins[i].getResultByName('uuid') }
 | 
			
		||||
      );
 | 
			
		||||
      changedUUIDs.add(pins[i].getResultByName('uuid'));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async updateLastChangeTimestamp(db) {
 | 
			
		||||
    const now = Date.now();
 | 
			
		||||
    await db.execute(
 | 
			
		||||
      `
 | 
			
		||||
      INSERT OR REPLACE INTO moz_meta (key, value)
 | 
			
		||||
      VALUES ('zen_pins_last_change', :now)
 | 
			
		||||
    `,
 | 
			
		||||
      { now }
 | 
			
		||||
    );
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async getLastChangeTimestamp() {
 | 
			
		||||
    const db = await PlacesUtils.promiseDBConnection();
 | 
			
		||||
    const result = await db.executeCached(`
 | 
			
		||||
      SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change'
 | 
			
		||||
    `);
 | 
			
		||||
    return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async updatePinPositions(pins) {
 | 
			
		||||
    const changedUUIDs = new Set();
 | 
			
		||||
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper(
 | 
			
		||||
      'ZenPinnedTabsStorage.updatePinPositions',
 | 
			
		||||
      async (db) => {
 | 
			
		||||
        await db.executeTransaction(async () => {
 | 
			
		||||
          const now = Date.now();
 | 
			
		||||
 | 
			
		||||
          for (let i = 0; i < pins.length; i++) {
 | 
			
		||||
            const pin = pins[i];
 | 
			
		||||
            const newPosition = (i + 1) * 1000;
 | 
			
		||||
 | 
			
		||||
            await db.execute(
 | 
			
		||||
              `
 | 
			
		||||
            UPDATE zen_pins
 | 
			
		||||
            SET position = :newPosition
 | 
			
		||||
            WHERE uuid = :uuid
 | 
			
		||||
          `,
 | 
			
		||||
              { newPosition, uuid: pin.uuid }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedUUIDs.add(pin.uuid);
 | 
			
		||||
 | 
			
		||||
            // Record the change
 | 
			
		||||
            await db.execute(
 | 
			
		||||
              `
 | 
			
		||||
            INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
            VALUES (:uuid, :timestamp)
 | 
			
		||||
          `,
 | 
			
		||||
              {
 | 
			
		||||
                uuid: pin.uuid,
 | 
			
		||||
                timestamp: Math.floor(now / 1000),
 | 
			
		||||
              }
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          await this.updateLastChangeTimestamp(db);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async updatePinTitle(uuid, newTitle, isEdited = true, notifyObservers = true) {
 | 
			
		||||
    if (!uuid || typeof newTitle !== 'string') {
 | 
			
		||||
      throw new Error('Invalid parameters: uuid and newTitle are required');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const changedUUIDs = new Set();
 | 
			
		||||
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinTitle', async (db) => {
 | 
			
		||||
      await db.executeTransaction(async () => {
 | 
			
		||||
        const now = Date.now();
 | 
			
		||||
 | 
			
		||||
        // Update the pin's title and edited_title flag
 | 
			
		||||
        const result = await db.execute(
 | 
			
		||||
          `
 | 
			
		||||
            UPDATE zen_pins
 | 
			
		||||
            SET title = :newTitle,
 | 
			
		||||
                edited_title = :isEdited,
 | 
			
		||||
                updated_at = :now
 | 
			
		||||
            WHERE uuid = :uuid
 | 
			
		||||
          `,
 | 
			
		||||
          {
 | 
			
		||||
            uuid,
 | 
			
		||||
            newTitle,
 | 
			
		||||
            isEdited,
 | 
			
		||||
            now,
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Only proceed with change tracking if a row was actually updated
 | 
			
		||||
        if (result.rowsAffected > 0) {
 | 
			
		||||
          changedUUIDs.add(uuid);
 | 
			
		||||
 | 
			
		||||
          // Record the change
 | 
			
		||||
          await db.execute(
 | 
			
		||||
            `
 | 
			
		||||
              INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
 | 
			
		||||
          VALUES (:uuid, :timestamp)
 | 
			
		||||
            `,
 | 
			
		||||
            {
 | 
			
		||||
              uuid,
 | 
			
		||||
              timestamp: Math.floor(now / 1000),
 | 
			
		||||
            }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await this.updateLastChangeTimestamp(db);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (notifyObservers && changedUUIDs.size > 0) {
 | 
			
		||||
      this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  async __dropTables() {
 | 
			
		||||
    await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.__dropTables', async (db) => {
 | 
			
		||||
      await db.execute(`DROP TABLE IF EXISTS zen_pins`);
 | 
			
		||||
      await db.execute(`DROP TABLE IF EXISTS zen_pins_changes`);
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ZenPinnedTabsStorage.promiseInitialized = new Promise((resolve) => {
 | 
			
		||||
  ZenPinnedTabsStorage._resolveInitialized = resolve;
 | 
			
		||||
  ZenPinnedTabsStorage.init();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										308
									
								
								src/zen/workspaces/ZenWindowSyncing.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								src/zen/workspaces/ZenWindowSyncing.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,308 @@
 | 
			
		||||
// 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/.
 | 
			
		||||
{
 | 
			
		||||
  class nsZenWorkspaceWindowSync extends nsZenMultiWindowFeature {
 | 
			
		||||
    #ignoreNextEvents = false;
 | 
			
		||||
    #waitForPromise = null;
 | 
			
		||||
 | 
			
		||||
    constructor() {
 | 
			
		||||
      super();
 | 
			
		||||
      if (!window.closed) {
 | 
			
		||||
        this.init();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async init() {
 | 
			
		||||
      await gZenWorkspaces.promiseInitialized;
 | 
			
		||||
      this.#makeSureAllTabsHaveIds();
 | 
			
		||||
      this.#setUpEventListeners();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #makeSureAllTabsHaveIds() {
 | 
			
		||||
      const allTabs = gZenWorkspaces.allStoredTabs;
 | 
			
		||||
      for (const tab of allTabs) {
 | 
			
		||||
        if (!tab.hasAttribute('zen-sync-id') && !tab.hasAttribute('zen-empty-tab')) {
 | 
			
		||||
          const tabId = gZenUIManager.generateUuidv4();
 | 
			
		||||
          tab.setAttribute('zen-sync-id', tabId);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #setUpEventListeners() {
 | 
			
		||||
      const kEvents = [
 | 
			
		||||
        'TabClose',
 | 
			
		||||
        'TabOpen',
 | 
			
		||||
        'TabMove',
 | 
			
		||||
 | 
			
		||||
        'TabPinned',
 | 
			
		||||
        'TabUnpinned',
 | 
			
		||||
 | 
			
		||||
        'TabAddedToEssentials',
 | 
			
		||||
        'TabRemovedFromEssentials',
 | 
			
		||||
 | 
			
		||||
        'TabHide',
 | 
			
		||||
        'TabShow',
 | 
			
		||||
 | 
			
		||||
        'ZenTabIconChanged',
 | 
			
		||||
        'ZenTabLabelChanged',
 | 
			
		||||
 | 
			
		||||
        'TabGroupCreate',
 | 
			
		||||
        'TabGroupRemoved',
 | 
			
		||||
        'TabGrouped',
 | 
			
		||||
        'TabUngrouped',
 | 
			
		||||
        'TabGroupMoved',
 | 
			
		||||
      ];
 | 
			
		||||
      const eventListener = this.#handleEvent.bind(this);
 | 
			
		||||
      for (const event of kEvents) {
 | 
			
		||||
        window.addEventListener(event, eventListener);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      window.addEventListener('unload', () => {
 | 
			
		||||
        for (const event of kEvents) {
 | 
			
		||||
          window.removeEventListener(event, eventListener);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #handleEvent(event) {
 | 
			
		||||
      this.#propagateToOtherWindows(event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #propagateToOtherWindows(event) {
 | 
			
		||||
      if (this.#ignoreNextEvents) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (this.#waitForPromise) {
 | 
			
		||||
        await this.#waitForPromise;
 | 
			
		||||
      }
 | 
			
		||||
      this.#waitForPromise = new Promise((resolve) => {
 | 
			
		||||
        this.foreachWindowAsActive(async (browser) => {
 | 
			
		||||
          if (browser.gZenWorkspaceWindowSync && !this.windowIsActive(browser)) {
 | 
			
		||||
            await browser.gZenWorkspaceWindowSync.onExternalTabEvent(event);
 | 
			
		||||
          }
 | 
			
		||||
        }).then(() => {
 | 
			
		||||
          resolve();
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async onExternalTabEvent(event) {
 | 
			
		||||
      this.#ignoreNextEvents = true;
 | 
			
		||||
      switch (event.type) {
 | 
			
		||||
        case 'TabClose':
 | 
			
		||||
          this.#onTabClose(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabOpen':
 | 
			
		||||
          await this.#onTabOpen(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabPinned':
 | 
			
		||||
          this.#onTabPinned(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabUnpinned':
 | 
			
		||||
          this.#onTabUnpinned(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabAddedToEssentials':
 | 
			
		||||
          this.#onTabAddedToEssentials(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabRemovedFromEssentials':
 | 
			
		||||
          this.#onTabRemovedFromEssentials(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabHide':
 | 
			
		||||
          this.#onTabHide(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabShow':
 | 
			
		||||
          this.#onTabShow(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabMove':
 | 
			
		||||
        case 'TabGroupMoved':
 | 
			
		||||
          this.#onTabMove(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'ZenTabIconChanged':
 | 
			
		||||
          this.#onTabIconChanged(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'ZenTabLabelChanged':
 | 
			
		||||
          this.#onTabLabelChanged(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabGroupCreate':
 | 
			
		||||
          this.#onTabGroupCreate(event);
 | 
			
		||||
          break;
 | 
			
		||||
        case 'TabGroupRemoved':
 | 
			
		||||
        case 'TabGrouped':
 | 
			
		||||
        case 'TabUngrouped':
 | 
			
		||||
          // Tab grouping changes are automatically synced by Firefox
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          console.warn(`Unhandled event type: ${event.type}`);
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      this.#ignoreNextEvents = false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #getTabId(tab) {
 | 
			
		||||
      return tab.getAttribute('zen-sync-id');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #getTabWithId(tabId) {
 | 
			
		||||
      for (const tab of gZenWorkspaces.allStoredTabs) {
 | 
			
		||||
        if (this.#getTabId(tab) === tabId) {
 | 
			
		||||
          return tab;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabClose(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToClose = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToClose) {
 | 
			
		||||
        gBrowser.removeTab(tabToClose);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabPinned(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      if (targetTab.hasAttribute('zen-essential')) {
 | 
			
		||||
        return this.#onTabAddedToEssentials(event);
 | 
			
		||||
      }
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const elementIndex = targetTab.elementIndex;
 | 
			
		||||
      const tabToPin = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToPin) {
 | 
			
		||||
        gBrowser.pinTab(tabToPin);
 | 
			
		||||
        gBrowser.moveTabTo(tabToPin, { elementIndex, forceUngrouped: !!targetTab.group });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabUnpinned(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToUnpin = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToUnpin) {
 | 
			
		||||
        gBrowser.unpinTab(tabToUnpin);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabIconChanged(event) {
 | 
			
		||||
      this.#updateTabIconAndLabel(event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabLabelChanged(event) {
 | 
			
		||||
      this.#updateTabIconAndLabel(event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #updateTabIconAndLabel(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToChange = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToChange && tabToChange.hasAttribute('pending')) {
 | 
			
		||||
        gBrowser.setIcon(tabToChange, gBrowser.getIcon(targetTab));
 | 
			
		||||
        gBrowser._setTabLabel(tabToChange, targetTab.label);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabAddedToEssentials(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToAdd = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToAdd) {
 | 
			
		||||
        gZenPinnedTabManager.addToEssentials(tabToAdd);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabRemovedFromEssentials(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToRemove = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToRemove) {
 | 
			
		||||
        gZenPinnedTabManager.removeFromEssentials(tabToRemove);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabHide(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToHide = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToHide) {
 | 
			
		||||
        gBrowser.hideTab(tabToHide);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabShow(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToShow = this.#getTabWithId(tabId);
 | 
			
		||||
      if (tabToShow) {
 | 
			
		||||
        gBrowser.showTab(tabToShow);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabMove(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const tabId = this.#getTabId(targetTab);
 | 
			
		||||
      const tabToMove = this.#getTabWithId(tabId);
 | 
			
		||||
      const workspaceId = targetTab.getAttribute('zen-workspace-id');
 | 
			
		||||
      const isEssential = targetTab.hasAttribute('zen-essential');
 | 
			
		||||
      if (tabToMove) {
 | 
			
		||||
        let tabSibling = targetTab.previousElementSibling;
 | 
			
		||||
        let isFirst = false;
 | 
			
		||||
        if (!tabSibling?.hasAttribute('zen-sync-id')) {
 | 
			
		||||
          isFirst = true;
 | 
			
		||||
        }
 | 
			
		||||
        gBrowser.zenHandleTabMove(tabToMove, () => {
 | 
			
		||||
          if (isFirst) {
 | 
			
		||||
            let container;
 | 
			
		||||
            if (isEssential) {
 | 
			
		||||
              container = gZenWorkspaces.getEssentialsSection(tabToMove);
 | 
			
		||||
            } else {
 | 
			
		||||
              const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId);
 | 
			
		||||
              container = tabToMove.pinned
 | 
			
		||||
                ? workspaceElement.pinnedTabsContainer
 | 
			
		||||
                : workspaceElement.tabsContainer;
 | 
			
		||||
            }
 | 
			
		||||
            container.insertBefore(tabToMove, container.firstChild);
 | 
			
		||||
          } else {
 | 
			
		||||
            let relativeTab = gZenWorkspaces.allStoredTabs.find((tab) => {
 | 
			
		||||
              return this.#getTabId(tab) === this.#getTabId(tabSibling);
 | 
			
		||||
            });
 | 
			
		||||
            if (relativeTab) {
 | 
			
		||||
              relativeTab.after(tabToMove);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #onTabOpen(event) {
 | 
			
		||||
      const targetTab = event.target;
 | 
			
		||||
      const isPinned = targetTab.pinned;
 | 
			
		||||
      const isEssential = isPinned && targetTab.hasAttribute('zen-essential');
 | 
			
		||||
      if (!this.#getTabId(targetTab) && !targetTab.hasAttribute('zen-empty-tab')) {
 | 
			
		||||
        const tabId = gZenUIManager.generateUuidv4();
 | 
			
		||||
        targetTab.setAttribute('zen-sync-id', tabId);
 | 
			
		||||
      }
 | 
			
		||||
      const duplicatedTab = gBrowser.addTrustedTab(targetTab.linkedBrowser.currentURI.spec, {
 | 
			
		||||
        createLazyBrowser: true,
 | 
			
		||||
        essential: isEssential,
 | 
			
		||||
        pinned: isPinned,
 | 
			
		||||
      });
 | 
			
		||||
      if (!isEssential) {
 | 
			
		||||
        gZenWorkspaces.moveTabToWorkspace(
 | 
			
		||||
          duplicatedTab,
 | 
			
		||||
          targetTab.getAttribute('zen-workspace-id')
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      duplicatedTab.setAttribute('zen-sync-id', targetTab.getAttribute('zen-sync-id'));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #onTabGroupCreate(event) {
 | 
			
		||||
      void event;
 | 
			
		||||
      //const targetGroup = event.target;
 | 
			
		||||
      //const isSplitView = targetGroup.classList.contains('zen-split-view');
 | 
			
		||||
      //const isFolder = targetGroup.isZenFolder;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  window.gZenWorkspaceWindowSync = new nsZenWorkspaceWindowSync();
 | 
			
		||||
}
 | 
			
		||||
@@ -932,7 +932,6 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature {
 | 
			
		||||
    await this.workspaceBookmarks();
 | 
			
		||||
    await this.initializeTabsStripSections();
 | 
			
		||||
    this._initializeEmptyTab();
 | 
			
		||||
    await gZenPinnedTabManager.refreshPinnedTabs({ init: true });
 | 
			
		||||
    await this.changeWorkspace(activeWorkspace, { onInit: true });
 | 
			
		||||
    this.#fixTabPositions();
 | 
			
		||||
    this.onWindowResize();
 | 
			
		||||
@@ -1471,11 +1470,6 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature {
 | 
			
		||||
        !tab.hasAttribute('zen-empty-tab') &&
 | 
			
		||||
        !tab.hasAttribute('zen-essential')
 | 
			
		||||
    );
 | 
			
		||||
    for (const tab of tabs) {
 | 
			
		||||
      if (tab.pinned) {
 | 
			
		||||
        await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    gBrowser.removeTabs(tabs, {
 | 
			
		||||
      animate: false,
 | 
			
		||||
      skipSessionStore: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,6 @@ export default [
 | 
			
		||||
  'ZenWorkspaceBookmarksStorage',
 | 
			
		||||
 | 
			
		||||
  'gZenPinnedTabManager',
 | 
			
		||||
  'ZenPinnedTabsStorage',
 | 
			
		||||
 | 
			
		||||
  'gZenEmojiPicker',
 | 
			
		||||
  'gZenSessionStore',
 | 
			
		||||
@@ -45,7 +44,6 @@ export default [
 | 
			
		||||
  'Cu',
 | 
			
		||||
  'Cc',
 | 
			
		||||
 | 
			
		||||
  'gZenActorsManager',
 | 
			
		||||
  'JSWindowActorParent',
 | 
			
		||||
  'JSWindowActorChild',
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user