diff --git a/locales/en-US/browser/browser/zen-workspaces.ftl b/locales/en-US/browser/browser/zen-workspaces.ftl
index 67f43f7fe6..9eea8c1be1 100644
--- a/locales/en-US/browser/browser/zen-workspaces.ftl
+++ b/locales/en-US/browser/browser/zen-workspaces.ftl
@@ -66,6 +66,10 @@ zen-panel-ui-gradient-click-to-add = Click to add a color
zen-workspace-creation-name =
.placeholder = Space Name
+zen-move-tab-to-workspace-button =
+ .label = Move To...
+ .tooltiptext = Move all tabs in this window to a Space
+
zen-workspaces-panel-context-reorder =
.label = Reorder Spaces
diff --git a/prefs/zen/session-store.yaml b/prefs/zen/session-store.yaml
new file mode 100644
index 0000000000..dc2e97d4c4
--- /dev/null
+++ b/prefs/zen/session-store.yaml
@@ -0,0 +1,9 @@
+# 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/.
+
+- name: zen.session-store.backup-file
+ value: true
+
+- name: zen.session-store.log
+ value: false
diff --git a/prefs/zen/window-sync.yaml b/prefs/zen/window-sync.yaml
new file mode 100644
index 0000000000..f3e30f29d3
--- /dev/null
+++ b/prefs/zen/window-sync.yaml
@@ -0,0 +1,9 @@
+# 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/.
+
+- name: zen.window-sync.enabled
+ value: true
+
+- name: zen.window-sync.log
+ value: false
diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml
index 47f083c49c..fb96936332 100644
--- a/src/browser/base/content/zen-assets.inc.xhtml
+++ b/src/browser/base/content/zen-assets.inc.xhtml
@@ -45,7 +45,6 @@
# Scripts used all over the browser
-
diff --git a/src/browser/base/content/zen-panels/popups.inc b/src/browser/base/content/zen-panels/popups.inc
index d94f76ae0c..b4ecd5e149 100644
--- a/src/browser/base/content/zen-panels/popups.inc
+++ b/src/browser/base/content/zen-panels/popups.inc
@@ -59,3 +59,8 @@
+
+
diff --git a/src/browser/base/content/zen-preloaded.inc.xhtml b/src/browser/base/content/zen-preloaded.inc.xhtml
index ab53540920..76f32fd5f5 100644
--- a/src/browser/base/content/zen-preloaded.inc.xhtml
+++ b/src/browser/base/content/zen-preloaded.inc.xhtml
@@ -6,7 +6,6 @@
# the window is fully loaded.
# Make sure they are loaded before the global-scripts.inc file.
-
diff --git a/src/browser/components/places/content/editBookmark-js.patch b/src/browser/components/places/content/editBookmark-js.patch
index 100ee182dd..92392184f5 100644
--- a/src/browser/components/places/content/editBookmark-js.patch
+++ b/src/browser/components/places/content/editBookmark-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/places/content/editBookmark.js b/browser/components/places/content/editBookmark.js
-index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4016594b9 100644
+index f562f19741d882d92365da531b55e2810a0e79ea..a68ce8191314845c589f3a9f14b56028e0532628 100644
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -387,6 +387,10 @@ var gEditItemOverlay = {
@@ -31,34 +31,11 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4
}
break;
}
-@@ -1280,6 +1288,148 @@ var gEditItemOverlay = {
+@@ -1280,6 +1288,128 @@ var gEditItemOverlay = {
get bookmarkState() {
return this._bookmarkState;
},
+
-+ async _initWorkspaceSelector() {
-+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
-+ return;
-+ }
-+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces();
-+
-+ const selectElement = this._workspaceSelect;
-+
-+ // Clear any existing options
-+ while (selectElement.firstChild) {
-+ selectElement.removeChild(selectElement.firstChild);
-+ }
-+
-+ // For each workspace, create an option element
-+ for (let workspace of this._workspaces) {
-+ const option = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
-+ option.textContent = workspace.name;
-+ option.value = workspace.uuid;
-+ selectElement.appendChild(option);
-+ }
-+
-+ selectElement.disabled = this.readOnly;
-+ },
+ async onWorkspaceSelectionChange(event) {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
@@ -129,7 +106,10 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
-+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces();
++ const { ZenSessionStore } = ChromeUtils.importESModule(
++ "resource:///modules/zen/ZenSessionManager.sys.mjs"
++ );
++ this._workspaces = ZenSessionStore.getClonedSpaces();
+ const workspaceList = this._workspaceList;
+ if(aInfo.node?.bookmarkGuid) {
+ this._selectedWorkspaces = await ZenWorkspaceBookmarksStorage.getBookmarkWorkspaces(aInfo.node.bookmarkGuid);
@@ -180,7 +160,7 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4
};
ChromeUtils.defineLazyGetter(gEditItemOverlay, "_folderTree", () => {
-@@ -1318,6 +1468,9 @@ for (let elt of [
+@@ -1318,6 +1448,9 @@ for (let elt of [
"locationField",
"keywordField",
"tagsField",
diff --git a/src/browser/components/sessionstore/SessionFile-sys-mjs.patch b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch
new file mode 100644
index 0000000000..895c4313fa
--- /dev/null
+++ b/src/browser/components/sessionstore/SessionFile-sys-mjs.patch
@@ -0,0 +1,21 @@
+diff --git a/browser/components/sessionstore/SessionFile.sys.mjs b/browser/components/sessionstore/SessionFile.sys.mjs
+index 31140cb8be3b529a0952ca8dc55165690b0e2120..605c9e0aa84da0a2d3171a0573e8cd95e27bd0c4 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";
+@@ -380,7 +381,7 @@ var SessionFileInternal = {
+ this._readOrigin = result.origin;
+
+ result.noFilesFound = noFilesFound;
+-
++ await lazy.ZenSessionStore.readFile();
+ return result;
+ },
+
diff --git a/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch
new file mode 100644
index 0000000000..e6956ea61b
--- /dev/null
+++ b/src/browser/components/sessionstore/SessionSaver-sys-mjs.patch
@@ -0,0 +1,20 @@
+diff --git a/browser/components/sessionstore/SessionSaver.sys.mjs b/browser/components/sessionstore/SessionSaver.sys.mjs
+index 9141793550f7c7ff6aa63d4c85bf571b4499e2d0..f00314ebf75ac826e1c9cca8af264ff8aae106c0 100644
+--- a/browser/components/sessionstore/SessionSaver.sys.mjs
++++ b/browser/components/sessionstore/SessionSaver.sys.mjs
+@@ -20,6 +20,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
+ SessionFile: "resource:///modules/sessionstore/SessionFile.sys.mjs",
+ SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
+ });
+
+ /*
+@@ -305,6 +306,7 @@ var SessionSaverInternal = {
+ this._maybeClearCookiesAndStorage(state);
+
+ Glean.sessionRestore.collectData.stopAndAccumulate(timerId);
++ lazy.ZenSessionStore.saveState(state);
+ return this._writeState(state);
+ },
+
diff --git a/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch
new file mode 100644
index 0000000000..b106193cf3
--- /dev/null
+++ b/src/browser/components/sessionstore/SessionStartup-sys-mjs.patch
@@ -0,0 +1,21 @@
+diff --git a/browser/components/sessionstore/SessionStartup.sys.mjs b/browser/components/sessionstore/SessionStartup.sys.mjs
+index be23213ae9ec7e59358a17276c6c3764d38d9996..ca5a8ccc916ceeab5140f1278d15233cefbe5815 100644
+--- a/browser/components/sessionstore/SessionStartup.sys.mjs
++++ b/browser/components/sessionstore/SessionStartup.sys.mjs
+@@ -40,6 +40,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
+ StartupPerformance:
+ "resource:///modules/sessionstore/StartupPerformance.sys.mjs",
+ sessionStoreLogger: "resource:///modules/sessionstore/SessionLogger.sys.mjs",
++ ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
+ });
+
+ const STATE_RUNNING_STR = "running";
+@@ -179,6 +180,8 @@ export var SessionStartup = {
+ this._initialState = parsed;
+ }
+
++ lazy.ZenSessionStore.onFileRead(this._initialState);
++
+ if (this._initialState == null) {
+ // No valid session found.
+ this._sessionType = this.NO_SESSION;
diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
index 7efaefb173..c8cbd23a32 100644
--- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
+++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
-index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb22176c60c4e 100644
+index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e77416a2ab48 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -127,6 +127,8 @@ const TAB_EVENTS = [
@@ -11,7 +11,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
];
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-@@ -1911,6 +1913,8 @@ var SessionStoreInternal = {
+@@ -196,6 +198,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", () => {
+@@ -1911,6 +1914,8 @@ var SessionStoreInternal = {
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
@@ -20,19 +28,68 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
-@@ -2384,11 +2388,9 @@ var SessionStoreInternal = {
- tabbrowser.selectedTab.label;
+@@ -2020,6 +2025,10 @@ var SessionStoreInternal = {
+ this._windows[aWindow.__SSi].isTaskbarTab = true;
+ }
+
++ if (aWindow.document.documentElement.hasAttribute("zen-unsynced-window")) {
++ this._windows[aWindow.__SSi].isZenUnsynced = true;
++ }
++
+ let tabbrowser = aWindow.gBrowser;
+
+ // add tab change listeners to all already existing tabs
+@@ -2151,7 +2160,6 @@ var SessionStoreInternal = {
+ if (closedWindowState) {
+ let newWindowState;
+ if (
+- AppConstants.platform == "macosx" ||
+ !lazy.SessionStartup.willRestore()
+ ) {
+ // We want to split the window up into pinned tabs and unpinned tabs.
+@@ -2215,6 +2223,15 @@ var SessionStoreInternal = {
+ });
+ this._shouldRestoreLastSession = false;
+ }
++ else if (!aInitialState && isRegularWindow) {
++ let windowPromises = [];
++ for (let window of this._browserWindows) {
++ windowPromises.push(lazy.TabStateFlusher.flushWindow(window));
++ }
++ Promise.all(windowPromises).finally(() => {
++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this);
++ });
++ }
+
+ if (this._restoreLastWindow && aWindow.toolbar.visible) {
+ // always reset (if not a popup window)
+@@ -2465,7 +2482,7 @@ var SessionStoreInternal = {
+ // 2) Flush the window.
+ // 3) When the flush is complete, revisit our decision to store the window
+ // in _closedWindows, and add/remove as necessary.
+- if (!winData.isPrivate && !winData.isTaskbarTab) {
++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
+ this.maybeSaveClosedWindow(winData, isLastWindow);
}
-- if (AppConstants.platform != "macosx") {
- // Until we decide otherwise elsewhere, this window is part of a series
- // of closing windows to quit.
- winData._shouldRestore = true;
-- }
+@@ -2486,7 +2503,7 @@ var SessionStoreInternal = {
- // Store the window's close date to figure out when each individual tab
- // was closed. This timestamp should allow re-arranging data based on how
-@@ -3373,7 +3375,7 @@ var SessionStoreInternal = {
+ // Save non-private windows if they have at
+ // least one saveable tab or are the last window.
+- if (!winData.isPrivate && !winData.isTaskbarTab) {
++ if (!winData.isPrivate && !winData.isTaskbarTab && !winData.isZenUnsynced) {
+ this.maybeSaveClosedWindow(winData, isLastWindow);
+
+ if (!isLastWindow && winData.closedId > -1) {
+@@ -2582,6 +2599,7 @@ var SessionStoreInternal = {
+ let alreadyStored = winIndex != -1;
+ // If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
+ let shouldStore = hasSaveableTabs || isLastWindow;
++ lazy.ZenSessionStore.maybeSaveClosedWindow(winData, isLastWindow);
+
+ if (shouldStore && !alreadyStored) {
+ let index = this._closedWindows.findIndex(win => {
+@@ -3373,7 +3391,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -41,12 +98,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
return;
}
-@@ -4089,6 +4091,12 @@ var SessionStoreInternal = {
+@@ -4089,6 +4107,12 @@ var SessionStoreInternal = {
Math.min(tabState.index, tabState.entries.length)
);
tabState.pinned = false;
+ tabState.zenEssential = false;
-+ tabState.zenPinnedId = null;
++ tabState.zenSyncId = null;
+ tabState.zenIsGlance = false;
+ tabState.zenGlanceId = null;
+ tabState.zenHasStaticLabel = false;
@@ -54,7 +111,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
-@@ -4525,6 +4533,7 @@ var SessionStoreInternal = {
+@@ -4525,6 +4549,7 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -62,7 +119,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
-@@ -5374,7 +5383,7 @@ var SessionStoreInternal = {
+@@ -5374,7 +5399,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -71,16 +128,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
removableTabs.push(tab);
}
}
-@@ -5434,7 +5443,7 @@ var SessionStoreInternal = {
- }
+@@ -5483,7 +5508,7 @@ var SessionStoreInternal = {
- let workspaceID = aWindow.getWorkspaceID();
-- if (workspaceID) {
-+ if (workspaceID && !(this.isLastRestorableWindow() && AppConstants.platform == "macosx")) {
- winData.workspaceID = workspaceID;
- }
- },
-@@ -5625,11 +5634,12 @@ var SessionStoreInternal = {
+ // collect the data for all windows
+ for (ix in this._windows) {
+- if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab) {
++ if (this._windows[ix]._restoring || this._windows[ix].isTaskbarTab || this._windows[ix].isZenUnsynced) {
+ // window data is still in _statesToRestore
+ continue;
+ }
+@@ -5625,11 +5650,16 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -90,19 +147,15 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
let winData = this._windows[aWindow.__SSi];
let tabsData = (winData.tabs = []);
++ winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null;
+ winData.splitViewData = aWindow.gZenViewSplitter?.storeDataForSessionStore();
++ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
++ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces();
++
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
-@@ -5640,6 +5650,7 @@ var SessionStoreInternal = {
- tabsData.push(tabData);
- }
-
-+ winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || [];
- // update tab group state for this window
- winData.groups = [];
- for (let tabGroup of aWindow.gBrowser.tabGroups) {
-@@ -5652,7 +5663,7 @@ var SessionStoreInternal = {
+@@ -5652,7 +5682,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) {
@@ -111,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
-@@ -5765,8 +5776,8 @@ var SessionStoreInternal = {
+@@ -5765,8 +5795,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -122,16 +175,17 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
selectTab = Math.min(selectTab, winData.tabs.length);
}
-@@ -5809,6 +5820,8 @@ var SessionStoreInternal = {
+@@ -5809,6 +5839,9 @@ var SessionStoreInternal = {
winData.tabs,
winData.groups ?? []
);
+ aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders);
+ aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData);
++ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData);
this._log.debug(
`restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
);
-@@ -6372,6 +6385,25 @@ var SessionStoreInternal = {
+@@ -6372,6 +6405,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -145,8 +199,8 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
+ 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("id", tabData.zenSyncId);
+ }
+ if (tabData.zenDefaultUserContextId) {
+ tab.setAttribute("zenDefaultUserContextId", true);
@@ -157,7 +211,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (tabData.pinned) {
tabbrowser.pinTab(tab);
-@@ -7290,7 +7322,7 @@ var SessionStoreInternal = {
+@@ -7290,7 +7342,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -166,7 +220,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
-@@ -7305,7 +7337,7 @@ var SessionStoreInternal = {
+@@ -7305,7 +7357,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;
diff --git a/src/browser/components/sessionstore/TabState-sys-mjs.patch b/src/browser/components/sessionstore/TabState-sys-mjs.patch
index 2100e23343..450c981c32 100644
--- a/src/browser/components/sessionstore/TabState-sys-mjs.patch
+++ b/src/browser/components/sessionstore/TabState-sys-mjs.patch
@@ -1,13 +1,13 @@
diff --git a/browser/components/sessionstore/TabState.sys.mjs b/browser/components/sessionstore/TabState.sys.mjs
-index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a12f2060b 100644
+index 82721356d191055bec0d4b0ca49e481221988801..80547ec951f881bef134b637730954eb1525c623 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
-@@ -85,7 +85,22 @@ class _TabState {
+@@ -85,7 +85,24 @@ class _TabState {
tabData.groupId = tab.group.id;
}
+ tabData.zenWorkspace = tab.getAttribute("zen-workspace-id");
-+ tabData.zenPinnedId = tab.getAttribute("zen-pin-id");
++ tabData.zenSyncId = tab.getAttribute("id");
+ tabData.zenEssential = tab.getAttribute("zen-essential");
+ tabData.pinned = tabData.pinned || tabData.zenEssential;
+ tabData.zenDefaultUserContextId = tab.getAttribute("zenDefaultUserContextId");
@@ -17,6 +17,8 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a
+ tabData.zenHasStaticLabel = tab.hasAttribute("zen-has-static-label");
+ tabData.zenGlanceId = tab.getAttribute("glance-id");
+ tabData.zenIsGlance = tab.hasAttribute("zen-glance-tab");
++ tabData._zenPinnedInitialState = tab._zenPinnedInitialState;
++ tabData._zenIsActiveTab = tab._zenContentsVisible;
+
tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true);
+ if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) {
@@ -25,3 +27,12 @@ index 82721356d191055bec0d4b0ca49e481221988801..1ea5c394c704da295149443d7794961a
tabData.userContextId = tab.userContextId || 0;
+@@ -98,7 +115,7 @@ class _TabState {
+
+ // Copy data from the tab state cache only if the tab has fully finished
+ // restoring. We don't want to overwrite data contained in __SS_data.
+- this.copyFromCache(browser.permanentKey, tabData, options);
++ this.copyFromCache(tab.permanentKey, tabData, options);
+
+ // After copyFromCache() was called we check for properties that are kept
+ // in the cache only while the tab is pending or restoring. Once that
diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch
index 3f27154b7f..dd0edd05f5 100644
--- a/src/browser/components/tabbrowser/content/tab-js.patch
+++ b/src/browser/components/tabbrowser/content/tab-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
-index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbfd6214f77 100644
+index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..23b134a8ba674978182415a8bac926f7600f564a 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@@ -121,15 +121,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf
on_click(event) {
if (event.button != 0) {
return;
-@@ -575,6 +599,7 @@
- )
- );
- } else {
-+ gZenPinnedTabManager._removePinnedAttributes(this, true);
- gBrowser.removeTab(this, {
- animate: true,
- triggeringEvent: event,
-@@ -587,6 +612,14 @@
+@@ -587,6 +611,14 @@
// (see tabbrowser-tabs 'click' handler).
gBrowser.tabContainer._blockDblClick = true;
}
@@ -144,7 +136,7 @@ index 2dacb325190b6ae42ebeb3e9f0e862dc690ecdca..02d70e9b0261f92917d274759838cfbf
}
on_dblclick(event) {
-@@ -610,6 +643,8 @@
+@@ -610,6 +642,8 @@
animate: true,
triggeringEvent: event,
});
diff --git a/src/browser/components/tabbrowser/content/tabbrowser-js.patch b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
index 8a71eaaae6..7327740e86 100644
--- a/src/browser/components/tabbrowser/content/tabbrowser-js.patch
+++ b/src/browser/components/tabbrowser/content/tabbrowser-js.patch
@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
-index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f53bc059b 100644
+index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..ebf699804db90e5509d1b1b704c95f18e829ca07 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -386,6 +386,7 @@
@@ -10,7 +10,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
const browsers = [];
if (this.#activeSplitView) {
for (const tab of this.#activeSplitView.tabs) {
-@@ -450,15 +451,64 @@
+@@ -450,15 +451,66 @@
return this.tabContainer.visibleTabs;
}
@@ -18,6 +18,8 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
+ return this.#handleTabMove(...args);
+ }
+
++ get zenTabProgressListener() { return TabProgressListener; }
++
+ get _numVisiblePinTabsWithoutCollapsed() {
+ let i = 0;
+ for (let item of this.tabContainer.ariaFocusableItems) {
@@ -77,7 +79,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
set selectedTab(val) {
if (
gSharedTabWarning.willShowSharedTabWarning(val) ||
-@@ -613,6 +663,7 @@
+@@ -613,6 +665,7 @@
this.tabpanels.appendChild(panel);
let tab = this.tabs[0];
@@ -85,7 +87,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.linkedPanel = uniqueId;
this._selectedTab = tab;
this._selectedBrowser = browser;
-@@ -898,13 +949,17 @@
+@@ -898,13 +951,17 @@
}
this.showTab(aTab);
@@ -104,7 +106,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.setAttribute("pinned", "true");
this._updateTabBarForPinnedTabs();
-@@ -917,11 +972,15 @@
+@@ -917,11 +974,15 @@
}
this.#handleTabMove(aTab, () => {
@@ -121,7 +123,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
aTab.style.marginInlineStart = "";
-@@ -1098,6 +1157,8 @@
+@@ -1098,6 +1159,8 @@
let LOCAL_PROTOCOLS = ["chrome:", "about:", "resource:", "data:"];
@@ -130,7 +132,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
aIconURL &&
!LOCAL_PROTOCOLS.some(protocol => aIconURL.startsWith(protocol))
-@@ -1107,6 +1168,9 @@
+@@ -1107,6 +1170,9 @@
);
return;
}
@@ -140,7 +142,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let browser = this.getBrowserForTab(aTab);
browser.mIconURL = aIconURL;
-@@ -1379,7 +1443,6 @@
+@@ -1379,7 +1445,6 @@
// Preview mode should not reset the owner
if (!this._previewMode && !oldTab.selected) {
@@ -148,7 +150,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
-@@ -1470,6 +1533,7 @@
+@@ -1470,6 +1535,7 @@
if (!this._previewMode) {
newTab.recordTimeFromUnloadToReload();
newTab.updateLastAccessed();
@@ -156,7 +158,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
oldTab.updateLastAccessed();
// if this is the foreground window, update the last-seen timestamps.
if (this.ownerGlobal == BrowserWindowTracker.getTopWindow()) {
-@@ -1622,6 +1686,9 @@
+@@ -1622,6 +1688,9 @@
}
let activeEl = document.activeElement;
@@ -166,17 +168,20 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If focus is on the old tab, move it to the new tab.
if (activeEl == oldTab) {
newTab.focus();
-@@ -1945,7 +2012,8 @@
+@@ -1945,7 +2014,11 @@
}
_setTabLabel(aTab, aLabel, { beforeTabOpen, isContentTitle, isURL } = {}) {
- if (!aLabel || aLabel.includes("about:reader?")) {
++ if (!aTab._zenContentsVisible && !aTab._zenChangeLabelFlag && !aTab._labelIsInitialTitle && !gZenWorkspaces.privateWindowOrDisabled) {
++ return false;
++ }
+ gZenPinnedTabManager.onTabLabelChanged(aTab);
-+ if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) {
++ if (!aLabel || aLabel.includes("about:reader?") || (aTab.hasAttribute("zen-has-static-label") && !aTab._zenChangeLabelFlag)) {
return false;
}
-@@ -2053,7 +2121,7 @@
+@@ -2053,7 +2126,7 @@
newIndex = this.selectedTab._tPos + 1;
}
@@ -185,7 +190,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (this.isTabGroupLabel(targetTab)) {
throw new Error(
"Replacing a tab group label with a tab is not supported"
-@@ -2328,6 +2396,7 @@
+@@ -2328,6 +2401,7 @@
uriIsAboutBlank,
userContextId,
skipLoad,
@@ -193,7 +198,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} = {}) {
let b = document.createXULElement("browser");
// Use the JSM global to create the permanentKey, so that if the
-@@ -2401,8 +2470,7 @@
+@@ -2401,8 +2475,7 @@
// we use a different attribute name for this?
b.setAttribute("name", name);
}
@@ -203,7 +208,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
b.setAttribute("transparent", "true");
}
-@@ -2567,7 +2635,7 @@
+@@ -2567,7 +2640,7 @@
let panel = this.getPanel(browser);
let uniqueId = this._generateUniquePanelID();
@@ -212,7 +217,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.linkedPanel = uniqueId;
// Inject the into the DOM if necessary.
-@@ -2626,8 +2694,8 @@
+@@ -2626,8 +2699,8 @@
// If we transitioned from one browser to two browsers, we need to set
// hasSiblings=false on both the existing browser and the new browser.
if (this.tabs.length == 2) {
@@ -223,7 +228,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
aTab.linkedBrowser.browsingContext.hasSiblings = this.tabs.length > 1;
}
-@@ -2814,7 +2882,6 @@
+@@ -2814,7 +2887,6 @@
this.selectedTab = this.addTrustedTab(BROWSER_NEW_TAB_URL, {
tabIndex: tab._tPos + 1,
userContextId: tab.userContextId,
@@ -231,16 +236,17 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
focusUrlBar: true,
});
resolve(this.selectedBrowser);
-@@ -2923,6 +2990,8 @@
+@@ -2923,6 +2995,9 @@
schemelessInput,
hasValidUserGestureActivation = false,
textDirectiveUserActivation = false,
+ _forZenEmptyTab,
+ essential,
++ zenWorkspaceId,
} = {}
) {
// all callers of addTab that pass a params object need to pass
-@@ -2933,10 +3002,17 @@
+@@ -2933,10 +3008,17 @@
);
}
@@ -258,7 +264,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If we're opening a foreground tab, set the owner by default.
ownerTab ??= inBackground ? null : this.selectedTab;
-@@ -2944,6 +3020,7 @@
+@@ -2944,6 +3026,7 @@
if (this.selectedTab.owner) {
this.selectedTab.owner = null;
}
@@ -266,14 +272,16 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Find the tab that opened this one, if any. This is used for
// determining positioning, and inherited attributes such as the
-@@ -2996,6 +3073,19 @@
+@@ -2996,6 +3079,21 @@
noInitialLabel,
skipBackgroundNotify,
});
+ if (hasZenDefaultUserContextId) {
+ t.setAttribute("zenDefaultUserContextId", "true");
+ }
-+ if (zenForcedWorkspaceId !== undefined) {
++ if (zenWorkspaceId) {
++ t.setAttribute("zen-workspace-id", zenWorkspaceId);
++ } else if (zenForcedWorkspaceId !== undefined) {
+ t.setAttribute("zen-workspace-id", zenForcedWorkspaceId);
+ t.setAttribute("change-workspace", "")
+ }
@@ -286,7 +294,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (insertTab) {
// Insert the tab into the tab container in the correct position.
this.#insertTabAtIndex(t, {
-@@ -3004,6 +3094,7 @@
+@@ -3004,6 +3102,7 @@
ownerTab,
openerTab,
pinned,
@@ -294,7 +302,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
bulkOrderedOpen,
tabGroup: tabGroup ?? openerTab?.group,
});
-@@ -3022,6 +3113,7 @@
+@@ -3022,6 +3121,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -302,7 +310,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}));
if (focusUrlBar) {
-@@ -3146,6 +3238,12 @@
+@@ -3146,6 +3246,12 @@
}
}
@@ -315,7 +323,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Additionally send pinned tab events
if (pinned) {
this.#notifyPinnedStatus(t);
-@@ -3349,10 +3447,10 @@
+@@ -3349,10 +3455,10 @@
isAdoptingGroup = false,
isUserTriggered = false,
telemetryUserCreateSource = "unknown",
@@ -327,7 +335,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
if (!color) {
-@@ -3373,9 +3471,14 @@
+@@ -3373,9 +3479,14 @@
label,
isAdoptingGroup
);
@@ -344,7 +352,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
group.addTabs(tabs);
-@@ -3496,7 +3599,7 @@
+@@ -3496,7 +3607,7 @@
}
this.#handleTabMove(tab, () =>
@@ -353,7 +361,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
}
-@@ -3698,6 +3801,7 @@
+@@ -3698,6 +3809,7 @@
openWindowInfo,
skipLoad,
triggeringRemoteType,
@@ -361,7 +369,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
) {
// If we don't have a preferred remote type (or it is `NOT_REMOTE`), and
-@@ -3767,6 +3871,7 @@
+@@ -3767,6 +3879,7 @@
openWindowInfo,
name,
skipLoad,
@@ -369,7 +377,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
}
-@@ -3955,7 +4060,7 @@
+@@ -3955,7 +4068,7 @@
// Add a new tab if needed.
if (!tab) {
let createLazyBrowser =
@@ -378,7 +386,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let url = "about:blank";
if (tabData.entries?.length) {
-@@ -3992,8 +4097,10 @@
+@@ -3992,8 +4105,10 @@
insertTab: false,
skipLoad: true,
preferredRemoteType,
@@ -390,7 +398,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (select) {
tabToSelect = tab;
}
-@@ -4005,7 +4112,8 @@
+@@ -4005,7 +4120,8 @@
this.pinTab(tab);
// Then ensure all the tab open/pinning information is sent.
this._fireTabOpen(tab, {});
@@ -400,7 +408,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
let { groupId } = tabData;
const tabGroup = tabGroupWorkingData.get(groupId);
// if a tab refers to a tab group we don't know, skip any group
-@@ -4019,7 +4127,10 @@
+@@ -4019,7 +4135,10 @@
tabGroup.stateData.id,
tabGroup.stateData.color,
tabGroup.stateData.collapsed,
@@ -412,7 +420,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
tabsFragment.appendChild(tabGroup.node);
}
-@@ -4064,9 +4175,23 @@
+@@ -4064,9 +4183,23 @@
// to remove the old selected tab.
if (tabToSelect) {
let leftoverTab = this.selectedTab;
@@ -428,15 +436,15 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
+ gZenWorkspaces._initialTab._shouldRemove = true;
+ }
+ }
-+ }
+ }
+ else {
+ gZenWorkspaces._tabToRemoveForEmpty = this.selectedTab;
- }
++ }
+ this._hasAlreadyInitializedZenSessionStore = true;
if (tabs.length > 1 || !tabs[0].selected) {
this._updateTabsAfterInsert();
-@@ -4257,11 +4382,14 @@
+@@ -4257,11 +4390,14 @@
if (ownerTab) {
tab.owner = ownerTab;
}
@@ -452,7 +460,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
!bulkOrderedOpen &&
((openerTab &&
-@@ -4273,7 +4401,7 @@
+@@ -4273,7 +4409,7 @@
let lastRelatedTab =
openerTab && this._lastRelatedTabMap.get(openerTab);
let previousTab = lastRelatedTab || openerTab || this.selectedTab;
@@ -461,7 +469,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tabGroup = previousTab.group;
}
if (
-@@ -4284,7 +4412,7 @@
+@@ -4284,7 +4420,7 @@
) {
elementIndex = Infinity;
} else if (previousTab.visible) {
@@ -470,7 +478,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (previousTab == FirefoxViewHandler.tab) {
elementIndex = 0;
}
-@@ -4312,14 +4440,14 @@
+@@ -4312,14 +4448,14 @@
}
// Ensure index is within bounds.
if (tab.pinned) {
@@ -489,7 +497,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (pinned && !itemAfter?.pinned) {
itemAfter = null;
-@@ -4330,7 +4458,7 @@
+@@ -4330,7 +4466,7 @@
this.tabContainer._invalidateCachedTabs();
@@ -498,7 +506,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (this.isTab(itemAfter) && itemAfter.group == tabGroup) {
// Place at the front of, or between tabs in, the same tab group
this.tabContainer.insertBefore(tab, itemAfter);
-@@ -4358,7 +4486,11 @@
+@@ -4358,7 +4494,11 @@
const tabContainer = pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -510,7 +518,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
this._updateTabsAfterInsert();
-@@ -4366,6 +4498,7 @@
+@@ -4366,6 +4506,7 @@
if (pinned) {
this._updateTabBarForPinnedTabs();
}
@@ -518,17 +526,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
TabBarVisibility.update();
}
-@@ -4655,6 +4788,9 @@
- return;
- }
-
-+ for (let tab of selectedTabs) {
-+ gZenPinnedTabManager._removePinnedAttributes(tab, true);
-+ }
- this.removeTabs(selectedTabs, { isUserTriggered, telemetrySource });
- }
-
-@@ -4916,6 +5052,7 @@
+@@ -4916,6 +5057,7 @@
telemetrySource,
} = {}
) {
@@ -536,7 +534,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
// can be considered equivalent to closing the window.
if (
-@@ -5005,6 +5142,7 @@
+@@ -5005,6 +5147,7 @@
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
@@ -544,7 +542,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} catch (e) {
console.error(e);
}
-@@ -5043,6 +5181,12 @@
+@@ -5043,6 +5186,12 @@
aTab._closeTimeNoAnimTimerId = Glean.browserTabclose.timeNoAnim.start();
}
@@ -557,7 +555,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Handle requests for synchronously removing an already
// asynchronously closing tab.
if (!animate && aTab.closing) {
-@@ -5057,6 +5201,9 @@
+@@ -5057,6 +5206,9 @@
// state).
let tabWidth = window.windowUtils.getBoundsWithoutFlushing(aTab).width;
let isLastTab = this.#isLastTabInWindow(aTab);
@@ -567,7 +565,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (
!this._beginRemoveTab(aTab, {
closeWindowFastpath: true,
-@@ -5105,7 +5252,13 @@
+@@ -5105,7 +5257,13 @@
// We're not animating, so we can cancel the animation stopwatch.
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
aTab._closeTimeAnimTimerId = null;
@@ -582,7 +580,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
-@@ -5239,7 +5392,7 @@
+@@ -5239,7 +5397,7 @@
closeWindowWithLastTab != null
? closeWindowWithLastTab
: !window.toolbar.visible ||
@@ -591,7 +589,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (closeWindow) {
// We've already called beforeunload on all the relevant tabs if we get here,
-@@ -5263,6 +5416,7 @@
+@@ -5263,6 +5421,7 @@
newTab = true;
}
@@ -599,7 +597,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab._endRemoveArgs = [closeWindow, newTab];
// swapBrowsersAndCloseOther will take care of closing the window without animation.
-@@ -5303,13 +5457,7 @@
+@@ -5303,13 +5462,7 @@
aTab._mouseleave();
if (newTab) {
@@ -614,7 +612,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
TabBarVisibility.update();
}
-@@ -5442,6 +5590,7 @@
+@@ -5442,6 +5595,7 @@
this.tabs[i]._tPos = i;
}
@@ -622,7 +620,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (!this._windowIsClosing) {
// update tab close buttons state
this.tabContainer._updateCloseButtons();
-@@ -5663,6 +5812,7 @@
+@@ -5663,6 +5817,7 @@
}
let excludeTabs = new Set(aExcludeTabs);
@@ -630,7 +628,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// If this tab has a successor, it should be selectable, since
// hiding or closing a tab removes that tab as a successor.
-@@ -5675,13 +5825,13 @@
+@@ -5675,13 +5830,13 @@
!excludeTabs.has(aTab.owner) &&
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
) {
@@ -646,7 +644,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
);
let tab = this.tabContainer.findNextTab(aTab, {
-@@ -5697,7 +5847,7 @@
+@@ -5697,7 +5852,7 @@
}
if (tab) {
@@ -655,7 +653,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
// If no qualifying visible tab was found, see if there is a tab in
-@@ -5718,7 +5868,7 @@
+@@ -5718,7 +5873,7 @@
});
}
@@ -664,7 +662,47 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
_blurTab(aTab) {
-@@ -6124,10 +6274,10 @@
+@@ -5729,7 +5884,7 @@
+ * @returns {boolean}
+ * False if swapping isn't permitted, true otherwise.
+ */
+- swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
++ swapBrowsersAndCloseOther(aOurTab, aOtherTab, zenCloseOther = true) {
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (
+@@ -5783,6 +5938,7 @@
+ // fire the beforeunload event in the process. Close the other
+ // window if this was its last tab.
+ if (
++ zenCloseOther &&
+ !remoteBrowser._beginRemoveTab(aOtherTab, {
+ adoptedByTab: aOurTab,
+ closeWindowWithLastTab: true,
+@@ -5794,7 +5950,7 @@
+ // If this is the last tab of the window, hide the window
+ // immediately without animation before the docshell swap, to avoid
+ // about:blank being painted.
+- let [closeWindow] = aOtherTab._endRemoveArgs;
++ let [closeWindow] = !zenCloseOther ? [false] : aOtherTab._endRemoveArgs;
+ if (closeWindow) {
+ let win = aOtherTab.ownerGlobal;
+ win.windowUtils.suppressAnimation(true);
+@@ -5918,11 +6074,13 @@
+ }
+
+ // Finish tearing down the tab that's going away.
++ if (zenCloseOther) {
+ if (closeWindow) {
+ aOtherTab.ownerGlobal.close();
+ } else {
+ remoteBrowser._endRemoveTab(aOtherTab);
+ }
++ }
+
+ this.setTabTitle(aOurTab);
+
+@@ -6124,10 +6282,10 @@
SessionStore.deleteCustomTabValue(aTab, "hiddenBy");
}
@@ -677,15 +715,33 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
-@@ -6186,6 +6336,7 @@
+@@ -6185,7 +6343,8 @@
+ *
* @param {MozTabbrowserTab|MozTabbrowserTabGroup|MozTabbrowserTabGroup.labelElement} aTab
*/
- replaceTabWithWindow(aTab, aOptions) {
+- replaceTabWithWindow(aTab, aOptions) {
++ replaceTabWithWindow(aTab, aOptions, zenForceSync = false) {
+ if (!this.isTab(aTab)) return; // TODO: Handle tab groups
if (this.tabs.length == 1) {
return null;
}
-@@ -6319,7 +6470,7 @@
+@@ -6209,12 +6368,14 @@
+ }
+
+ // tell a new window to take the "dropped" tab
+- return window.openDialog(
++ let win = window.openDialog(
+ AppConstants.BROWSER_CHROME_URL,
+ "_blank",
+ options,
+ aTab
+ );
++ win._zenStartupSyncFlag = zenForceSync ? 'synced' : 'unsynced';
++ return win;
+ }
+
+ /**
+@@ -6319,7 +6480,7 @@
* `true` if element is a ``
*/
isTabGroup(element) {
@@ -694,7 +750,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
/**
-@@ -6404,8 +6555,8 @@
+@@ -6404,8 +6565,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -705,7 +761,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
-@@ -6431,10 +6582,16 @@
+@@ -6431,10 +6592,16 @@
this.#handleTabMove(
element,
() => {
@@ -724,7 +780,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
-@@ -6492,23 +6649,28 @@
+@@ -6492,23 +6659,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -759,7 +815,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (!element.pinned && targetElement && targetElement.pinned) {
// If the caller asks to move an unpinned element next to a pinned
// tab, move the unpinned element to be the first unpinned element
-@@ -6521,14 +6683,34 @@
+@@ -6521,14 +6693,34 @@
// move the tab group right before the first unpinned tab.
// 4. Moving a tab group and the first unpinned tab is grouped:
// move the tab group right before the first unpinned tab's tab group.
@@ -795,7 +851,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
-@@ -6537,7 +6719,7 @@
+@@ -6537,7 +6729,7 @@
element,
() => {
if (moveBefore) {
@@ -804,7 +860,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (targetElement) {
targetElement.after(element);
} else {
-@@ -6607,10 +6789,10 @@
+@@ -6607,10 +6799,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -817,7 +873,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
-@@ -6656,6 +6838,7 @@
+@@ -6656,6 +6848,7 @@
let state = {
tabIndex: tab._tPos,
@@ -825,7 +881,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
-@@ -6682,7 +6865,7 @@
+@@ -6682,7 +6875,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -834,7 +890,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
-@@ -6723,6 +6906,10 @@
+@@ -6723,6 +6916,10 @@
moveActionCallback();
@@ -845,7 +901,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Clear tabs cache after moving nodes because the order of tabs may have
// changed.
this.tabContainer._invalidateCachedTabs();
-@@ -7623,7 +7810,7 @@
+@@ -7623,7 +7820,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -854,7 +910,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
window.focus();
aEvent.preventDefault();
break;
-@@ -7640,7 +7827,6 @@
+@@ -7640,7 +7837,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -862,7 +918,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
break;
case "TabGroupCreateByUser":
-@@ -8589,6 +8775,7 @@
+@@ -8589,6 +8785,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -870,7 +926,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
-@@ -9623,7 +9810,7 @@ var TabContextMenu = {
+@@ -9623,7 +9820,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
@@ -879,11 +935,3 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
// Build Ask Chat items
TabContextMenu.GenAI.buildTabMenu(
document.getElementById("context_askChat"),
-@@ -9943,6 +10130,7 @@ var TabContextMenu = {
- )
- );
- } else {
-+ gZenPinnedTabManager._removePinnedAttributes(this.contextTab, true);
- gBrowser.removeTab(this.contextTab, {
- animate: true,
- ...gBrowser.TabMetrics.userTriggeredContext(
diff --git a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch b/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch
deleted file mode 100644
index 31608dbe08..0000000000
--- a/src/browser/components/urlbar/UrlbarProviderPlaces-sys-mjs.patch
+++ /dev/null
@@ -1,44 +0,0 @@
-diff --git a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
-index 4db61038e5e476bad3a61dbdb707e5222c1f08f8..9eca13d9cfac3b762917aaaa942267effb743cf7 100644
---- a/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
-+++ b/browser/components/urlbar/UrlbarProviderPlaces.sys.mjs
-@@ -45,11 +45,13 @@ function defaultQuery(conditions = "") {
- let query = `
- SELECT h.url, h.title, ${SQL_BOOKMARK_TAGS_FRAGMENT}, h.id, t.open_count,
- ${lazy.PAGES_FRECENCY_FIELD} AS frecency, t.userContextId,
-- h.last_visit_date, NULLIF(t.groupId, '') groupId
-+ h.last_visit_date, NULLIF(t.groupId, '') groupId, zp.url AS pinned_url, zp.title AS pinned_title
- FROM moz_places h
- LEFT JOIN moz_openpages_temp t
- ON t.url = h.url
- AND (t.userContextId = :userContextId OR (t.userContextId <> -1 AND :userContextId IS NULL))
-+ LEFT JOIN zen_pins zp
-+ ON zp.url = h.url
- WHERE (
- (:switchTabsEnabled AND t.open_count > 0) OR
- ${lazy.PAGES_FRECENCY_FIELD} <> 0
-@@ -63,7 +65,7 @@ function defaultQuery(conditions = "") {
- :matchBehavior, :searchBehavior, NULL)
- ELSE
- AUTOCOMPLETE_MATCH(:searchString, h.url,
-- h.title, '',
-+ IFNULL(zp.title, h.title), '',
- h.visit_count, h.typed,
- 0, t.open_count,
- :matchBehavior, :searchBehavior, NULL)
-@@ -1176,11 +1178,13 @@ class Search {
- ? lazy.PlacesUtils.toDate(lastVisitPRTime).getTime()
- : undefined;
- let tabGroup = row.getResultByName("groupId");
-+ let pinnedTitle = row.getResultByIndex(12);
-+ let pinnedUrl = row.getResultByIndex("pinned_url");
-
- let match = {
- placeId,
-- value: url,
-- comment: bookmarkTitle || historyTitle,
-+ value: pinnedUrl || url,
-+ comment: pinnedTitle || bookmarkTitle || historyTitle,
- icon: UrlbarUtils.getIconForUrl(url),
- frecency: frecency || FRECENCY_DEFAULT,
- userContextId,
diff --git a/src/zen/ZenComponents.manifest b/src/zen/ZenComponents.manifest
index 74c0232b85..0f38828a14 100644
--- a/src/zen/ZenComponents.manifest
+++ b/src/zen/ZenComponents.manifest
@@ -13,3 +13,4 @@
category app-startup nsBrowserGlue @mozilla.org/browser/browserglue;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
#include common/Components.manifest
+#include sessionstore/SessionComponents.manifest
diff --git a/src/zen/common/modules/ZenCommonUtils.mjs b/src/zen/common/modules/ZenCommonUtils.mjs
index e1ac284bc9..905939d8a1 100644
--- a/src/zen/common/modules/ZenCommonUtils.mjs
+++ b/src/zen/common/modules/ZenCommonUtils.mjs
@@ -38,6 +38,10 @@ export class nsZenMultiWindowFeature {
if (!nsZenMultiWindowFeature.isActiveWindow) {
return;
}
+ return this.forEachWindow(callback);
+ }
+
+ async forEachWindow(callback) {
for (const browser of nsZenMultiWindowFeature.browsers) {
try {
if (browser.closed) continue;
diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs
index daa0d0962c..95b2aac202 100644
--- a/src/zen/common/modules/ZenSessionStore.mjs
+++ b/src/zen/common/modules/ZenSessionStore.mjs
@@ -17,8 +17,9 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenWorkspace) {
tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
}
- if (tabData.zenPinnedId) {
- tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
+ // Keep for now, for backward compatibility for window sync to work.
+ if (tabData.zenSyncId || tabData.zenPinnedId) {
+ tab.setAttribute('id', tabData.zenSyncId || tabData.zenPinnedId);
}
if (tabData.zenHasStaticLabel) {
tab.setAttribute('zen-has-static-label', 'true');
@@ -32,6 +33,9 @@ class ZenSessionStore extends nsZenPreloadedFeature {
if (tabData.zenPinnedEntry) {
tab.setAttribute('zen-pinned-entry', tabData.zenPinnedEntry);
}
+ if (tabData._zenPinnedInitialState) {
+ tab._zenPinnedInitialState = tabData._zenPinnedInitialState;
+ }
}
async #waitAndCleanup() {
diff --git a/src/zen/common/modules/ZenStartup.mjs b/src/zen/common/modules/ZenStartup.mjs
index 942dd27eef..d855312960 100644
--- a/src/zen/common/modules/ZenStartup.mjs
+++ b/src/zen/common/modules/ZenStartup.mjs
@@ -12,12 +12,7 @@ class ZenStartup {
isReady = false;
- async init() {
- // important: We do this to ensure that some firefox components
- // are initialized before we start our own initialization.
- // please, do not remove this line and if you do, make sure to
- // test the startup process.
- await new Promise((resolve) => setTimeout(resolve, 0));
+ init() {
this.openWatermark();
this.#initBrowserBackground();
this.#changeSidebarLocation();
@@ -97,6 +92,7 @@ class ZenStartup {
// Just in case we didn't get the right size.
gZenUIManager.updateTabsToolbar();
this.closeWatermark();
+ document.getElementById('tabbrowser-arrowscrollbox').setAttribute('orient', 'vertical');
this.isReady = true;
});
}
diff --git a/src/zen/common/modules/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
index e16fc982f7..517696d30b 100644
--- a/src/zen/common/modules/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -811,7 +811,6 @@ window.gZenVerticalTabsManager = {
!aItem.isConnected ||
gZenUIManager.testingEnabled ||
!gZenStartup.isReady ||
- !gZenPinnedTabManager.hasInitializedPins ||
aItem.group?.hasAttribute('split-view-group')
) {
return;
@@ -1310,14 +1309,6 @@ window.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(
diff --git a/src/zen/common/styles/zen-browser-container.css b/src/zen/common/styles/zen-browser-container.css
index a1715df59b..c75e00d2e2 100644
--- a/src/zen/common/styles/zen-browser-container.css
+++ b/src/zen/common/styles/zen-browser-container.css
@@ -60,3 +60,15 @@
}
}
}
+
+.zen-pseudo-browser-image {
+ position: absolute;
+ inset: 0;
+ opacity: 0.4;
+ pointer-events: none;
+ z-index: 2;
+}
+
+browser[zen-pseudo-hidden='true'] {
+ -moz-subtree-hidden-only-visually: 1 !important;
+}
diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css
index 93f4f8808d..c1f40a074d 100644
--- a/src/zen/common/styles/zen-theme.css
+++ b/src/zen/common/styles/zen-theme.css
@@ -231,6 +231,13 @@
--toolbox-textcolor: currentColor !important;
}
+ &[zen-unsynced-window='true'] {
+ --zen-main-browser-background: linear-gradient(130deg, light-dark(rgb(240, 230, 200), rgb(30, 25, 20)) 0%, light-dark(rgb(220, 200, 150), rgb(50, 45, 40)) 100%);
+ --zen-main-browser-background-toolbar: var(--zen-main-browser-background);
+ --zen-primary-color: light-dark(rgb(200, 100, 20), rgb(220, 120, 30)) !important;
+ --toolbox-textcolor: currentColor !important;
+ }
+
--toolbar-field-background-color: var(--zen-colors-input-bg) !important;
--arrowpanel-background: var(--zen-dialog-background) !important;
diff --git a/src/zen/common/sys/ZenCustomizableUI.sys.mjs b/src/zen/common/sys/ZenCustomizableUI.sys.mjs
index bd7efc760d..42fd818dab 100644
--- a/src/zen/common/sys/ZenCustomizableUI.sys.mjs
+++ b/src/zen/common/sys/ZenCustomizableUI.sys.mjs
@@ -116,6 +116,9 @@ export const ZenCustomizableUI = new (class {
#initCreateNewButton(window) {
const button = window.document.getElementById('zen-create-new-button');
button.addEventListener('command', (event) => {
+ if (window.gZenWorkspaces.privateWindowOrDisabled) {
+ return window.document.getElementById('cmd_newNavigatorTab').doCommand();
+ }
if (button.hasAttribute('open')) {
return;
}
diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js
index b7b6c00ad6..25cfe4b018 100644
--- a/src/zen/common/zen-sets.js
+++ b/src/zen/common/zen-sets.js
@@ -6,138 +6,135 @@ document.addEventListener(
'MozBeforeInitialXULLayout',
() => {
// defined in browser-sets.inc
- document
- .getElementById('zenCommandSet')
-
- .addEventListener('command', (event) => {
- switch (event.target.id) {
- case 'cmd_zenCompactModeToggle':
- gZenCompactModeManager.toggle();
- break;
- case 'cmd_zenCompactModeShowSidebar':
- gZenCompactModeManager.toggleSidebar();
- break;
- case 'cmd_toggleCompactModeIgnoreHover':
- gZenCompactModeManager.toggle(true);
- break;
- case 'cmd_zenWorkspaceForward':
- gZenWorkspaces.changeWorkspaceShortcut();
- break;
- case 'cmd_zenWorkspaceBackward':
- gZenWorkspaces.changeWorkspaceShortcut(-1);
- break;
- case 'cmd_zenSplitViewGrid':
- gZenViewSplitter.toggleShortcut('grid');
- break;
- case 'cmd_zenSplitViewVertical':
- gZenViewSplitter.toggleShortcut('vsep');
- break;
- case 'cmd_zenSplitViewHorizontal':
- gZenViewSplitter.toggleShortcut('hsep');
- break;
- case 'cmd_zenSplitViewUnsplit':
- gZenViewSplitter.toggleShortcut('unsplit');
- break;
- case 'cmd_zenSplitViewContextMenu':
- gZenViewSplitter.contextSplitTabs();
- break;
- case 'cmd_zenCopyCurrentURLMarkdown':
- gZenCommonActions.copyCurrentURLAsMarkdownToClipboard();
- break;
- case 'cmd_zenCopyCurrentURL':
- gZenCommonActions.copyCurrentURLToClipboard();
- break;
- case 'cmd_zenPinnedTabReset':
- gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab);
- break;
- case 'cmd_zenPinnedTabResetNoTab':
- gZenPinnedTabManager.resetPinnedTab();
- break;
- case 'cmd_zenToggleSidebar':
- gZenVerticalTabsManager.toggleExpand();
- break;
- case 'cmd_zenOpenZenThemePicker':
- gZenThemePicker.openThemePicker(event);
- break;
- case 'cmd_zenChangeWorkspaceTab':
- gZenWorkspaces.changeTabWorkspace(
- event.sourceEvent.target.getAttribute('zen-workspace-id')
- );
- break;
- case 'cmd_zenToggleTabsOnRight':
- gZenVerticalTabsManager.toggleTabsOnRight();
- break;
- case 'cmd_zenSplitViewLinkInNewTab':
- gZenViewSplitter.splitLinkInNewTab();
- break;
- case 'cmd_zenNewEmptySplit':
- setTimeout(() => {
- gZenViewSplitter.createEmptySplit();
- }, 0);
- break;
- case 'cmd_zenReplacePinnedUrlWithCurrent':
- gZenPinnedTabManager.replacePinnedUrlWithCurrent();
- break;
- case 'cmd_contextZenAddToEssentials':
- gZenPinnedTabManager.addToEssentials();
- break;
- case 'cmd_contextZenRemoveFromEssentials':
- gZenPinnedTabManager.removeEssentials();
- break;
- case 'cmd_zenCtxDeleteWorkspace':
- gZenWorkspaces.contextDeleteWorkspace(event);
- break;
- case 'cmd_zenChangeWorkspaceName':
- gZenVerticalTabsManager.renameTabStart({
- target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(
- '.zen-current-workspace-indicator-name'
- ),
- });
- break;
- case 'cmd_zenChangeWorkspaceIcon':
- gZenWorkspaces.changeWorkspaceIcon();
- break;
- case 'cmd_zenReorderWorkspaces':
- gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', {
- timeout: 9000,
- descriptionId: 'zen-workspaces-how-to-reorder-desc',
- });
- break;
- case 'cmd_zenOpenWorkspaceCreation':
- gZenWorkspaces.openWorkspaceCreation(event);
- break;
- case 'cmd_zenOpenFolderCreation':
- gZenFolders.createFolder([], {
- renameFolder: true,
- });
- break;
- case 'cmd_zenTogglePinTab': {
- const currentTab = gBrowser.selectedTab;
- if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) {
- if (currentTab.pinned) {
- gBrowser.unpinTab(currentTab);
- } else {
- gBrowser.pinTab(currentTab);
- }
+ document.getElementById('zenCommandSet').addEventListener('command', (event) => {
+ switch (event.target.id) {
+ case 'cmd_zenCompactModeToggle':
+ gZenCompactModeManager.toggle();
+ break;
+ case 'cmd_zenCompactModeShowSidebar':
+ gZenCompactModeManager.toggleSidebar();
+ break;
+ case 'cmd_toggleCompactModeIgnoreHover':
+ gZenCompactModeManager.toggle(true);
+ break;
+ case 'cmd_zenWorkspaceForward':
+ gZenWorkspaces.changeWorkspaceShortcut();
+ break;
+ case 'cmd_zenWorkspaceBackward':
+ gZenWorkspaces.changeWorkspaceShortcut(-1);
+ break;
+ case 'cmd_zenSplitViewGrid':
+ gZenViewSplitter.toggleShortcut('grid');
+ break;
+ case 'cmd_zenSplitViewVertical':
+ gZenViewSplitter.toggleShortcut('vsep');
+ break;
+ case 'cmd_zenSplitViewHorizontal':
+ gZenViewSplitter.toggleShortcut('hsep');
+ break;
+ case 'cmd_zenSplitViewUnsplit':
+ gZenViewSplitter.toggleShortcut('unsplit');
+ break;
+ case 'cmd_zenSplitViewContextMenu':
+ gZenViewSplitter.contextSplitTabs();
+ break;
+ case 'cmd_zenCopyCurrentURLMarkdown':
+ gZenCommonActions.copyCurrentURLAsMarkdownToClipboard();
+ break;
+ case 'cmd_zenCopyCurrentURL':
+ gZenCommonActions.copyCurrentURLToClipboard();
+ break;
+ case 'cmd_zenPinnedTabReset':
+ gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab);
+ break;
+ case 'cmd_zenPinnedTabResetNoTab':
+ gZenPinnedTabManager.resetPinnedTab();
+ break;
+ case 'cmd_zenToggleSidebar':
+ gZenVerticalTabsManager.toggleExpand();
+ break;
+ case 'cmd_zenOpenZenThemePicker':
+ gZenThemePicker.openThemePicker(event);
+ break;
+ case 'cmd_zenChangeWorkspaceTab':
+ gZenWorkspaces.changeTabWorkspace(
+ event.sourceEvent.target.getAttribute('zen-workspace-id')
+ );
+ break;
+ case 'cmd_zenToggleTabsOnRight':
+ gZenVerticalTabsManager.toggleTabsOnRight();
+ break;
+ case 'cmd_zenSplitViewLinkInNewTab':
+ gZenViewSplitter.splitLinkInNewTab();
+ break;
+ case 'cmd_zenNewEmptySplit':
+ setTimeout(() => {
+ gZenViewSplitter.createEmptySplit();
+ }, 0);
+ break;
+ case 'cmd_zenReplacePinnedUrlWithCurrent':
+ gZenPinnedTabManager.replacePinnedUrlWithCurrent();
+ break;
+ case 'cmd_contextZenAddToEssentials':
+ gZenPinnedTabManager.addToEssentials();
+ break;
+ case 'cmd_contextZenRemoveFromEssentials':
+ gZenPinnedTabManager.removeEssentials();
+ break;
+ case 'cmd_zenCtxDeleteWorkspace':
+ gZenWorkspaces.contextDeleteWorkspace(event);
+ break;
+ case 'cmd_zenChangeWorkspaceName':
+ gZenVerticalTabsManager.renameTabStart({
+ target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(
+ '.zen-current-workspace-indicator-name'
+ ),
+ });
+ break;
+ case 'cmd_zenChangeWorkspaceIcon':
+ gZenWorkspaces.changeWorkspaceIcon();
+ break;
+ case 'cmd_zenReorderWorkspaces':
+ gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', {
+ timeout: 9000,
+ descriptionId: 'zen-workspaces-how-to-reorder-desc',
+ });
+ break;
+ case 'cmd_zenOpenWorkspaceCreation':
+ gZenWorkspaces.openWorkspaceCreation(event);
+ break;
+ case 'cmd_zenOpenFolderCreation':
+ gZenFolders.createFolder([], {
+ renameFolder: true,
+ });
+ break;
+ case 'cmd_zenTogglePinTab': {
+ const currentTab = gBrowser.selectedTab;
+ if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) {
+ if (currentTab.pinned) {
+ gBrowser.unpinTab(currentTab);
+ } else {
+ gBrowser.pinTab(currentTab);
}
- break;
- }
- case 'cmd_zenCloseUnpinnedTabs':
- gZenWorkspaces.closeAllUnpinnedTabs();
- break;
- case 'cmd_zenUnloadWorkspace': {
- gZenWorkspaces.unloadWorkspace();
- break;
}
- default:
- gZenGlanceManager.handleMainCommandSet(event);
- if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) {
- const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1;
- gZenWorkspaces.shortcutSwitchTo(index);
- }
- break;
+ break;
}
- });
+ case 'cmd_zenCloseUnpinnedTabs':
+ gZenWorkspaces.closeAllUnpinnedTabs();
+ break;
+ case 'cmd_zenUnloadWorkspace': {
+ gZenWorkspaces.unloadWorkspace();
+ break;
+ }
+ default:
+ gZenGlanceManager.handleMainCommandSet(event);
+ if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) {
+ const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1;
+ gZenWorkspaces.shortcutSwitchTo(index);
+ }
+ break;
+ }
+ });
},
{ once: true }
);
diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs
index 2209e67138..6c1d2dbc78 100644
--- a/src/zen/folders/ZenFolder.mjs
+++ b/src/zen/folders/ZenFolder.mjs
@@ -150,7 +150,6 @@ class ZenFolder extends MozTabbrowserTabGroup {
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 @@ class ZenFolder extends MozTabbrowserTabGroup {
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.
diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs
index 8fdf63fec7..01d9f1062f 100644
--- a/src/zen/folders/ZenFolders.mjs
+++ b/src/zen/folders/ZenFolders.mjs
@@ -101,20 +101,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.querySelector('menupopup');
changeFolderSpace.innerHTML = '';
for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
- const item = document.createXULElement('menuitem');
- item.className = 'zen-workspace-context-menu-item';
- item.setAttribute('zen-workspace-id', workspace.uuid);
- item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace);
- let name = workspace.name;
- const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
- if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
- name = `${workspace.icon} ${name}`;
- }
- item.setAttribute('label', name);
- if (iconIsSvg) {
- item.setAttribute('image', workspace.icon);
- item.classList.add('zen-workspace-context-icon');
- }
+ const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (event) => {
if (!this.#lastFolderContextMenu) return;
this.changeFolderToSpace(
@@ -418,7 +405,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (selectedTab) {
selectedTab.setAttribute('zen-workspace-id', newWorkspace.uuid);
selectedTab.removeAttribute('folder-active');
- gZenWorkspaces._lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
+ gZenWorkspaces.lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
}
resolve();
});
@@ -434,10 +421,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tab.style.height = '';
}
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
- if (gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
+ if (gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to
// the current workspace
- delete gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid];
+ delete gZenWorkspaces.lastSelectedWorkspaceTabs[currentWorkspace.uuid];
}
}
}
@@ -456,9 +443,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
// we may encounter
tab.group.setAttribute('zen-workspace-id', workspaceId);
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
- if (gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId] === tab) {
+ if (gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to a new workspace
- delete gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId];
+ delete gZenWorkspaces.lastSelectedWorkspaceTabs[workspaceId];
}
}
folder.dispatchEvent(new CustomEvent('ZenFolderChangedWorkspace', { bubbles: true }));
@@ -506,9 +493,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
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);
@@ -860,7 +844,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
.open(group.icon, { onlySvgIcons: true })
.then((icon) => {
this.setFolderUserIcon(group, icon);
- group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true }));
+ group.dispatchEvent(new CustomEvent('TabGroupUpdate', { bubbles: true }));
})
.catch((err) => {
console.error(err);
@@ -938,7 +922,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
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('id'));
let prevSiblingInfo = null;
const prevSibling = folder.previousElementSibling;
@@ -947,9 +931,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
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');
- prevSiblingInfo = { type: 'tab', id: zenPinId };
+ } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('id')) {
+ prevSiblingInfo = { type: 'tab', id: prevSibling.getAttribute('id') };
} else {
prevSiblingInfo = { type: 'start', id: null };
}
@@ -967,7 +950,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
prevSiblingInfo: prevSiblingInfo,
emptyTabIds: emptyFolderTabs,
userIcon: userIcon?.getAttribute('href'),
- pinId: folder.getAttribute('zen-pin-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'),
@@ -994,10 +976,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
tabFolderWorkingData.set(folderData.id, workingData);
const oldGroup = document.getElementById(folderData.id);
- folderData.emptyTabIds.forEach((zenPinId) => {
- oldGroup
- ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
- ?.setAttribute('zen-empty-tab', true);
+ folderData.emptyTabIds.forEach((id) => {
+ oldGroup?.querySelector(`tab[id="${id}"]`)?.setAttribute('zen-empty-tab', true);
});
if (oldGroup) {
if (!folderData.splitViewGroup) {
@@ -1009,7 +989,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
saveOnWindowClose: folderData.saveOnWindowClose,
workspaceId: folderData.workspaceId,
});
- folder.setAttribute('zen-pin-id', folderData.pinId);
+ folder.setAttribute('id', folderData.id);
workingData.node = folder;
oldGroup.before(folder);
} else {
@@ -1041,9 +1021,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
if (parentWorkingData && parentWorkingData.node) {
switch (stateData?.prevSiblingInfo?.type) {
case 'tab': {
- const tab = parentWorkingData.node.querySelector(
- `[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
- );
+ const tab = document.getElementById(stateData.prevSiblingInfo.id);
tab.after(node);
break;
}
@@ -1153,7 +1131,8 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
- let firstGroupElem = dropElementGroup.querySelector('.zen-tab-group-start').nextElementSibling;
+ let firstGroupElem =
+ dropElementGroup?.querySelector('.zen-tab-group-start')?.nextElementSibling;
if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
const isInMiddleZone =
diff --git a/src/zen/moz.build b/src/zen/moz.build
index 21915a69ab..eb681597f0 100644
--- a/src/zen/moz.build
+++ b/src/zen/moz.build
@@ -13,4 +13,5 @@ DIRS += [
"tests",
"urlbar",
"toolkit",
+ "sessionstore",
]
diff --git a/src/zen/sessionstore/SessionComponents.manifest b/src/zen/sessionstore/SessionComponents.manifest
new file mode 100644
index 0000000000..8e3b0f9b08
--- /dev/null
+++ b/src/zen/sessionstore/SessionComponents.manifest
@@ -0,0 +1,10 @@
+# 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
+category browser-before-ui-startup resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.init
+
+# App shutdown consumers
+category browser-quit-application-granted resource:///modules/zen/ZenWindowSync.sys.mjs ZenWindowSync.uninit
diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs
new file mode 100644
index 0000000000..724087d98c
--- /dev/null
+++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs
@@ -0,0 +1,367 @@
+/* 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/. */
+
+import { JSONFile } from 'resource://gre/modules/JSONFile.sys.mjs';
+import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs';
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ 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',
+ SessionSaver: 'resource:///modules/sessionstore/SessionSaver.sys.mjs',
+ setTimeout: 'resource://gre/modules/Timer.sys.mjs',
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.session-store.log', true);
+
+// 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 SHOULD_BACKUP_FILE = Services.prefs.getBoolPref('zen.session-store.backup-file', true);
+
+const FILE_NAME = SHOULD_COMPRESS_FILE ? 'zen-sessions.jsonlz4' : 'zen-sessions.json';
+const MIGRATION_PREF = 'zen.ui.migration.session-manager-restore';
+
+/**
+ * Class representing the sidebar object stored in the session file.
+ * This object holds all the data related to tabs, groups, folders
+ * and split view state.
+ */
+class nsZenSidebarObject {
+ #sidebar = {};
+
+ get data() {
+ return Cu.cloneInto(this.#sidebar, {});
+ }
+
+ set data(data) {
+ this.#sidebar = data;
+ }
+}
+
+export class nsZenSessionManager {
+ /**
+ * The JSON file instance used to read/write session data.
+ * @type {JSONFile}
+ */
+ #file = null;
+ /**
+ * The sidebar object holding tabs, groups, folders and split view data.
+ * @type {nsZenSidebarObject}
+ */
+ #sidebarObject = new nsZenSidebarObject();
+
+ // Called from SessionComponents.manifest on app-startup
+ init() {
+ let profileDir = Services.dirsvc.get('ProfD', Ci.nsIFile).path;
+ let backupFile = null;
+ if (SHOULD_BACKUP_FILE) {
+ backupFile = PathUtils.join(profileDir, 'zen-sessions-backup', FILE_NAME);
+ }
+ let filePath = PathUtils.join(profileDir, FILE_NAME);
+ this.#file = new JSONFile({
+ path: filePath,
+ compression: SHOULD_COMPRESS_FILE ? 'lz4' : undefined,
+ backupFile,
+ });
+ }
+
+ log(...args) {
+ if (lazy.gShouldLog) {
+ console.info('ZenSessionManager:', ...args);
+ }
+ }
+
+ /**
+ * Gets the spaces data from the Places database for migration.
+ * This is only called once during the first run after updating
+ * to a version that uses the new session manager.
+ */
+ async #getSpacesFromDBForMigration() {
+ try {
+ const { PlacesUtils } = ChromeUtils.importESModule(
+ 'resource://gre/modules/PlacesUtils.sys.mjs'
+ );
+ const db = await PlacesUtils.promiseDBConnection();
+ const rows = await db.executeCached('SELECT * FROM zen_workspaces ORDER BY created_at ASC');
+ this._migrationSpaceData = rows.map((row) => ({
+ uuid: row.getResultByName('uuid'),
+ name: row.getResultByName('name'),
+ icon: row.getResultByName('icon'),
+ containerTabId: row.getResultByName('container_id') ?? 0,
+ position: row.getResultByName('position'),
+ theme: row.getResultByName('theme_type')
+ ? {
+ type: row.getResultByName('theme_type'),
+ gradientColors: JSON.parse(row.getResultByName('theme_colors')),
+ opacity: row.getResultByName('theme_opacity'),
+ rotation: row.getResultByName('theme_rotation'),
+ texture: row.getResultByName('theme_texture'),
+ }
+ : null,
+ }));
+ } catch {
+ /* ignore errors during migration */
+ }
+ }
+
+ /**
+ * Reads the session file and populates the sidebar object.
+ * This should be only called once at startup.
+ * @see SessionFileInternal.read
+ */
+ async readFile() {
+ try {
+ let promises = [];
+ promises.push(this.#file.load());
+ if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
+ promises.push(this.#getSpacesFromDBForMigration());
+ }
+ await Promise.all(promises);
+ } catch (e) {
+ console.error('ZenSessionManager: Failed to read session file', e);
+ }
+ this.#sidebar = this.#file.data || {};
+ }
+
+ /**
+ * Called when the session file is read. Restores the sidebar data
+ * into all windows.
+ *
+ * @param initialState
+ * The initial session state read from the session file.
+ */
+ onFileRead(initialState) {
+ // For the first time after migration, we restore the tabs
+ // That where going to be restored by SessionStore. The sidebar
+ // object will always be empty after migration because we haven't
+ // gotten the opportunity to save the session yet.
+ if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) {
+ Services.prefs.setBoolPref(MIGRATION_PREF, true);
+ for (const winData of initialState.windows || []) {
+ winData.spaces = this._migrationSpaceData || [];
+ }
+ delete this._migrationSpaceData;
+ return;
+ }
+ // If there's no initial state, nothing to restore. This would
+ // happen if the file is empty or corrupted.
+ if (!initialState) {
+ return;
+ }
+ // If there are no windows, we create an empty one. By default,
+ // firefox would create simply a new empty window, but we want
+ // to make sure that the sidebar object is properly initialized.
+ // This would happen on first run after having a single private window
+ // open when quitting the app, for example.
+ if (!initialState.windows?.length) {
+ initialState.windows = [{}];
+ }
+ // Restore all windows with the same sidebar object, this will
+ // guarantee that all tabs, groups, folders and split view data
+ // are properly synced across all windows.
+ this.log(`Restoring Zen session data into ${initialState.windows?.length || 0} windows`);
+ for (const winData of initialState.windows || []) {
+ this.#restoreWindowData(winData);
+ }
+ }
+
+ get #sidebar() {
+ return this.#sidebarObject.data;
+ }
+
+ set #sidebar(data) {
+ this.#sidebarObject.data = data;
+ }
+
+ /**
+ * Saves the current session state. Collects data and writes to disk.
+ *
+ * @param state
+ * The current session state.
+ */
+ saveState(state) {
+ if (lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || !state?.windows?.length) {
+ // Don't save (or even collect) anything in permanent private
+ // browsing mode. We also don't want to save if there are no windows.
+ return;
+ }
+ this.#collectWindowData(state);
+ // This would save the data to disk asynchronously or when
+ // quitting the app.
+ this.#file.data = this.#sidebar;
+ this.#file.saveSoon();
+ this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
+ }
+
+ /**
+ * Saves the session data for a closed window if it meets the criteria.
+ * See SessionStoreInternal.maybeSaveClosedWindow for more details.
+ *
+ * @param aWinData - The window data object to save.
+ * @param isLastWindow - Whether this is the last saveable window.
+ */
+ maybeSaveClosedWindow(aWinData, isLastWindow) {
+ // We only want to save the *last* normal window that is closed.
+ // If its not the last window, we can still update the sidebar object
+ // based on other open windows.
+ if (aWinData.isPopup || aWinData.isTaskbarTab || aWinData.isZenUnsynced || !isLastWindow) {
+ return;
+ }
+ this.log('Saving closed window session data into Zen session store');
+ this.saveState({ windows: [aWinData] });
+ }
+
+ /**
+ * Collects session data for a given window.
+ *
+ * @param state
+ * The current session state.
+ */
+ #collectWindowData(state) {
+ let sidebarData = this.#sidebar;
+ if (!sidebarData) {
+ sidebarData = {};
+ }
+
+ sidebarData.lastCollected = Date.now();
+ this.#collectTabsData(sidebarData, state);
+ this.#sidebar = sidebarData;
+ }
+
+ #filterUnusedTabs(tabs) {
+ return tabs.filter((tab) => {
+ // We need to ignore empty tabs with no group association
+ // as they are not useful to restore.
+ return !(tab.zenIsEmpty && !tab.groupId);
+ });
+ }
+
+ /**
+ * Collects session data for all tabs in a given window.
+ *
+ * @param sidebarData
+ * The sidebar data object to populate.
+ * @param state
+ * The current session state.
+ */
+ #collectTabsData(sidebarData, state) {
+ const tabIdRelationMap = new Map();
+ for (const window of state.windows) {
+ // Only accept the tabs with `_zenIsActiveTab` set to true from
+ // every window. We do this to avoid collecting tabs with invalid
+ // state when multiple windows are open. Note that if we a tab without
+ // this flag set in any other window, we just add it anyway.
+ for (const tabData of window.tabs) {
+ if (!tabIdRelationMap.has(tabData.zenSyncId) || tabData._zenIsActiveTab) {
+ tabIdRelationMap.set(tabData.zenSyncId, tabData);
+ }
+ }
+ }
+
+ sidebarData.tabs = this.#filterUnusedTabs(Array.from(tabIdRelationMap.values()));
+
+ sidebarData.folders = state.windows[0].folders;
+ sidebarData.splitViewData = state.windows[0].splitViewData;
+ sidebarData.groups = state.windows[0].groups;
+ sidebarData.spaces = state.windows[0].spaces;
+ }
+
+ /**
+ * Restores the sidebar data into a given window data object.
+ * We do this in order to make sure all new window objects
+ * have the same sidebar data.
+ *
+ * @param aWindowData
+ * The window data object to restore into.
+ */
+ #restoreWindowData(aWindowData) {
+ const sidebar = this.#sidebar;
+ if (!sidebar) {
+ return;
+ }
+ aWindowData.tabs = sidebar.tabs || [];
+ aWindowData.splitViewData = sidebar.splitViewData;
+ aWindowData.folders = sidebar.folders;
+ aWindowData.groups = sidebar.groups;
+ aWindowData.spaces = sidebar.spaces;
+ }
+
+ /**
+ * Restores a new window with Zen session data. This should be called
+ * not at startup, but when a new window is opened by the user.
+ *
+ * @param aWindow
+ * The window to restore.
+ * @param SessionStoreInternal
+ * The SessionStore module instance.
+ * @param resolvePromise
+ * The promise resolver to call when done. We use a promise
+ * here because out workspace manager always waits for SessionStore
+ * to restore all the windows before initializing, but when opening
+ * a new window, that promise is always resolved, meaning it may run
+ * into a race condition if we try to restore the window synchronously
+ * here.
+ */
+ restoreNewWindow(aWindow, SessionStoreInternal) {
+ if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) {
+ return;
+ }
+ this.log('Restoring new window with Zen session data');
+ const state = lazy.SessionStore.getCurrentState(true);
+ const windows = (state.windows || []).filter(
+ (win) => !win.isPrivate && !win.isPopup && !win.isTaskbarTab && !win.isZenUnsynced
+ );
+ let windowToClone = windows[0] || {};
+ let newWindow = Cu.cloneInto(windowToClone, {});
+ if (windows.length < 2) {
+ // We only want to restore the sidebar object if we found
+ // only one normal window to clone from (which is the one
+ // we are opening).
+ this.log('Restoring sidebar data into new window');
+ this.#restoreWindowData(newWindow);
+ }
+ newWindow.tabs = this.#filterUnusedTabs(newWindow.tabs || []);
+
+ // These are window-specific from the previous window state that
+ // we don't want to restore into the new window. Otherwise, new
+ // windows would appear overlapping the previous one, or with
+ // the same size and position, which should be decided by the
+ // window manager.
+ delete newWindow.selected;
+ delete newWindow.screenX;
+ delete newWindow.screenY;
+ delete newWindow.width;
+ delete newWindow.height;
+ delete newWindow.sizemode;
+ delete newWindow.sizemodeBeforeMinimized;
+ delete newWindow.zIndex;
+
+ const newState = { windows: [newWindow] };
+ this.log(`Cloning window with ${newWindow.tabs.length} tabs`);
+
+ SessionStoreInternal._deferredInitialState = newState;
+ SessionStoreInternal.initializeWindow(aWindow, newState);
+ }
+
+ /**
+ * Gets the cloned spaces data from the sidebar object.
+ * This is used during migration to restore spaces into
+ * the initial session state.
+ *
+ * @returns {Array} The cloned spaces data.
+ */
+ getClonedSpaces() {
+ const sidebar = this.#sidebar;
+ if (!sidebar || !sidebar.spaces) {
+ return [];
+ }
+ return Cu.cloneInto(sidebar.spaces, {});
+ }
+}
+
+export const ZenSessionStore = new nsZenSessionManager();
diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs
new file mode 100644
index 0000000000..e0b0b79d9b
--- /dev/null
+++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs
@@ -0,0 +1,1031 @@
+/* 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/. */
+
+import { XPCOMUtils } from 'resource://gre/modules/XPCOMUtils.sys.mjs';
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserWindowTracker: 'resource:///modules/BrowserWindowTracker.sys.mjs',
+ SessionStore: 'resource:///modules/sessionstore/SessionStore.sys.mjs',
+ TabStateFlusher: 'resource:///modules/sessionstore/TabStateFlusher.sys.mjs',
+ ZenSessionStore: 'resource:///modules/zen/ZenSessionManager.sys.mjs',
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gWindowSyncEnabled', 'zen.window-sync.enabled');
+XPCOMUtils.defineLazyPreferenceGetter(lazy, 'gShouldLog', 'zen.window-sync.log', true);
+
+const OBSERVING = ['browser-window-before-show'];
+const INSTANT_EVENTS = ['SSWindowClosing'];
+const EVENTS = [
+ 'TabOpen',
+ 'TabClose',
+
+ 'ZenTabIconChanged',
+ 'ZenTabLabelChanged',
+
+ 'TabMove',
+ 'TabPinned',
+ 'TabUnpinned',
+ 'TabAddedToEssentials',
+ 'TabRemovedFromEssentials',
+
+ 'TabGroupUpdate',
+ 'TabGroupCreate',
+ 'TabGroupRemoved',
+ 'TabGroupMoved',
+
+ 'ZenTabRemovedFromSplit',
+ 'ZenSplitViewTabsSplit',
+
+ 'TabSelect',
+
+ 'focus',
+ ...INSTANT_EVENTS,
+];
+
+// Flags acting as an enum for sync types.
+const SYNC_FLAG_LABEL = 1 << 0;
+const SYNC_FLAG_ICON = 1 << 1;
+const SYNC_FLAG_MOVE = 1 << 2;
+
+class nsZenWindowSync {
+ constructor() {}
+
+ /**
+ * Context about the currently handled event.
+ * Used to avoid re-entrancy issues.
+ *
+ * We do still want to keep a stack of these in order
+ * to handle consecutive events properly. For example,
+ * loading a webpage will call IconChanged and TitleChanged
+ * events one after another.
+ */
+ #eventHandlingContext = {
+ window: null,
+ eventCount: 0,
+ lastHandlerPromise: Promise.resolve(),
+ };
+
+ /**
+ * Last focused window.
+ * Used to determine which window to sync tab contents visibility from.
+ */
+ #lastFocusedWindow = null;
+
+ /**
+ * Last selected tab.
+ * Used to determine if we should run another sync operation
+ * when switching browser views.
+ */
+ #lastSelectedTab = null;
+
+ /**
+ * Iterator that yields all currently opened browser windows.
+ * (Might miss the most recent one.)
+ * This list is in focus order, but may include minimized windows
+ * before non-minimized windows.
+ */
+ #browserWindows = {
+ *[Symbol.iterator]() {
+ for (let window of lazy.BrowserWindowTracker.orderedWindows) {
+ if (
+ window.__SSi &&
+ !window.closed &&
+ window.gZenStartup.isReady &&
+ !window.gZenWorkspaces?.privateWindowOrDisabled
+ ) {
+ yield window;
+ }
+ }
+ },
+ };
+
+ init() {
+ if (!lazy.gWindowSyncEnabled) {
+ return;
+ }
+ for (let topic of OBSERVING) {
+ Services.obs.addObserver(this, topic);
+ }
+ }
+
+ uninit() {
+ for (let topic of OBSERVING) {
+ Services.obs.removeObserver(this, topic);
+ }
+ }
+
+ log(...args) {
+ if (lazy.gShouldLog) {
+ console.info('ZenWindowSync:', ...args);
+ }
+ }
+
+ /**
+ * Called when a browser window is about to be shown.
+ * Adds event listeners for the specified events.
+ *
+ * @param {Window} aWindow - The browser window that is about to be shown.
+ */
+ #onWindowBeforeShow(aWindow) {
+ // There are 2 possibilities to know if we are trying to open
+ // a new *unsynced* window:
+ // 1. We are passing `zen-unsynced` in the window arguments.
+ // 2. We are trying to open a link in a new window where other synced
+ // windows already exist
+ let forcedSync = false;
+ let hasUnsyncedArg = false;
+ if (aWindow._zenStartupSyncFlag === 'synced') {
+ forcedSync = true;
+ } else if (aWindow._zenStartupSyncFlag === 'unsynced') {
+ hasUnsyncedArg = true;
+ }
+ delete aWindow._zenStartupSyncFlag;
+ if (
+ !forcedSync &&
+ (hasUnsyncedArg ||
+ (typeof aWindow.arguments[0] === 'string' &&
+ aWindow.arguments.length > 1 &&
+ [...this.#browserWindows].length > 0))
+ ) {
+ this.log('Not syncing new window due to unsynced argument or existing synced windows');
+ aWindow.document.documentElement.setAttribute('zen-unsynced-window', 'true');
+ return;
+ }
+ aWindow.gZenWindowSync = this;
+ for (let eventName of EVENTS) {
+ aWindow.addEventListener(eventName, this, true);
+ }
+ }
+
+ /**
+ * @returns {string} A unique tab ID.
+ */
+ get #newTabSyncId() {
+ // Note: If this changes, make sure to also update the
+ // getExtTabGroupIdForInternalTabGroupId implementation in
+ // browser/components/extensions/parent/ext-browser.js.
+ // See: Bug 1960104 - Improve tab group ID generation in addTabGroup
+ // This is implemented from gBrowser.addTabGroup.
+ return `${Date.now()}-${Math.round(Math.random() * 100)}`;
+ }
+
+ /**
+ * Runs a callback function on all browser windows except the specified one.
+ *
+ * @param {Window} aWindow - The browser window to exclude.
+ * @param {Function} aCallback - The callback function to run on each window.
+ * @returns {any} The value returned by the callback function, if any.
+ */
+ #runOnAllWindows(aWindow, aCallback) {
+ for (let window of this.#browserWindows) {
+ if (window !== aWindow && !window._zenClosingWindow) {
+ let value = aCallback(window);
+ if (value) {
+ return value;
+ }
+ }
+ }
+ return null;
+ }
+
+ observe(aSubject, aTopic) {
+ switch (aTopic) {
+ case 'browser-window-before-show': {
+ this.#onWindowBeforeShow(aSubject);
+ break;
+ }
+ }
+ }
+
+ handleEvent(aEvent) {
+ const window = aEvent.currentTarget.ownerGlobal;
+ if (
+ !window.gZenStartup.isReady ||
+ window.gZenWorkspaces?.privateWindowOrDisabled ||
+ window._zenClosingWindow
+ ) {
+ return;
+ }
+ if (INSTANT_EVENTS.includes(aEvent.type)) {
+ return this.#handleNextEvent(aEvent);
+ }
+ if (this.#eventHandlingContext.window && this.#eventHandlingContext.window !== window) {
+ // We're already handling an event for another window.
+ // To avoid re-entrancy issues, we skip this event.
+ return;
+ }
+ const lastHandlerPromise = this.#eventHandlingContext.lastHandlerPromise;
+ this.#eventHandlingContext.eventCount++;
+ this.#eventHandlingContext.window = window;
+ let resolveNewPromise;
+ this.#eventHandlingContext.lastHandlerPromise = new Promise((resolve) => {
+ resolveNewPromise = resolve;
+ });
+ // Wait for the last handler to finish before processing the next event.
+ lastHandlerPromise.then(() => {
+ this.#handleNextEvent(aEvent).finally(() => {
+ if (--this.#eventHandlingContext.eventCount === 0) {
+ this.#eventHandlingContext.window = null;
+ }
+ resolveNewPromise();
+ });
+ });
+ }
+
+ /**
+ * Handles the next event by calling the appropriate handler method.
+ *
+ * @param {Event} aEvent - The event to handle.
+ */
+ #handleNextEvent(aEvent) {
+ const handler = `on_${aEvent.type}`;
+ try {
+ if (typeof this[handler] === 'function') {
+ return this[handler](aEvent) || Promise.resolve();
+ } else {
+ throw new Error(`No handler for event type: ${aEvent.type}`);
+ }
+ } catch (e) {
+ return Promise.reject(e);
+ }
+ }
+
+ /**
+ * Ensures that all synced tabs with a given ID has the same permanentKey.
+ * @param {Object} aTab - The tab to ensure sync for.
+ */
+ #makeSureTabSyncsPermanentKey(aTab) {
+ if (!aTab.id) {
+ return;
+ }
+ this.#runOnAllWindows(null, (win) => {
+ const tab = this.#getItemFromWindow(win, aTab.id);
+ if (tab) {
+ tab.permanentKey = aTab.linkedBrowser.permanentKey;
+ }
+ });
+ }
+
+ /**
+ * Retrieves a item element from a window by its ID.
+ *
+ * @param {Window} aWindow - The window containing the item.
+ * @param {string} aItemId - The ID of the item to retrieve.
+ * @returns {MozTabbrowserTab|MozTabbrowserTabGroup|null} The item element if found, otherwise null.
+ */
+ #getItemFromWindow(aWindow, aItemId) {
+ return aWindow.document.getElementById(aItemId);
+ }
+
+ /**
+ * Synchronizes a specific attribute from the original item to the target item.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to copy from.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to copy to.
+ * @param {string} aAttributeName - The name of the attribute to synchronize.
+ */
+ #maybeSyncAttributeChange(aOriginalItem, aTargetItem, aAttributeName) {
+ if (aOriginalItem.hasAttribute(aAttributeName)) {
+ aTargetItem.setAttribute(aAttributeName, aOriginalItem.getAttribute(aAttributeName));
+ } else {
+ aTargetItem.removeAttribute(aAttributeName);
+ }
+ }
+
+ /**
+ * Synchronizes the icon and label of the target tab with the original tab.
+ *
+ * @param {Object} aOriginalTab - The original tab to copy from.
+ * @param {Object} aTargetTab - The target tab to copy to.
+ * @param {Window} aWindow - The window containing the tabs.
+ * @param {number} flags - The sync flags indicating what to synchronize.
+ */
+ #syncItemWithOriginal(aOriginalItem, aTargetItem, aWindow, flags = 0) {
+ if (!aOriginalItem || !aTargetItem) {
+ return;
+ }
+ const { gBrowser, gZenFolders } = aWindow;
+ if (flags & SYNC_FLAG_ICON) {
+ if (gBrowser.isTab(aOriginalItem)) {
+ gBrowser.setIcon(aTargetItem, gBrowser.getIcon(aOriginalItem));
+ } else if (aOriginalItem.isZenFolder) {
+ // Icons are a zen-only feature for tab groups.
+ gZenFolders.setFolderUserIcon(aTargetItem, aOriginalItem.iconURL);
+ }
+ }
+ if (flags & SYNC_FLAG_LABEL) {
+ if (gBrowser.isTab(aOriginalItem)) {
+ aTargetItem._zenChangeLabelFlag = true;
+ gBrowser._setTabLabel(aTargetItem, aOriginalItem.label);
+ delete aTargetItem._zenChangeLabelFlag;
+ this.#maybeSyncAttributeChange(aOriginalItem, aTargetItem, 'zen-has-static-label');
+ } else if (gBrowser.isTabGroup(aOriginalItem)) {
+ aTargetItem.label = aOriginalItem.label;
+ }
+ }
+ if (flags & SYNC_FLAG_MOVE && !aTargetItem.hasAttribute('zen-empty-tab')) {
+ this.#maybeSyncAttributeChange(aOriginalItem, aTargetItem, 'zen-workspace-id');
+ this.#syncItemPosition(aOriginalItem, aTargetItem, aWindow);
+ }
+ if (gBrowser.isTab(aTargetItem)) {
+ lazy.TabStateFlusher.flush(aTargetItem.linkedBrowser);
+ }
+ }
+
+ /**
+ * Synchronizes the position of the target item with the original item.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to copy from.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to copy to.
+ * @param {Window} aWindow - The window containing the items.
+ */
+ #syncItemPosition(aOriginalItem, aTargetItem, aWindow) {
+ const { gBrowser, gZenPinnedTabManager } = aWindow;
+ const originalIsEssential = aOriginalItem.hasAttribute('zen-essential');
+ const targetIsEssential = aTargetItem.hasAttribute('zen-essential');
+ const originalIsPinned = aOriginalItem.pinned;
+ const targetIsPinned = aTargetItem.pinned;
+
+ const isGroup = gBrowser.isTabGroup(aOriginalItem);
+ const isTab = !isGroup;
+
+ if (isTab) {
+ if (originalIsEssential !== targetIsEssential) {
+ if (originalIsEssential) {
+ gZenPinnedTabManager.addToEssentials(aTargetItem);
+ } else {
+ gZenPinnedTabManager.removeEssentials(aTargetItem, /* unpin= */ !targetIsPinned);
+ }
+ } else if (originalIsPinned !== targetIsPinned) {
+ if (originalIsPinned) {
+ gBrowser.pinTab(aTargetItem);
+ } else {
+ gBrowser.unpinTab(aTargetItem);
+ }
+ }
+ } else {
+ aTargetItem.pinned = aOriginalItem.pinned;
+ }
+
+ this.#moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, {
+ isEssential: originalIsEssential,
+ isPinned: originalIsPinned,
+ });
+ }
+
+ /**
+ * Moves the target item to match the position of the original item.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aOriginalItem - The original item to match.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aTargetItem - The target item to move.
+ * @param {Window} aWindow - The window containing the items.
+ * @param {Object} options - Additional options for moving the item.
+ * @param {boolean} options.isEssential - Indicates if the item is essential.
+ * @param {boolean} options.isPinned - Indicates if the item is pinned.
+ */
+ #moveItemToMatchOriginal(aOriginalItem, aTargetItem, aWindow, { isEssential, isPinned }) {
+ const { gBrowser, gZenWorkspaces } = aWindow;
+ const originalSibling = aOriginalItem.previousElementSibling;
+ let isFirstTab = true;
+ if (gBrowser.isTabGroup(originalSibling) || gBrowser.isTab(originalSibling)) {
+ isFirstTab =
+ !originalSibling.hasAttribute('id') || originalSibling.hasAttribute('zen-empty-tab');
+ }
+
+ gBrowser.zenHandleTabMove(aOriginalItem, () => {
+ if (isFirstTab) {
+ let container;
+ const parentGroup = aOriginalItem.group;
+ if (parentGroup?.hasAttribute('id')) {
+ container = this.#getItemFromWindow(aWindow, parentGroup.getAttribute('id'));
+ if (container) {
+ if (container?.tabs?.length) {
+ // First tab in folders is the empty tab placeholder.
+ container.tabs[0].after(aTargetItem);
+ } else {
+ container.appendChild(aTargetItem);
+ }
+ return;
+ }
+ }
+ if (isEssential) {
+ container = gZenWorkspaces.getEssentialsSection(aTargetItem);
+ } else {
+ const workspaceId =
+ aTargetItem.getAttribute('zen-workspace-id') ||
+ aOriginalItem.ownerGlobal.gZenWorkspaces.activeWorkspace;
+ const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId);
+ container = isPinned
+ ? workspaceElement?.pinnedTabsContainer
+ : workspaceElement?.tabsContainer;
+ }
+ if (container) {
+ container.insertBefore(aTargetItem, container.firstChild);
+ }
+ return;
+ }
+ const relativeTab = this.#getItemFromWindow(aWindow, originalSibling.id);
+ if (relativeTab) {
+ relativeTab.after(aTargetItem);
+ }
+ });
+ }
+
+ /**
+ * Synchronizes a item across all browser windows.
+ *
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroup} aItem - The item to synchronize.
+ * @param {number} flags - The sync flags indicating what to synchronize.
+ */
+ #syncItemForAllWindows(aItem, flags = 0) {
+ const window = aItem.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ this.#syncItemWithOriginal(aItem, this.#getItemFromWindow(win, aItem.id), win, flags);
+ });
+ }
+
+ /**
+ * Swaps the browser docshells between two tabs.
+ *
+ * @param {Object} aOurTab - The tab in the current window.
+ * @param {Object} aOtherTab - The tab in the other window.
+ */
+ async #swapBrowserDocShellsAsync(aOurTab, aOtherTab) {
+ await this.#styleSwapedBrowsers(aOurTab, aOtherTab, () => {
+ this.#swapBrowserDocSheellsInner(aOurTab, aOtherTab);
+ });
+ }
+
+ /**
+ * Restores the tab progress listener for a given tab.
+ *
+ * @param {Object} aTab - The tab to restore the progress listener for.
+ * @param {Function} callback - The callback function to execute while the listener is removed.
+ * @param {boolean} onClose - Indicates if the swap is done during a tab close operation.
+ */
+ #withRestoreTabProgressListener(aTab, callback, onClose = false) {
+ const otherTabBrowser = aTab.ownerGlobal.gBrowser;
+ const otherBrowser = aTab.linkedBrowser;
+
+ // We aren't closing the other tab so, we also need to swap its tablisteners.
+ let filter = otherTabBrowser._tabFilters.get(aTab);
+ let tabListener = otherTabBrowser._tabListeners.get(aTab);
+ try {
+ otherBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(tabListener);
+ } catch {
+ /* ignore errors, we might have already removed them */
+ }
+
+ try {
+ callback();
+ } catch (e) {
+ console.error(e);
+ }
+
+ // Restore the listeners for the swapped in tab.
+ if (!onClose) {
+ tabListener = new otherTabBrowser.zenTabProgressListener(aTab, otherBrowser, true, false);
+ otherTabBrowser._tabListeners.set(aTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ otherBrowser.webProgress.addProgressListener(filter, notifyAll);
+ }
+ }
+
+ /**
+ * Swaps the browser docshells between two tabs.
+ *
+ * @param {Object} aOurTab - The tab in the current window.
+ * @param {Object} aOtherTab - The tab in the other window.
+ * @param {boolean} focus - Indicates if the tab should be focused after the swap.
+ * @param {boolean} onClose - Indicates if the swap is done during a tab close operation.
+ */
+ #swapBrowserDocSheellsInner(aOurTab, aOtherTab, focus = true, onClose = false) {
+ // Can't swap between chrome and content processes.
+ if (aOurTab.linkedBrowser.isRemoteBrowser != aOtherTab.linkedBrowser.isRemoteBrowser) {
+ return false;
+ }
+ // Load about:blank if by any chance we loaded the previous tab's URL.
+ // TODO: We should maybe start using a singular about:blank preloaded view
+ // to avoid loading a full blank page each time and wasting resources.
+ // We do need to do this though instead of just unloading the browser because
+ // firefox doesn't expect an unloaded + selected tab, so we need to get
+ // around this limitation somehow.
+ if (!onClose && aOurTab.linkedBrowser?.currentURI.spec !== 'about:blank') {
+ this.log(`Loading about:blank in our tab ${aOurTab.id} before swap`);
+ aOurTab.linkedBrowser.loadURI(Services.io.newURI('about:blank'), {
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY,
+ });
+ }
+ // Running `swapBrowsersAndCloseOther` doesn't expect us to use the tab after
+ // the operation, so it doesn't really care about cleaning up the other tab.
+ // We need to make a new tab progress listener for the other tab after the swap.
+ this.#withRestoreTabProgressListener(
+ aOtherTab,
+ () => {
+ this.log(`Swapping docshells between windows for tab ${aOurTab.id}`);
+ aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false);
+ this.#makeSureTabSyncsPermanentKey(aOurTab);
+ if (!aOtherTab.hasAttribute('busy')) {
+ aOurTab.removeAttribute('busy');
+ }
+ },
+ onClose
+ );
+ const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture', 'busy'];
+ // swapBrowsersAndCloseOther already takes care of transferring attributes like 'muted',
+ // but we need to manually remove some attributes from the other tab.
+ for (let attr of kAttributesToRemove) {
+ aOtherTab.removeAttribute(attr);
+ }
+ if (focus) {
+ // Recalculate the focus in order to allow the user to continue typing
+ // inside the web content area without having to click outside and back in.
+ aOurTab.linkedBrowser.blur();
+ aOurTab.ownerGlobal.gBrowser._adjustFocusAfterTabSwitch(aOurTab);
+ }
+ // Ensure the tab's state is flushed after the swap. By doing this,
+ // we can re-schedule another session store delayed process to fire.
+ // It's also important to note that if we don't flush the state here,
+ // we would start receiving invalid history changes from the the incorrect
+ // browser view that was just swapped out.
+ lazy.TabStateFlusher.flush(aOurTab.linkedBrowser);
+ return true;
+ }
+
+ /**
+ * Styles the swapped browsers to ensure proper visibility and layout.
+ *
+ * @param {Object} aOurTab - The tab in the current window.
+ * @param {Object} aOtherTab - The tab in the other window.
+ * @param {Function|undefined} callback - The callback function to execute after styling.
+ */
+ async #styleSwapedBrowsers(aOurTab, aOtherTab, callback = undefined) {
+ const ourBrowser = aOurTab.linkedBrowser;
+ const otherBrowser = aOtherTab.linkedBrowser;
+
+ if (callback) {
+ const browserBlob = await aOtherTab.ownerGlobal.PageThumbs.captureToBlob(
+ aOtherTab.linkedBrowser,
+ {
+ fullScale: true,
+ fullViewport: true,
+ }
+ );
+
+ let mySrc = await new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(browserBlob);
+ reader.onloadend = function () {
+ // result includes identifier 'data:image/png;base64,' plus the base64 data
+ resolve(reader.result);
+ };
+ reader.onerror = function () {
+ reject(new Error('Failed to read blob as data URL'));
+ };
+ });
+
+ const [img, loadPromise] = this.#createPseudoImageForBrowser(otherBrowser, mySrc);
+ // Run a reflow to ensure the image is rendered before hiding the browser.
+ void img.getBoundingClientRect();
+ await loadPromise;
+ otherBrowser.setAttribute('zen-pseudo-hidden', 'true');
+ callback();
+ }
+
+ this.#maybeRemovePseudoImageForBrowser(ourBrowser);
+ ourBrowser.removeAttribute('zen-pseudo-hidden');
+ }
+
+ /**
+ * Create and insert a new pseudo image for a browser element.
+ *
+ * @param {Object} aBrowser - The browser element to create the pseudo image for.
+ * @param {string} aSrc - The source URL of the image.
+ * @returns {Object} The created pseudo image element.
+ */
+ #createPseudoImageForBrowser(aBrowser, aSrc) {
+ const doc = aBrowser.ownerDocument;
+ const img = doc.createElement('img');
+ img.className = 'zen-pseudo-browser-image';
+ aBrowser.after(img);
+ const loadPromise = new Promise((resolve) => {
+ img.onload = () => resolve();
+ img.src = aSrc;
+ });
+ return [img, loadPromise];
+ }
+
+ /**
+ * Removes the pseudo image element for a browser if it exists.
+ *
+ * @param {Object} aBrowser - The browser element to remove the pseudo image for.
+ */
+ #maybeRemovePseudoImageForBrowser(aBrowser) {
+ const elements = aBrowser.parentNode?.querySelectorAll('.zen-pseudo-browser-image');
+ if (elements) {
+ elements.forEach((element) => element.remove());
+ }
+ }
+
+ /**
+ * Retrieves the active tab, where the web contents are being viewed
+ * from other windows by its ID.
+ *
+ * @param {Window} aWindow - The window to exclude.
+ * @param {string} aTabId - The ID of the tab to retrieve.
+ * @param {Function} filter - A function to filter the tabs.
+ * @returns {Object|null} The active tab from other windows if found, otherwise null.
+ */
+ #getActiveTabFromOtherWindows(aWindow, aTabId, filter = (tab) => tab?._zenContentsVisible) {
+ return this.#runOnAllWindows(aWindow, (win) => {
+ const tab = this.#getItemFromWindow(win, aTabId);
+ if (filter(tab)) {
+ return tab;
+ }
+ });
+ }
+
+ /**
+ * Moves all active tabs from the specified window to other windows.
+ *
+ * @param {Window} aWindow - The window to move active tabs from.
+ */
+ #moveAllActiveTabsToOtherWindows(aWindow) {
+ const mostRecentWindow = [...this.#browserWindows].find((win) => win !== aWindow);
+ if (!mostRecentWindow || !aWindow.gZenWorkspaces) {
+ return;
+ }
+ const activeTabsOnClosedWindow = aWindow.gZenWorkspaces.allStoredTabs.filter(
+ (tab) => tab._zenContentsVisible
+ );
+ for (let tab of activeTabsOnClosedWindow) {
+ const targetTab = this.#getItemFromWindow(mostRecentWindow, tab.id);
+ if (targetTab) {
+ targetTab._zenContentsVisible = true;
+ this.log(`Moving active tab ${tab.id} to most recent window on close`);
+ this.#swapBrowserDocSheellsInner(targetTab, tab, targetTab.selected, /* onClose =*/ true);
+ // We can animate later, whats important is to always stay on the same
+ // process and avoid async operations here to avoid the closed window
+ // being unloaded before the swap is done.
+ this.#styleSwapedBrowsers(targetTab, tab);
+ }
+ }
+ }
+
+ /**
+ * Handles tab switch or window focus events to synchronize tab contents visibility.
+ *
+ * @param {Window} aWindow - The window that triggered the event.
+ * @param {Object} aPreviousTab - The previously selected tab.
+ * @param {boolean} ignoreSameTab - Indicates if the same tab should be ignored.
+ */
+ async #onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null, ignoreSameTab = false) {
+ // On some occasions, such as when closing a window, this
+ // function might be called multiple times for the same tab.
+ if (aWindow.gBrowser.selectedTab === this.#lastSelectedTab && !ignoreSameTab) {
+ return;
+ }
+ if (aPreviousTab?._zenContentsVisible) {
+ const otherTabToShow = this.#getActiveTabFromOtherWindows(
+ aWindow,
+ aPreviousTab.id,
+ (tab) => tab?.selected
+ );
+ if (otherTabToShow) {
+ otherTabToShow._zenContentsVisible = true;
+ delete aPreviousTab._zenContentsVisible;
+ await this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab);
+ }
+ }
+ let promises = [];
+ for (const browserView of aWindow.gBrowser.selectedBrowsers) {
+ const selectedTab = aWindow.gBrowser.getTabForBrowser(browserView);
+ if (selectedTab._zenContentsVisible || selectedTab.hasAttribute('zen-empty-tab')) {
+ continue;
+ }
+ const otherSelectedTab = this.#getActiveTabFromOtherWindows(aWindow, selectedTab.id);
+ selectedTab._zenContentsVisible = true;
+ if (otherSelectedTab) {
+ delete otherSelectedTab._zenContentsVisible;
+ promises.push(this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab));
+ }
+ }
+ await Promise.all(promises);
+ }
+
+ /**
+ * Delegates generic sync events to synchronize tabs across windows.
+ *
+ * @param {Event} aEvent - The event to delegate.
+ * @param {number} flags - The sync flags indicating what to synchronize.
+ */
+ #delegateGenericSyncEvent(aEvent, flags = 0) {
+ const item = aEvent.target;
+ this.#syncItemForAllWindows(item, flags);
+ }
+
+ /**
+ * Retrieves the tab state for a given tab.
+ *
+ * @param {Object} tab - The tab to retrieve the state for.
+ * @returns {Object} The tab state.
+ */
+ #getTabState(tab) {
+ return JSON.parse(lazy.SessionStore.getTabState(tab));
+ }
+
+ /* Mark: Public API */
+
+ /**
+ * Sets the initial pinned state for a tab across all windows.
+ *
+ * @param {Object} aTab - The tab to set the pinned state for.
+ */
+ setPinnedTabState(aTab) {
+ const state = this.#getTabState(aTab);
+ const initialState = {
+ entry: state.entries[state.index - 1],
+ image: state.image,
+ };
+ this.#runOnAllWindows(null, (win) => {
+ const targetTab = this.#getItemFromWindow(win, aTab.id);
+ if (targetTab) {
+ targetTab._zenPinnedInitialState = initialState;
+ }
+ });
+ }
+
+ /**
+ * Propagates the workspaces to all windows.
+ * @param {Array} aWorkspaces - The workspaces to propagate.
+ */
+ propagateWorkspacesToAllWindows(aWorkspaces) {
+ this.#runOnAllWindows(null, (win) => {
+ win.gZenWorkspaces.propagateWorkspaces(aWorkspaces);
+ });
+ }
+
+ /**
+ * Moves all tabs from a window to a synced workspace in another window.
+ * If no synced window exists, creates a new one.
+ *
+ * @param {Window} aWindow - The window to move tabs from.
+ * @param {string} aWorkspaceId - The ID of the workspace to move tabs to.
+ */
+ moveTabsToSyncedWorkspace(aWindow, aWorkspaceId) {
+ const tabsToMove = aWindow.gZenWorkspaces.allStoredTabs.filter(
+ (tab) => !tab.hasAttribute('zen-empty-tab')
+ );
+ const selectedTab = aWindow.gBrowser.selectedTab;
+ let win = [...this.#browserWindows][0];
+ const moveAllTabsToWindow = async (allowSelected = false) => {
+ const { gBrowser, gZenWorkspaces } = win;
+ win.focus();
+ let success = true;
+ for (const tab of tabsToMove) {
+ if (tab !== selectedTab || allowSelected) {
+ const newTab = gBrowser.adoptTab(tab, { tabIndex: Infinity });
+ if (!newTab) {
+ // The adoption failed. Restore "fadein" and don't increase the index.
+ tab.setAttribute('fadein', 'true');
+ success = false;
+ continue;
+ }
+ newTab._zenContentsVisible = true;
+ gBrowser.setTabTitle(newTab);
+ gZenWorkspaces.moveTabToWorkspace(newTab, aWorkspaceId);
+ }
+ }
+ if (success) {
+ aWindow.close();
+ await gZenWorkspaces.changeWorkspaceWithID(aWorkspaceId);
+ gBrowser.selectedBrowser.focus();
+ }
+ };
+ if (!win) {
+ this.log('No synced window found, creating a new one');
+ win = aWindow.gBrowser.replaceTabWithWindow(selectedTab, {}, /* zenForceSync = */ true);
+ win.gZenWorkspaces.promiseInitialized.then(() => {
+ moveAllTabsToWindow();
+ });
+ return;
+ }
+ moveAllTabsToWindow(true);
+ }
+
+ /* Mark: Event Handlers */
+
+ on_TabOpen(aEvent) {
+ const tab = aEvent.target;
+ const window = tab.ownerGlobal;
+ if (tab.id) {
+ // This tab was opened as part of a sync operation.
+ return;
+ }
+ tab._zenContentsVisible = true;
+ tab.id = this.#newTabSyncId;
+ this.#runOnAllWindows(window, (win) => {
+ const newTab = win.gBrowser.addTrustedTab('about:blank', {
+ animate: true,
+ createLazyBrowser: true,
+ zenWorkspaceId: tab.getAttribute('zen-workspace-id') || '',
+ _forZenEmptyTab: tab.hasAttribute('zen-empty-tab'),
+ });
+ newTab.id = tab.id;
+ this.#syncItemWithOriginal(
+ tab,
+ newTab,
+ win,
+ SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE
+ );
+ });
+ this.#makeSureTabSyncsPermanentKey(tab);
+ }
+
+ on_ZenTabIconChanged(aEvent) {
+ if (!aEvent.target?._zenContentsVisible) {
+ // No need to sync icon changes for tabs that aren't active in this window.
+ return;
+ }
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON);
+ }
+
+ on_ZenTabLabelChanged(aEvent) {
+ if (!aEvent.target?._zenContentsVisible) {
+ // No need to sync label changes for tabs that aren't active in this window.
+ return;
+ }
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL);
+ }
+
+ on_TabMove(aEvent) {
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_MOVE);
+ }
+
+ on_TabPinned(aEvent) {
+ const tab = aEvent.target;
+ // There are cases where the pinned state is changed but we don't
+ // wan't to override the initial state we stored when the tab was created.
+ // For example, when session restore pins a tab again.
+ if (!tab._zenPinnedInitialState) {
+ this.setPinnedTabState(tab);
+ }
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabUnpinned(aEvent) {
+ const tab = aEvent.target;
+ this.#runOnAllWindows(null, (win) => {
+ const targetTab = this.#getItemFromWindow(win, tab.id);
+ if (targetTab) {
+ delete targetTab._zenPinnedInitialState;
+ }
+ });
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabAddedToEssentials(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabRemovedFromEssentials(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabClose(aEvent) {
+ const tab = aEvent.target;
+ const window = tab.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ const targetTab = this.#getItemFromWindow(win, tab.id);
+ if (targetTab) {
+ win.gBrowser.removeTab(targetTab, { animate: true });
+ }
+ });
+ }
+
+ on_focus(aEvent) {
+ const { ownerGlobal: window } = aEvent.target;
+ if (
+ !window.gBrowser ||
+ this.#lastFocusedWindow?.deref() === window ||
+ window.closing ||
+ !window.toolbar.visible
+ ) {
+ return;
+ }
+ this.#lastFocusedWindow = new WeakRef(window);
+ this.#lastSelectedTab = new WeakRef(window.gBrowser.selectedTab);
+ return this.#onTabSwitchOrWindowFocus(window);
+ }
+
+ on_TabSelect(aEvent) {
+ const tab = aEvent.target;
+ if (this.#lastSelectedTab?.deref() === tab) {
+ return;
+ }
+ this.#lastSelectedTab = new WeakRef(tab);
+ const previousTab = aEvent.detail.previousTab;
+ return this.#onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab);
+ }
+
+ on_SSWindowClosing(aEvent) {
+ const window = aEvent.target.ownerGlobal;
+ window._zenClosingWindow = true;
+ for (let eventName of EVENTS) {
+ window.removeEventListener(eventName, this);
+ }
+ delete window.gZenWindowSync;
+ this.#moveAllActiveTabsToOtherWindows(window);
+ }
+
+ on_TabGroupCreate(aEvent) {
+ const tabGroup = aEvent.target;
+ if (tabGroup.id && tabGroup.alreadySynced) {
+ // This tab group was opened as part of a sync operation.
+ return;
+ }
+ const window = tabGroup.ownerGlobal;
+ const isFolder = tabGroup.isZenFolder;
+ const isSplitView = tabGroup.hasAttribute('split-view-group');
+ if (isSplitView) {
+ return; // Split view groups are synced via ZenSplitViewTabsSplit event.
+ }
+ // Tab groups already have an ID upon creation.
+ this.#runOnAllWindows(window, (win) => {
+ const newGroup = isFolder
+ ? win.gZenFolders.createFolder([], {})
+ : win.gBrowser.addTabGroup([]);
+ newGroup.id = tabGroup.id;
+ newGroup.alreadySynced = true;
+ this.#syncItemWithOriginal(
+ tabGroup,
+ newGroup,
+ win,
+ SYNC_FLAG_ICON | SYNC_FLAG_LABEL | SYNC_FLAG_MOVE
+ );
+ });
+ }
+
+ on_TabGroupRemoved(aEvent) {
+ const tabGroup = aEvent.target;
+ const window = tabGroup.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ const targetGroup = this.#getItemFromWindow(win, tabGroup.id);
+ if (targetGroup) {
+ if (targetGroup.isZenFolder) {
+ targetGroup.delete();
+ } else {
+ win.gBrowser.removeTabGroup(targetGroup, { isUserTriggered: true });
+ }
+ }
+ });
+ }
+
+ on_TabGroupMoved(aEvent) {
+ return this.on_TabMove(aEvent);
+ }
+
+ on_TabGroupUpdate(aEvent) {
+ return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON | SYNC_FLAG_LABEL);
+ }
+
+ on_ZenTabRemovedFromSplit(aEvent) {
+ const tab = aEvent.target;
+ const window = tab.ownerGlobal;
+ this.#runOnAllWindows(window, (win) => {
+ const targetTab = this.#getItemFromWindow(win, tab.id);
+ if (targetTab && win.gZenViewSplitter) {
+ win.gZenViewSplitter.removeTabFromGroup(targetTab);
+ }
+ });
+ }
+
+ on_ZenSplitViewTabsSplit(aEvent) {
+ const tabGroup = aEvent.target;
+ const window = tabGroup.ownerGlobal;
+ const tabs = tabGroup.tabs;
+ this.#runOnAllWindows(window, (win) => {
+ const otherWindowTabs = tabs
+ .map((tab) => this.#getItemFromWindow(win, tab.id))
+ .filter(Boolean);
+ if (otherWindowTabs.length > 0 && win.gZenViewSplitter) {
+ const group = win.gZenViewSplitter.splitTabs(otherWindowTabs, 'grid', -1);
+ if (group) {
+ let otherTabGroup = group.tabs[0].group;
+ otherTabGroup.id = tabGroup.id;
+ this.#syncItemWithOriginal(aEvent.target, otherTabGroup, win, SYNC_FLAG_MOVE);
+ }
+ }
+ });
+
+ return this.#onTabSwitchOrWindowFocus(window, null, /* ignoreSameTab = */ true);
+ }
+}
+
+export const ZenWindowSync = new nsZenWindowSync();
diff --git a/src/zen/sessionstore/moz.build b/src/zen/sessionstore/moz.build
new file mode 100644
index 0000000000..188f4c27ce
--- /dev/null
+++ b/src/zen/sessionstore/moz.build
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXTRA_JS_MODULES.zen += [
+ "ZenSessionManager.sys.mjs",
+ "ZenWindowSync.sys.mjs",
+]
diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs
index 89488cd436..8fd03aa9ab 100644
--- a/src/zen/split-view/ZenViewSplitter.mjs
+++ b/src/zen/split-view/ZenViewSplitter.mjs
@@ -202,6 +202,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.resetTabState(tab, forUnsplit);
if (tab.group && tab.group.hasAttribute('split-view-group')) {
gBrowser.ungroupTab(tab);
+ this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab);
}
if (group.tabs.length < 2) {
// We need to remove all remaining tabs from the group when unsplitting
@@ -895,6 +896,21 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
}
+ /**
+ * Dispatches a custom event on a tab.
+ *
+ * @param {string} eventName - The name of the event to dispatch.
+ * @param {HTMLElement} item - The item on which to dispatch the event.
+ */
+ #dispatchItemEvent(eventName, item) {
+ const event = new CustomEvent(eventName, {
+ detail: { item },
+ bubbles: true,
+ cancelable: false,
+ });
+ item.dispatchEvent(event);
+ }
+
/**
* Removes a group.
*
@@ -905,6 +921,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
for (const tab of group.tabs.reverse()) {
if (tab.group?.hasAttribute('split-view-group')) {
gBrowser.ungroupTab(tab);
+ this.#dispatchItemEvent('ZenTabRemovedFromSplit', tab);
}
}
if (this.currentView === groupIndex) {
@@ -1067,9 +1084,13 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
*
* @param {Tab[]} tabs - The tabs to split.
* @param {string|undefined} gridType - The type of grid layout.
+ * @param {number} initialIndex - The index of the initially active tab.
+ * use -1 to avoid selecting any tab.
+ * @return {object|undefined} The split view data or undefined if the split was not performed.
*/
splitTabs(tabs, gridType, initialIndex = 0) {
- this.#withoutSplitViewTransition(() => {
+ const tabIndexToUse = Math.max(0, initialIndex);
+ return this.#withoutSplitViewTransition(() => {
// TODO: Add support for splitting essential tabs
tabs = tabs.filter((t) => !t.hidden && !t.hasAttribute('zen-empty-tab'));
if (tabs.length < 2 || tabs.length > this.MAX_TABS) {
@@ -1078,7 +1099,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const existingSplitTab = tabs.find((tab) => tab.splitView);
if (existingSplitTab) {
- this._moveTabsToContainer(tabs, tabs[initialIndex]);
+ this._moveTabsToContainer(tabs, tabs[tabIndexToUse]);
const groupIndex = this._data.findIndex((group) => group.tabs.includes(existingSplitTab));
const group = this._data[groupIndex];
const gridTypeChange = gridType && group.gridType !== gridType;
@@ -1105,7 +1126,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
this.activateSplitView(group, true);
- return;
+ this.#dispatchItemEvent('ZenSplitViewTabsSplit', group);
+ return group;
}
// We are here if none of the tabs have been previously split
@@ -1132,8 +1154,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
layoutTree: this.calculateLayoutTree(tabs, gridType),
};
this._data.push(splitData);
- if (!this._sessionRestoring) {
- window.gBrowser.selectedTab = tabs[initialIndex] ?? tabs[0];
+ if (!this._sessionRestoring && initialIndex >= 0) {
+ window.gBrowser.selectedTab = tabs[tabIndexToUse] ?? tabs[0];
}
// Add tabs to the split view group
@@ -1150,6 +1172,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
return;
}
this.activateSplitView(splitData);
+ this.#dispatchItemEvent('ZenSplitViewTabsSplit', splitGroup);
+ return splitData;
});
}
@@ -1855,9 +1879,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
// We can't create an empty group, so only create if we have tabs
+ let group = null;
if (tabs?.length) {
// Create a new group with the initial tabs
- gBrowser.addTabGroup(tabs, {
+ group = gBrowser.addTabGroup(tabs, {
label: '',
showCreateUI: false,
insertBefore: tabs[0],
@@ -1865,7 +1890,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
});
}
- return null;
+ return group;
}
storeDataForSessionStore() {
@@ -1936,7 +1961,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
#withoutSplitViewTransition(callback) {
this.tabBrowserPanel.classList.add('zen-split-view-no-transition');
try {
- callback();
+ return callback();
} finally {
requestAnimationFrame(() => {
this.tabBrowserPanel.classList.remove('zen-split-view-no-transition');
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index c922d11663..182fa63ebf 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -7,23 +7,7 @@ import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components
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 = [];
@@ -76,12 +60,11 @@ class ZenPinnedTabsObserver {
}
class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
- hasInitializedPins = false;
promiseInitializedPinned = new Promise((resolve) => {
this._resolvePinnedInitializedInternal = resolve;
});
- async init() {
+ init() {
if (!this.enabled) {
return;
}
@@ -103,43 +86,16 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
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})`);
}
}
_onTabResetPinButton(event, tab) {
event.stopPropagation();
- const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (!pin) {
- return;
- }
- let userContextId;
- if (tab.hasAttribute('usercontextid')) {
- userContextId = tab.getAttribute('usercontextid');
- }
- const pinnedUrl = Services.io.newURI(pin.url);
- const browser = tab.linkedBrowser;
- browser.loadURI(pinnedUrl, {
- triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
- userContextId,
- }),
- });
- this.resetPinChangedUrl(tab);
+ this._resetTabToStoredState(tab);
}
get enabled() {
@@ -150,260 +106,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
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;
- }
- }, 100);
- }
-
- 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;
@@ -413,236 +115,24 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
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, action);
- 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, action) {
- 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);
- switch (action) {
- case 'ZenFolderRenamed':
- case 'ZenFolderIconChanged':
- case 'TabGroupCollapse':
- case 'TabGroupExpand':
- break;
- default:
- for (const item of group.allItems) {
- if (gBrowser.isTabGroup(item)) {
- await this.#updateGroupInfo(item, action);
- } 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 },
- })
- );
+ #getTabState(tab) {
+ return JSON.parse(SessionStore.getTabState(tab));
}
async _onTabClick(e) {
@@ -668,110 +158,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
async replacePinnedUrlWithCurrent(tab = undefined) {
tab ??= TabContextMenu.contextTab;
- if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) {
- return;
- }
-
- const browser = tab.linkedBrowser;
-
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
-
- if (!pin) {
+ if (!tab || !tab.pinned) {
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);
+ window.gZenWindowSync.setPinnedTabState(tab);
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');
@@ -780,21 +175,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
- 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,
@@ -841,7 +221,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
switch (behavior) {
case 'close': {
for (const tab of pinnedTabs) {
- this._removePinnedAttributes(tab, true);
gBrowser.removeTab(tab, { animate: true });
}
break;
@@ -943,35 +322,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
_resetTabToStoredState(tab) {
- const id = tab.getAttribute('zen-pin-id');
- if (!id) {
- return;
- }
+ const state = this.#getTabState(tab);
- const pin = this._pinsCache.find((pin) => pin.uuid === id);
- if (!pin) {
- return;
- }
+ const initialState = tab._zenPinnedInitialState;
- const tabState = SessionStore.getTabState(tab);
- const state = JSON.parse(tabState);
-
- const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url);
- if (foundEntryIndex === -1) {
- state.entries = [
- {
- url: pin.url,
- title: pin.title,
- triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
- },
- ];
- } else {
- // Remove everything except the entry we want to keep
- const existingEntry = state.entries[foundEntryIndex];
- existingEntry.title = pin.title;
- state.entries = [existingEntry];
- }
- state.image = pin.iconUrl || state.image;
+ // Remove everything except the entry we want to keep
+ state.entries = [initialState.entry];
+
+ state.image = initialState.image;
state.index = 0;
SessionStore.setTabState(tab, state);
@@ -1016,22 +374,15 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
if (tab.hasAttribute('zen-workspace-id')) {
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);
- }
+ if (tab.pinned) {
gBrowser.zenHandleTabMove(tab, () => {
if (tab.ownerGlobal !== window) {
tab = gBrowser.adoptTab(tab, {
selectTab: tab.selected,
});
tab.setAttribute('zen-essential', 'true');
- } else {
- section.appendChild(tab);
}
+ section.appendChild(tab);
});
} else {
gBrowser.pinTab(tab);
@@ -1124,8 +475,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
const isVisible = contextTab.pinned && !contextTab.multiselected;
const zenAddEssential = document.getElementById('context_zen-add-essential');
- document.getElementById('context_zen-reset-pinned-tab').hidden =
- !isVisible || !contextTab.getAttribute('zen-pin-id');
+ document.getElementById('context_zen-reset-pinned-tab').hidden = !isVisible;
document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible;
zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group;
zenAddEssential.setAttribute(
@@ -1261,24 +611,25 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
- async onLocationChange(browser) {
+ onLocationChange(browser) {
const tab = gBrowser.getTabForBrowser(browser);
- if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) {
- return;
- }
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (!pin) {
+ if (
+ !tab ||
+ !tab.pinned ||
+ tab.hasAttribute('zen-essential') ||
+ !tab._zenPinnedInitialState?.entry
+ ) {
return;
}
// Remove # and ? from the URL
- const pinUrl = pin.url.split('#')[0];
+ const pinUrl = tab._zenPinnedInitialState.entry.url.split('#')[0];
const currentUrl = browser.currentURI.spec.split('#')[0];
// Add an indicator that the pin has been changed
if (pinUrl === currentUrl) {
this.resetPinChangedUrl(tab);
return;
}
- this.pinHasChangedUrl(tab, pin);
+ this.pinHasChangedUrl(tab);
}
resetPinChangedUrl(tab) {
@@ -1290,7 +641,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
tab.style.removeProperty('--zen-original-tab-icon');
}
- pinHasChangedUrl(tab, pin) {
+ pinHasChangedUrl(tab) {
if (tab.hasAttribute('zen-pinned-changed')) {
return;
}
@@ -1299,7 +650,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
} else {
tab.setAttribute('zen-pinned-changed', 'true');
}
- tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`);
+ tab.style.setProperty('--zen-original-tab-icon', `url(${tab._zenPinnedInitialState.image})`);
}
removeTabContainersDragoverClass(hideIndicator = true) {
@@ -1415,39 +766,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
}
- async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = 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');
-
- // update the label for the same pin across all windows
- for (const browser of browsers) {
- const tabs = browser.gBrowser.tabs;
- // Fix pinned cache for the browser
- const browserCache = browser.gZenPinnedTabManager?._pinsCache;
- if (browserCache) {
- const pin = browserCache.find((pin) => pin.uuid === uuid);
- if (pin) {
- pin.title = newTitle;
- pin.editedTitle = isEdited;
- }
- }
- for (let i = 0; i < tabs.length; i++) {
- const tabToEdit = tabs[i];
- if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) {
- tabToEdit.removeAttribute('zen-has-static-label');
- if (isEdited) {
- gBrowser._setTabLabel(tabToEdit, newTitle);
- tabToEdit.setAttribute('zen-has-static-label', 'true');
- } else {
- gBrowser.setTabTitle(tabToEdit);
- }
- break;
- }
- }
+ async updatePinTitle(tab, newTitle, isEdited = true) {
+ tab.removeAttribute('zen-has-static-label');
+ if (isEdited) {
+ gBrowser._setTabLabel(tab, newTitle);
+ tab.setAttribute('zen-has-static-label', 'true');
+ } else {
+ gBrowser.setTabTitle(tab);
}
}
@@ -1563,19 +888,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
- async onTabLabelChanged(tab) {
- if (!this._pinsCache) {
- return;
- }
- // If our current pin in the cache point to about:blank, we need to update the entry
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (!pin) {
- return;
- }
-
- if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') {
- await this.replacePinnedUrlWithCurrent(tab);
- }
+ onTabLabelChanged(tab) {
+ tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } }));
}
}
diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs
deleted file mode 100644
index a407dad353..0000000000
--- a/src/zen/tabs/ZenPinnedTabsStorage.mjs
+++ /dev/null
@@ -1,660 +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/.
-
-window.ZenPinnedTabsStorage = {
- _saveCache: [],
-
- 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} 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) {
- // If we find the exact same pin in the cache, skip saving
- const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid);
- const copy = { ...pin };
- if (existingIndex !== -1) {
- const existingPin = this._saveCache[existingIndex];
- const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]);
- if (isSame) {
- return; // No changes, skip saving
- } else {
- // Update the cached pin
- this._saveCache[existingIndex] = { ...copy };
- }
- } else {
- // Add to cache
- this._saveCache.push(copy);
- }
-
- 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} 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 cachedIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === uuid);
- if (cachedIndex !== -1) {
- this._saveCache.splice(cachedIndex, 1);
- }
-
- 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();
-});
diff --git a/src/zen/tabs/jar.inc.mn b/src/zen/tabs/jar.inc.mn
index 2acab4816b..192d0d33d2 100644
--- a/src/zen/tabs/jar.inc.mn
+++ b/src/zen/tabs/jar.inc.mn
@@ -2,7 +2,6 @@
# 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/.
- 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)
\ No newline at end of file
diff --git a/src/zen/tests/container_essentials/browser_container_auto_switch.js b/src/zen/tests/container_essentials/browser_container_auto_switch.js
index b46279668b..1e9a94826c 100644
--- a/src/zen/tests/container_essentials/browser_container_auto_switch.js
+++ b/src/zen/tests/container_essentials/browser_container_auto_switch.js
@@ -6,7 +6,7 @@
add_task(async function test_Container_Essentials_Auto_Swithc() {
await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1);
const workspaces = await gZenWorkspaces._workspaces();
- ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.');
+ ok(workspaces.length === 2, 'Two workspaces should exist.');
let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', {
skipAnimation: true,
@@ -27,11 +27,11 @@ add_task(async function test_Container_Essentials_Auto_Swithc() {
const newWorkspaceUUID = gZenWorkspaces.activeWorkspace;
Assert.equal(
gZenWorkspaces.activeWorkspace,
- workspaces.workspaces[1].uuid,
+ workspaces[1].uuid,
'The new workspace should be active.'
);
// Change to the original workspace, there should be no essential tabs
- await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]);
+ await gZenWorkspaces.changeWorkspace(workspaces[0]);
await gZenWorkspaces.removeWorkspace(newWorkspaceUUID);
});
diff --git a/src/zen/tests/container_essentials/browser_container_specific_essentials.js b/src/zen/tests/container_essentials/browser_container_specific_essentials.js
index a376c01651..3d0b82ec84 100644
--- a/src/zen/tests/container_essentials/browser_container_specific_essentials.js
+++ b/src/zen/tests/container_essentials/browser_container_specific_essentials.js
@@ -6,9 +6,9 @@
add_task(async function test_Check_Creation() {
await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1);
const workspaces = await gZenWorkspaces._workspaces();
- ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.');
+ ok(workspaces.length === 2, 'Two workspaces should exist.');
- await gZenWorkspaces.changeWorkspace(workspaces.workspaces[1]);
+ await gZenWorkspaces.changeWorkspace(workspaces[1]);
let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', {
skipAnimation: true,
userContextId: 1,
@@ -28,7 +28,7 @@ add_task(async function test_Check_Creation() {
const newWorkspaceUUID = gZenWorkspaces.activeWorkspace;
// Change to the original workspace, there should be no essential tabs
- await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]);
+ await gZenWorkspaces.changeWorkspace(workspaces[0]);
ok(
!gBrowser.tabs.find(
(t) => t.hasAttribute('zen-essential') && t.getAttribute('usercontextid') == 1
diff --git a/src/zen/tests/folders/browser_folder_owner_tabs.js b/src/zen/tests/folders/browser_folder_owner_tabs.js
index 00de0f44a7..7af7c963a5 100644
--- a/src/zen/tests/folders/browser_folder_owner_tabs.js
+++ b/src/zen/tests/folders/browser_folder_owner_tabs.js
@@ -31,9 +31,6 @@ add_task(async function test_Duplicate_Tab_Inside_Folder() {
for (const t of folder.tabs) {
ok(t.pinned, 'All tabs in the folder should be pinned');
- if (!t.hasAttribute('zen-empty-tab')) {
- ok(t.hasAttribute('zen-pin-id'), 'All non-empty tabs should have a zen-pinned-id attribute');
- }
}
gBrowser.selectedTab = selectedTab;
diff --git a/src/zen/tests/pinned/browser.toml b/src/zen/tests/pinned/browser.toml
index b26d96f321..22082e1f5f 100644
--- a/src/zen/tests/pinned/browser.toml
+++ b/src/zen/tests/pinned/browser.toml
@@ -13,14 +13,9 @@ prefs = ["zen.workspaces.separate-essentials=false"]
["browser_pinned_close.js"]
["browser_pinned_changed.js"]
["browser_pinned_created.js"]
-["browser_pinned_edit_label.js"]
-["browser_pinned_removed.js"]
-["browser_pinned_reorder_changed_label.js"]
-["browser_pinned_reordered.js"]
["browser_pinned_to_essential.js"]
["browser_private_mode_no_essentials.js"]
["browser_private_mode_no_ctx_menu.js"]
-["browser_issue_7654.js"]
["browser_issue_8726.js"]
diff --git a/src/zen/tests/pinned/browser_issue_7654.js b/src/zen/tests/pinned/browser_issue_7654.js
deleted file mode 100644
index d982a910f4..0000000000
--- a/src/zen/tests/pinned/browser_issue_7654.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- https://creativecommons.org/publicdomain/zero/1.0/ */
-
-'use strict';
-
-ChromeUtils.defineESModuleGetters(this, {
- UrlbarTestUtils: 'resource://testing-common/UrlbarTestUtils.sys.mjs',
-});
-
-add_task(async function test_Search_Pinned_Title() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- const customLabel = 'ZEN ROCKS';
-
- await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
- const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function () {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- await gZenPinnedTabManager.updatePinTitle(tab, customLabel, true);
-
- const pinnedTabs = await ZenPinnedTabsStorage.getPins();
- const pinObject = pinnedTabs.find((pin) => pin.uuid === pinTabID);
- Assert.equal(pinObject.title, customLabel, 'The pin object should have the correct title');
-
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/2', true);
-
- await UrlbarTestUtils.promiseAutocompleteResultPopup({
- window,
- value: customLabel,
- waitForFocus: SimpleTest.waitForFocus,
- });
-
- const total = UrlbarTestUtils.getResultCount(window);
- info(`Found ${total} matches`);
-
- const result = await UrlbarTestUtils.getDetailsOfResultAt(window, 1);
-
- const url = result?.url;
- Assert.equal(
- url,
- 'https://example.com/1',
- `Should have the found result '${url}' in the expected list of entries`
- );
- Assert.equal(
- result?.title,
- customLabel,
- `Should have the found result '${result?.title}' in the expected list of entries`
- );
-
- BrowserTestUtils.removeTab(gBrowser.selectedTab);
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.pinTab(tab);
- await promise;
- });
-});
diff --git a/src/zen/tests/pinned/browser_pinned_changed.js b/src/zen/tests/pinned/browser_pinned_changed.js
index 576c16946d..f2ffb466fa 100644
--- a/src/zen/tests/pinned/browser_pinned_changed.js
+++ b/src/zen/tests/pinned/browser_pinned_changed.js
@@ -11,27 +11,20 @@ add_task(async function test_Changed_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
+ gBrowser.pinTab(tab);
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
+ ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
+
+ BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
+ await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should have a zen-pinned-changed attribute after being pinned'
+ );
+ resolvePromise();
+ }, 0);
- BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
- await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should have a zen-pinned-changed attribute after being pinned'
- );
- resolvePromise();
- }, 0);
- },
- { once: true }
- );
- gBrowser.pinTab(tab);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_pinned_close.js b/src/zen/tests/pinned/browser_pinned_close.js
index c3c009ae2b..b283a8c10b 100644
--- a/src/zen/tests/pinned/browser_pinned_close.js
+++ b/src/zen/tests/pinned/browser_pinned_close.js
@@ -15,20 +15,13 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
- document.getElementById('cmd_close').doCommand();
- setTimeout(() => {
- ok(tab.closing, 'The tab should be closing after being closed');
- resolvePromise();
- }, 100);
- },
- { once: true }
- );
gBrowser.pinTab(tab);
+
+ document.getElementById('cmd_close').doCommand();
+ setTimeout(() => {
+ ok(tab.closing, 'The tab should be closing after being closed');
+ resolvePromise();
+ }, 100);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_pinned_created.js b/src/zen/tests/pinned/browser_pinned_created.js
index 6a8070aa15..bc7658fcbc 100644
--- a/src/zen/tests/pinned/browser_pinned_created.js
+++ b/src/zen/tests/pinned/browser_pinned_created.js
@@ -12,38 +12,24 @@ add_task(async function test_Create_Pinned() {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- try {
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
- Assert.equal(
- pinObject.url,
- 'https://example.com/',
- 'The pin object should have the correct URL'
- );
- Assert.equal(
- pinObject.workspaceUuid,
- gZenWorkspaces.activeWorkspace,
- 'The pin object should have the correct workspace UUID'
- );
- } catch (error) {
- ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
- }
-
- resolvePromise();
- },
- { once: true }
- );
gBrowser.pinTab(newTab);
+ ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
+
+ try {
+ const pinObject = newTab.__zenPinnedInitialState;
+ ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
+ Assert.equal(
+ pinObject.entry.url,
+ 'https://example.com/',
+ 'The pin object should have the correct URL'
+ );
+ } catch (error) {
+ ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
+ }
+
+ resolvePromise();
+
await promise;
await BrowserTestUtils.removeTab(newTab);
});
diff --git a/src/zen/tests/pinned/browser_pinned_edit_label.js b/src/zen/tests/pinned/browser_pinned_edit_label.js
deleted file mode 100644
index e03cb7cb87..0000000000
--- a/src/zen/tests/pinned/browser_pinned_edit_label.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- https://creativecommons.org/publicdomain/zero/1.0/ */
-
-'use strict';
-
-add_task(async function test_Create_Pinned() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- const customLabel = 'Test Label';
-
- await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/' }, async (browser) => {
- const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(tab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- await gZenPinnedTabManager.updatePinTitle(tab, customLabel, true);
-
- const pinnedTabs = await ZenPinnedTabsStorage.getPins();
- const pinObject = pinnedTabs.find((pin) => pin.uuid === pinTabID);
- Assert.equal(pinObject.title, customLabel, 'The pin object should have the correct title');
- Assert.equal(
- pinObject.url,
- 'https://example.com/',
- 'The pin object should have the correct URL'
- );
-
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.pinTab(tab);
- await promise;
- });
-});
diff --git a/src/zen/tests/pinned/browser_pinned_nounload_reset.js b/src/zen/tests/pinned/browser_pinned_nounload_reset.js
index 9930b02681..8effbee1f4 100644
--- a/src/zen/tests/pinned/browser_pinned_nounload_reset.js
+++ b/src/zen/tests/pinned/browser_pinned_nounload_reset.js
@@ -15,37 +15,26 @@ add_task(async function test_NoUnload_Changed_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
- await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should have a zen-pinned-changed attribute after being pinned'
- );
- document.getElementById('cmd_close').doCommand();
- setTimeout(() => {
- ok(
- !tab.hasAttribute('zen-pinned-changed'),
- 'The tab should not have a zen-pinned-changed attribute after being closed'
- );
- ok(
- !tab.hasAttribute('discarded'),
- 'The tab should not be discarded after being closed'
- );
- ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
- resolvePromise();
- }, 100);
- }, 0);
- },
- { once: true }
- );
gBrowser.pinTab(tab);
+
+ BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
+ await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should have a zen-pinned-changed attribute after being pinned'
+ );
+ document.getElementById('cmd_close').doCommand();
+ setTimeout(() => {
+ ok(
+ !tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should not have a zen-pinned-changed attribute after being closed'
+ );
+ ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
+ ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
+ resolvePromise();
+ }, 100);
+ }, 0);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_pinned_removed.js b/src/zen/tests/pinned/browser_pinned_removed.js
deleted file mode 100644
index 4e4bc79599..0000000000
--- a/src/zen/tests/pinned/browser_pinned_removed.js
+++ /dev/null
@@ -1,52 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- https://creativecommons.org/publicdomain/zero/1.0/ */
-
-'use strict';
-
-add_task(async function test_Remove_Pinned() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
-
- const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
- newTab.addEventListener(
- 'ZenPinnedTabRemoved',
- async function (event) {
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- ok(
- !pinObject,
- 'The pin object should not exist in the ZenPinnedTabsStorage after removal'
- );
- ok(
- !newTab.hasAttribute('zen-pin-id'),
- 'The tab should not have a zen-pin-id attribute after removal'
- );
- ok(!newTab.pinned, 'The tab should not be pinned after removal');
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.unpinTab(newTab);
- },
- { once: true }
- );
- gBrowser.pinTab(newTab);
-
- await promise;
- await BrowserTestUtils.removeTab(newTab);
-});
diff --git a/src/zen/tests/pinned/browser_pinned_reorder_changed_label.js b/src/zen/tests/pinned/browser_pinned_reorder_changed_label.js
deleted file mode 100644
index 10908c565f..0000000000
--- a/src/zen/tests/pinned/browser_pinned_reorder_changed_label.js
+++ /dev/null
@@ -1,126 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- https://creativecommons.org/publicdomain/zero/1.0/ */
-
-'use strict';
-
-add_task(async function test_Pinned_Reorder_Changed_Label() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- const tabsToRemove = [];
- for (let i = 0; i < 3; i++) {
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- gBrowser.pinTab(gBrowser.selectedTab);
- tabsToRemove.push(gBrowser.selectedTab);
- }
-
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- tabsToRemove.push(gBrowser.selectedTab);
-
- const customLabel = 'Test Label';
-
- const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- await gZenPinnedTabManager.updatePinTitle(newTab, customLabel, true);
-
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
-
- newTab.addEventListener(
- 'ZenPinnedTabMoved',
- async function (event) {
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- Assert.equal(
- pinObject.title,
- customLabel,
- 'The pin object should have the correct title'
- );
- Assert.equal(
- pinObject.position,
- 2,
- 'The pin object should have the correct position after moving'
- );
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.moveTabTo(newTab, { tabIndex: 2 });
- },
- { once: true }
- );
- gBrowser.pinTab(newTab);
-
- await promise;
- for (const tab of tabsToRemove) {
- await BrowserTestUtils.removeTab(tab);
- }
-});
-
-add_task(async function test_Pinned_Reorder_Changed_Label() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- const tabsToRemove = [];
- for (let i = 0; i < 3; i++) {
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- gBrowser.pinTab(gBrowser.selectedTab);
- tabsToRemove.push(gBrowser.selectedTab);
- }
-
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- tabsToRemove.push(gBrowser.selectedTab);
-
- const customLabel = 'Test Label';
-
- const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- newTab.addEventListener(
- 'ZenPinnedTabMoved',
- async function (event) {
- await gZenPinnedTabManager.updatePinTitle(newTab, customLabel, true);
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- Assert.equal(
- pinObject.title,
- customLabel,
- 'The pin object should have the correct title'
- );
- Assert.equal(
- pinObject.position,
- 1,
- 'The pin object should have the correct position after moving'
- );
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.moveTabTo(newTab, { tabIndex: 1 });
- },
- { once: true }
- );
- gBrowser.pinTab(newTab);
-
- await promise;
- for (const tab of tabsToRemove) {
- await BrowserTestUtils.removeTab(tab);
- }
-});
diff --git a/src/zen/tests/pinned/browser_pinned_reordered.js b/src/zen/tests/pinned/browser_pinned_reordered.js
deleted file mode 100644
index 6fc523b8bd..0000000000
--- a/src/zen/tests/pinned/browser_pinned_reordered.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- https://creativecommons.org/publicdomain/zero/1.0/ */
-
-'use strict';
-
-add_task(async function test_Create_Pinned() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- const tabsToRemove = [];
- for (let i = 0; i < 3; i++) {
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- gBrowser.pinTab(gBrowser.selectedTab);
- tabsToRemove.push(gBrowser.selectedTab);
- }
-
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- tabsToRemove.push(gBrowser.selectedTab);
-
- const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- const startIndex = pinObject.position;
- Assert.greater(startIndex, 0, 'The pin object should have the correct start index');
-
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.pinTab(newTab);
-
- await promise;
- for (const tab of tabsToRemove) {
- await BrowserTestUtils.removeTab(tab);
- }
-});
-
-add_task(async function test_Create_Pinned() {
- let resolvePromise;
- const promise = new Promise((resolve) => {
- resolvePromise = resolve;
- });
-
- const tabsToRemove = [];
- for (let i = 0; i < 3; i++) {
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- gBrowser.pinTab(gBrowser.selectedTab);
- tabsToRemove.push(gBrowser.selectedTab);
- }
-
- await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
- tabsToRemove.push(gBrowser.selectedTab);
-
- const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- newTab.addEventListener(
- 'ZenPinnedTabMoved',
- async function (event) {
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- Assert.equal(
- pinObject.position,
- 0,
- 'The pin object should have the correct position after moving'
- );
- resolvePromise();
- },
- { once: true }
- );
- gBrowser.moveTabTo(newTab, { tabIndex: 0 });
- },
- { once: true }
- );
- gBrowser.pinTab(newTab);
-
- await promise;
- for (const tab of tabsToRemove) {
- await BrowserTestUtils.removeTab(tab);
- }
-});
diff --git a/src/zen/tests/pinned/browser_pinned_reset_noswitch.js b/src/zen/tests/pinned/browser_pinned_reset_noswitch.js
index 969e68f31d..458e62af96 100644
--- a/src/zen/tests/pinned/browser_pinned_reset_noswitch.js
+++ b/src/zen/tests/pinned/browser_pinned_reset_noswitch.js
@@ -15,37 +15,26 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
- await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should have a zen-pinned-changed attribute after being pinned'
- );
- document.getElementById('cmd_close').doCommand();
- setTimeout(() => {
- ok(
- !tab.hasAttribute('zen-pinned-changed'),
- 'The tab should not have a zen-pinned-changed attribute after being closed'
- );
- ok(
- !tab.hasAttribute('discarded'),
- 'The tab should not be discarded after being closed'
- );
- ok(tab === gBrowser.selectedTab, 'The tab should not be selected after being closed');
- resolvePromise();
- }, 100);
- }, 0);
- },
- { once: true }
- );
gBrowser.pinTab(tab);
+
+ BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
+ await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should have a zen-pinned-changed attribute after being pinned'
+ );
+ document.getElementById('cmd_close').doCommand();
+ setTimeout(() => {
+ ok(
+ !tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should not have a zen-pinned-changed attribute after being closed'
+ );
+ ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
+ ok(tab === gBrowser.selectedTab, 'The tab should not be selected after being closed');
+ resolvePromise();
+ }, 100);
+ }, 0);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_pinned_switch.js b/src/zen/tests/pinned/browser_pinned_switch.js
index d0a2caa569..8d62e5fc42 100644
--- a/src/zen/tests/pinned/browser_pinned_switch.js
+++ b/src/zen/tests/pinned/browser_pinned_switch.js
@@ -15,37 +15,26 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
- await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should have a zen-pinned-changed attribute after being pinned'
- );
- document.getElementById('cmd_close').doCommand();
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should not have a zen-pinned-changed attribute after being closed'
- );
- ok(
- !tab.hasAttribute('discarded'),
- 'The tab should not be discarded after being closed'
- );
- ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
- resolvePromise();
- }, 100);
- }, 0);
- },
- { once: true }
- );
gBrowser.pinTab(tab);
+
+ BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
+ await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should have a zen-pinned-changed attribute after being pinned'
+ );
+ document.getElementById('cmd_close').doCommand();
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should not have a zen-pinned-changed attribute after being closed'
+ );
+ ok(!tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
+ ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
+ resolvePromise();
+ }, 100);
+ }, 0);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_pinned_to_essential.js b/src/zen/tests/pinned/browser_pinned_to_essential.js
index 51315384dd..2a605c1be1 100644
--- a/src/zen/tests/pinned/browser_pinned_to_essential.js
+++ b/src/zen/tests/pinned/browser_pinned_to_essential.js
@@ -12,26 +12,18 @@ add_task(async function test_Pinned_To_Essential() {
await BrowserTestUtils.openNewForegroundTab(window.gBrowser, 'https://example.com/', true);
const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- gZenPinnedTabManager.addToEssentials(newTab);
- ok(
- newTab.hasAttribute('zen-essential') && newTab.parentNode.getAttribute('container') == '0',
- 'New tab should be marked as essential.'
- );
-
- resolvePromise();
- },
- { once: true }
- );
gBrowser.pinTab(newTab);
+ ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
+
+ gZenPinnedTabManager.addToEssentials(newTab);
+ ok(
+ newTab.hasAttribute('zen-essential') && newTab.parentNode.getAttribute('container') == '0',
+ 'New tab should be marked as essential.'
+ );
+
+ resolvePromise();
+
await promise;
await BrowserTestUtils.removeTab(newTab);
});
diff --git a/src/zen/tests/pinned/browser_pinned_unload_changed.js b/src/zen/tests/pinned/browser_pinned_unload_changed.js
index 5d7bc6f539..ca4d77cf4f 100644
--- a/src/zen/tests/pinned/browser_pinned_unload_changed.js
+++ b/src/zen/tests/pinned/browser_pinned_unload_changed.js
@@ -15,35 +15,27 @@ add_task(async function test_Unload_Changed_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
+ gBrowser.pinTab(tab);
- BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
- await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should have a zen-pinned-changed attribute after being pinned'
- );
- document.getElementById('cmd_close').doCommand();
- setTimeout(() => {
- ok(
- !tab.hasAttribute('zen-pinned-changed'),
- 'The tab should not have a zen-pinned-changed attribute after being closed'
- );
+ BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
+ await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should have a zen-pinned-changed attribute after being pinned'
+ );
+ document.getElementById('cmd_close').doCommand();
+ setTimeout(() => {
+ ok(
+ !tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should not have a zen-pinned-changed attribute after being closed'
+ );
- ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
- ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
- resolvePromise();
- }, 100);
- }, 0);
- },
- { once: true }
- );
- gBrowser.pinTab(tab);
+ ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
+ ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
+ resolvePromise();
+ }, 100);
+ }, 0);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_pinned_unload_noreset.js b/src/zen/tests/pinned/browser_pinned_unload_noreset.js
index 538c84bac5..0163a5a73f 100644
--- a/src/zen/tests/pinned/browser_pinned_unload_noreset.js
+++ b/src/zen/tests/pinned/browser_pinned_unload_noreset.js
@@ -15,34 +15,26 @@ add_task(async function test_Unload_NoReset_Pinned() {
await BrowserTestUtils.withNewTab({ gBrowser, url: 'https://example.com/1' }, async (browser) => {
const tab = gBrowser.getTabForBrowser(browser);
- tab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- const pinTabID = tab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
- await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should have a zen-pinned-changed attribute after being pinned'
- );
- document.getElementById('cmd_close').doCommand();
- setTimeout(() => {
- ok(
- tab.hasAttribute('zen-pinned-changed'),
- 'The tab should not have a zen-pinned-changed attribute after being closed'
- );
- ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
- ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
- resolvePromise();
- }, 100);
- }, 0);
- },
- { once: true }
- );
gBrowser.pinTab(tab);
+
+ BrowserTestUtils.startLoadingURIString(browser, 'https://example.com/2');
+ await BrowserTestUtils.browserLoaded(browser, false, 'https://example.com/2');
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should have a zen-pinned-changed attribute after being pinned'
+ );
+ document.getElementById('cmd_close').doCommand();
+ setTimeout(() => {
+ ok(
+ tab.hasAttribute('zen-pinned-changed'),
+ 'The tab should not have a zen-pinned-changed attribute after being closed'
+ );
+ ok(tab.hasAttribute('discarded'), 'The tab should not be discarded after being closed');
+ ok(tab != gBrowser.selectedTab, 'The tab should not be selected after being closed');
+ resolvePromise();
+ }, 100);
+ }, 0);
await promise;
});
});
diff --git a/src/zen/tests/pinned/browser_private_mode_no_essentials.js b/src/zen/tests/pinned/browser_private_mode_no_essentials.js
index 06b178289c..8de324771f 100644
--- a/src/zen/tests/pinned/browser_private_mode_no_essentials.js
+++ b/src/zen/tests/pinned/browser_private_mode_no_essentials.js
@@ -11,36 +11,29 @@ add_task(async function test_Private_Mode_No_Essentials() {
});
const newTab = gBrowser.selectedTab;
- newTab.addEventListener(
- 'ZenPinnedTabCreated',
- async function (event) {
- ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
-
- const pinTabID = newTab.getAttribute('zen-pin-id');
- ok(pinTabID, 'The tab should have a zen-pin-id attribute after being pinned');
-
- try {
- const pins = await ZenPinnedTabsStorage.getPins();
- const pinObject = pins.find((pin) => pin.uuid === pinTabID);
- ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
-
- let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
- private: true,
- });
- await privateWindow.gZenWorkspaces.promiseInitialized;
- ok(
- !privateWindow.gBrowser.tabs.some((tab) => tab.pinned),
- 'Private window should not have any pinned tabs initially'
- );
-
- await BrowserTestUtils.closeWindow(privateWindow);
- } catch (error) {
- ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
- }
- resolvePromise();
- },
- { once: true }
- );
+ gBrowser.pinTab(newTab);
+
+ ok(newTab.pinned, 'The tab should be pinned after calling gBrowser.pinTab()');
+
+ try {
+ const pins = await ZenPinnedTabsStorage.getPins();
+ const pinObject = pins.find((pin) => pin.uuid === pinTabID);
+ ok(pinObject, 'The pin object should exist in the ZenPinnedTabsStorage');
+
+ let privateWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await privateWindow.gZenWorkspaces.promiseInitialized;
+ ok(
+ !privateWindow.gBrowser.tabs.some((tab) => tab.pinned),
+ 'Private window should not have any pinned tabs initially'
+ );
+
+ await BrowserTestUtils.closeWindow(privateWindow);
+ } catch (error) {
+ ok(false, 'Error while checking the pin object in ZenPinnedTabsStorage: ' + error);
+ }
+ resolvePromise();
gZenPinnedTabManager.addToEssentials(newTab);
await promise;
diff --git a/src/zen/tests/welcome/browser_welcome.js b/src/zen/tests/welcome/browser_welcome.js
index d780ce9f13..3b95fab201 100644
--- a/src/zen/tests/welcome/browser_welcome.js
+++ b/src/zen/tests/welcome/browser_welcome.js
@@ -162,7 +162,6 @@ add_task(async function test_Welcome_Steps() {
);
Assert.equal(tab.group, group, 'Pinned tabs should belong to the first tab group');
}
- ok(tab.hasAttribute('zen-pin-id'), 'Pinned tabs should have a zen-pin-id attribute');
}
}
group.delete();
diff --git a/src/zen/tests/workspaces/browser_basic_workspaces.js b/src/zen/tests/workspaces/browser_basic_workspaces.js
index 2a6ac2017e..dc0bc77675 100644
--- a/src/zen/tests/workspaces/browser_basic_workspaces.js
+++ b/src/zen/tests/workspaces/browser_basic_workspaces.js
@@ -9,9 +9,9 @@ add_task(async function test_Check_Creation() {
const currentWorkspaceUUID = gZenWorkspaces.activeWorkspace;
await gZenWorkspaces.createAndSaveWorkspace('Test Workspace 2');
const workspaces = await gZenWorkspaces._workspaces();
- ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.');
+ ok(workspaces.length === 2, 'Two workspaces should exist.');
ok(
- currentWorkspaceUUID !== workspaces.workspaces[1].uuid,
+ currentWorkspaceUUID !== workspaces[1].uuid,
'The new workspace should be different from the current one.'
);
@@ -26,7 +26,7 @@ add_task(async function test_Check_Creation() {
const workspacesAfterRemove = await gZenWorkspaces._workspaces();
ok(workspacesAfterRemove.workspaces.length === 1, 'One workspace should exist.');
ok(
- workspacesAfterRemove.workspaces[0].uuid === currentWorkspaceUUID,
+ workspacesAfterRemove[0].uuid === currentWorkspaceUUID,
'The workspace should be the one we started with.'
);
ok(gBrowser.tabs.length === 2, 'There should be one tab.');
diff --git a/src/zen/tests/workspaces/browser_change_to_empty.js b/src/zen/tests/workspaces/browser_change_to_empty.js
index c7f6f90b74..9913a988f0 100644
--- a/src/zen/tests/workspaces/browser_change_to_empty.js
+++ b/src/zen/tests/workspaces/browser_change_to_empty.js
@@ -19,6 +19,6 @@ add_task(async function test_Change_To_Empty() {
);
const workspacesAfterRemove = await gZenWorkspaces._workspaces();
- ok(workspacesAfterRemove.workspaces.length === 1, 'One workspace should exist.');
+ ok(workspacesAfterRemove.length === 1, 'One workspace should exist.');
ok(gBrowser.tabs.length === 2, 'There should be two tabs.');
});
diff --git a/src/zen/tests/workspaces/browser_workspace_bookmarks.js b/src/zen/tests/workspaces/browser_workspace_bookmarks.js
index 02734f5dd1..4a9e7ec489 100644
--- a/src/zen/tests/workspaces/browser_workspace_bookmarks.js
+++ b/src/zen/tests/workspaces/browser_workspace_bookmarks.js
@@ -107,9 +107,9 @@ add_task(async function test_workspace_bookmark() {
await withBookmarksShowing(async () => {
await gZenWorkspaces.createAndSaveWorkspace('Test Workspace 2');
const workspaces = await gZenWorkspaces._workspaces();
- ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.');
- const firstWorkspace = workspaces.workspaces[0];
- const secondWorkspace = workspaces.workspaces[1];
+ ok(workspaces.length === 2, 'Two workspaces should exist.');
+ const firstWorkspace = workspaces[0];
+ const secondWorkspace = workspaces[1];
ok(
firstWorkspace.uuid !== secondWorkspace.uuid,
'The new workspace should be different from the current one.'
diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs
index 12d2883469..bc161200a9 100644
--- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs
+++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs
@@ -76,7 +76,7 @@ const globalActionsTemplate = [
command: 'cmd_zenWorkspaceForward',
icon: 'chrome://browser/skin/zen-icons/forward.svg',
isAvailable: (window) => {
- return window.gZenWorkspaces._workspaceCache.workspaces.length > 1;
+ return window.gZenWorkspaces._workspaceCache.length > 1;
},
},
{
@@ -85,7 +85,7 @@ const globalActionsTemplate = [
icon: 'chrome://browser/skin/zen-icons/back.svg',
isAvailable: (window) => {
// This also covers the case of being in private mode
- return window.gZenWorkspaces._workspaceCache.workspaces.length > 1;
+ return window.gZenWorkspaces._workspaceCache.length > 1;
},
},
{
diff --git a/src/zen/welcome/ZenWelcome.mjs b/src/zen/welcome/ZenWelcome.mjs
index 3e1074bf54..36c680ef26 100644
--- a/src/zen/welcome/ZenWelcome.mjs
+++ b/src/zen/welcome/ZenWelcome.mjs
@@ -292,9 +292,6 @@
for (const tab of _tabsToPin) {
tab.setAttribute('zen-workspace-id', gZenWorkspaces.activeWorkspace);
gBrowser.pinTab(tab);
- await new Promise((resolve) => {
- tab.addEventListener('ZenPinnedTabCreated', resolve, { once: true });
- });
}
for (const tab of _tabsToPinEssentials) {
tab.removeAttribute('pending'); // Make it appear loaded
diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs
index 5ece76491e..a1495d4edc 100644
--- a/src/zen/workspaces/ZenGradientGenerator.mjs
+++ b/src/zen/workspaces/ZenGradientGenerator.mjs
@@ -1318,7 +1318,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
// Use theme from workspace object or passed theme
let workspaceTheme = theme || workspace.theme;
- await this.foreachWindowAsActive(async (browser) => {
+ await this.forEachWindow(async (browser) => {
if (!browser.gZenThemePicker?.promiseInitialized) {
return;
}
@@ -1332,7 +1332,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
}
// Do not rebuild if the workspace is not the same as the current one
- const windowWorkspace = await browser.gZenWorkspaces.getActiveWorkspace();
+ const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace();
if (windowWorkspace.uuid !== uuid) {
return;
}
@@ -1630,13 +1630,12 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
};
});
const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture);
- let currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
+ let currentWorkspace = gZenWorkspaces.getActiveWorkspace();
if (!skipSave) {
- await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient);
- await gZenWorkspaces._propagateWorkspaceData();
+ currentWorkspace.theme = gradient;
+ gZenWorkspaces.saveWorkspace(currentWorkspace);
gZenUIManager.showToast('zen-panel-ui-gradient-generator-saved-message');
- currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
}
await this.onWorkspaceChange(currentWorkspace, skipSave, skipSave ? gradient : null);
@@ -1691,7 +1690,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
invalidateGradientCache() {
this.#gradientsCache = {};
- window.dispatchEvent(new Event('ZenGradientCacheChanged'));
+ window.dispatchEvent(new Event('ZenGradientCacheChanged', { bubbles: true }));
}
getGradientForWorkspace(workspace) {
diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs
index ce6baa122b..7cb1af16db 100644
--- a/src/zen/workspaces/ZenWorkspace.mjs
+++ b/src/zen/workspaces/ZenWorkspace.mjs
@@ -37,6 +37,14 @@ class nsZenWorkspace extends MozXULElement {
`;
}
+ static get moveTabToButtonMarkup() {
+ return `
+
+ `;
+ }
+
static get inheritedAttributes() {
return {
'.zen-workspace-tabs-section': 'zen-workspace-id=id',
@@ -88,6 +96,20 @@ class nsZenWorkspace extends MozXULElement {
gZenWorkspaces.changeWorkspaceIcon();
});
+ if (!gZenWorkspaces.currentWindowIsSyncing) {
+ let actionsButton = this.indicator.querySelector('.zen-workspaces-actions');
+ const moveTabToFragment = window.MozXULElement.parseXULToFragment(
+ nsZenWorkspace.moveTabToButtonMarkup
+ );
+ actionsButton.after(moveTabToFragment);
+ actionsButton.setAttribute('hidden', 'true');
+ actionsButton = actionsButton.nextElementSibling;
+ actionsButton.addEventListener('command', (event) => {
+ event.stopPropagation();
+ this.#openMoveTabsToWorkspacePanel(event.target);
+ });
+ }
+
this.scrollbox._getScrollableElements = () => {
const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children];
if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) {
@@ -209,7 +231,7 @@ class nsZenWorkspace extends MozXULElement {
if (newName === '') {
return;
}
- let workspaces = (await gZenWorkspaces._workspaces()).workspaces;
+ let workspaces = gZenWorkspaces.getWorkspaces();
let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid);
workspaceData.name = newName;
await gZenWorkspaces.saveWorkspace(workspaceData);
@@ -256,6 +278,36 @@ class nsZenWorkspace extends MozXULElement {
this.style.removeProperty('--toolbox-textcolor');
this.style.removeProperty('--zen-primary-color');
}
+
+ #openMoveTabsToWorkspacePanel(button) {
+ button = button.closest('toolbarbutton');
+ if (!button) return;
+
+ const popup = document.getElementById('zenMoveTabsToSyncedWorkspacePopup');
+ popup.innerHTML = '';
+
+ const workspaces = gZenWorkspaces.getWorkspaces(true);
+ for (const workspace of workspaces) {
+ const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace);
+ item.addEventListener('command', async () => {
+ const { ZenWindowSync } = ChromeUtils.importESModule(
+ 'resource:///modules/zen/ZenWindowSync.sys.mjs'
+ );
+ ZenWindowSync.moveTabsToSyncedWorkspace(window, workspace.uuid);
+ });
+ popup.appendChild(item);
+ }
+
+ button.setAttribute('open', 'true');
+ popup.addEventListener(
+ 'popuphidden',
+ () => {
+ button.removeAttribute('open');
+ },
+ { once: true }
+ );
+ popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */);
+ }
}
customElements.define('zen-workspace', nsZenWorkspace);
diff --git a/src/zen/workspaces/ZenWorkspaceCreation.mjs b/src/zen/workspaces/ZenWorkspaceCreation.mjs
index cbaeed4c2b..d037bade7a 100644
--- a/src/zen/workspaces/ZenWorkspaceCreation.mjs
+++ b/src/zen/workspaces/ZenWorkspaceCreation.mjs
@@ -199,7 +199,7 @@ class nsZenWorkspaceCreation extends MozXULElement {
}
async onCreateButtonCommand() {
- const workspace = await gZenWorkspaces.getActiveWorkspace();
+ const workspace = gZenWorkspaces.getActiveWorkspace();
workspace.name = this.inputName.value.trim();
workspace.icon = this.inputIcon.image || this.inputIcon.label || undefined;
workspace.containerTabId = this.currentProfile;
@@ -320,8 +320,8 @@ class nsZenWorkspaceCreation extends MozXULElement {
this.remove();
gZenUIManager.updateTabsToolbar();
- const workspace = await gZenWorkspaces.getActiveWorkspace();
- await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true);
+ const workspace = gZenWorkspaces.getActiveWorkspace();
+ await gZenWorkspaces._organizeWorkspaceStripLocations(workspace);
await gZenWorkspaces.updateTabsContainers();
await gZenUIManager.motion.animate(
diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs
index f9186c43f2..9672c4a273 100644
--- a/src/zen/workspaces/ZenWorkspaceIcons.mjs
+++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs
@@ -126,13 +126,13 @@ class nsZenWorkspaceIcons extends MozXULElement {
}
async #updateIcons() {
- const workspaces = await gZenWorkspaces._workspaces();
+ const workspaces = gZenWorkspaces.getWorkspaces();
this.innerHTML = '';
- for (const workspace of workspaces.workspaces) {
+ for (const workspace of workspaces) {
const button = this.#createWorkspaceIcon(workspace);
this.appendChild(button);
}
- if (workspaces.workspaces.length <= 1) {
+ if (workspaces.length <= 1) {
this.setAttribute('dont-show', 'true');
} else {
this.removeAttribute('dont-show');
@@ -168,6 +168,9 @@ class nsZenWorkspaceIcons extends MozXULElement {
}
i++;
}
+ if (selected == -1) {
+ return;
+ }
buttons[selected].setAttribute('active', true);
this.scrollLeft = buttons[selected].offsetLeft - 10;
this.setAttribute('selected', selected);
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index 5d87a17c49..9b3941cc95 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -2,18 +2,20 @@
// 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/.
-import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
import { nsZenThemePicker } from 'chrome://browser/content/zen-components/ZenGradientGenerator.mjs';
-class nsZenWorkspaces extends nsZenMultiWindowFeature {
+class nsZenWorkspaces {
/**
* Stores workspace IDs and their last selected tabs.
*/
- _lastSelectedWorkspaceTabs = {};
- _inChangingWorkspace = false;
+ lastSelectedWorkspaceTabs = {};
+ #inChangingWorkspace = false;
draggedElement = null;
+ #hasInitialized = false;
+
#canDebug = Services.prefs.getBoolPref('zen.workspaces.debug', false);
+ #activeWorkspace = '';
_swipeState = {
isGestureActive: true,
@@ -21,7 +23,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
direction: null,
};
- _lastScrollTime = 0;
+ _workspaceCache = [];
+
+ #lastScrollTime = 0;
bookmarkMenus = [
'PlacesToolbar',
@@ -31,18 +35,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
'BMB_mobileBookmarks',
];
- promiseDBInitialized = new Promise((resolve) => {
- this._resolveDBInitialized = resolve;
- });
-
promisePinnedInitialized = new Promise((resolve) => {
this._resolvePinnedInitialized = resolve;
});
- promiseSectionsInitialized = new Promise((resolve) => {
- this._resolveSectionsInitialized = resolve;
- });
-
promiseInitialized = new Promise((resolve) => {
this._resolveInitialized = resolve;
});
@@ -51,11 +47,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (this.privateWindowOrDisabled) {
return;
}
- await Promise.all([
- this.promiseDBInitialized,
- this.promisePinnedInitialized,
- SessionStore.promiseAllWindowsRestored,
- ]);
+ await Promise.all([this.promisePinnedInitialized, SessionStore.promiseAllWindowsRestored]);
}
async init() {
@@ -116,7 +108,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
ChromeUtils.defineLazyGetter(this, 'workspaceIcons', () =>
document.getElementById('zen-workspaces-button')
);
- this._activeWorkspace = Services.prefs.getStringPref('zen.workspaces.active', '');
+ this.#activeWorkspace ||= Services.prefs.getStringPref('zen.workspaces.active', '');
if (this.isPrivateWindow) {
document.documentElement.setAttribute('zen-private-window', 'true');
@@ -128,8 +120,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this.addPopupListeners();
await this.#waitForPromises();
- await this._workspaces();
-
await this.afterLoadInit();
}
@@ -144,8 +134,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
await this.delayedStartup();
}
this._initializeWorkspaceTabContextMenus();
- await this.initializeWorkspaces();
- await this.promiseSectionsInitialized;
// Non UI related initializations
if (
@@ -156,24 +144,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this.initializeGestureHandlers();
this.initializeWorkspaceNavigation();
}
-
- if (!this.privateWindowOrDisabled) {
- const observerFunction = async () => {
- this._workspaceBookmarksCache = null;
- await this.workspaceBookmarks();
- this._invalidateBookmarkContainers();
- };
- Services.obs.addObserver(this, 'weave:engine:sync:finish');
- Services.obs.addObserver(observerFunction, 'workspace-bookmarks-updated');
- window.addEventListener(
- 'unload',
- () => {
- Services.obs.removeObserver(this, 'weave:engine:sync:finish');
- Services.obs.removeObserver(observerFunction, 'workspace-bookmarks-updated');
- },
- { once: true }
- );
- }
}
// Validate browser state before tab operations
@@ -314,18 +284,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
});
};
this._pinnedTabsResizeObserver = new ResizeObserver(onResize);
- await this._createDefaultWorkspaceIfNeeded();
- }
-
- async _createDefaultWorkspaceIfNeeded() {
- const workspaces = await this._workspaces();
- if (!workspaces.workspaces.length) {
- await this.createAndSaveWorkspace('Space', null, true);
- this._workspaceCache = null;
+ if (this.privateWindowOrDisabled) {
+ await this.restoreWorkspacesFromSessionStore({});
}
}
- _initializeEmptyTab() {
+ #initializeEmptyTab() {
for (const tab of gBrowser.tabs) {
// Check if session store has an empty tab
if (tab.hasAttribute('zen-empty-tab') && !tab.pinned) {
@@ -405,49 +369,43 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return document.getElementById(workspaceId);
}
- async initializeTabsStripSections() {
+ async #initializeTabsStripSections() {
await SessionStore.promiseInitialized;
await SessionStore.promiseAllWindowsRestored;
const perifery = document.getElementById('tabbrowser-arrowscrollbox-periphery');
perifery.setAttribute('hidden', 'true');
- await new Promise((resolve) => {
- setTimeout(async () => {
- const tabs = gBrowser.tabContainer.allTabs;
- const workspaces = await this._workspaces();
- for (const workspace of workspaces.workspaces) {
- await this._createWorkspaceTabsSection(workspace, tabs);
- }
- if (tabs.length) {
- const defaultSelectedContainer = this.workspaceElement(
- this.activeWorkspace
- )?.querySelector('.zen-workspace-normal-tabs-section');
- const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector(
- '.zen-workspace-pinned-tabs-section'
- );
- // New profile with no workspaces does not have a default selected container
- if (defaultSelectedContainer) {
- for (const tab of tabs) {
- if (tab.hasAttribute('zen-essential')) {
- this.getEssentialsSection(tab).appendChild(tab);
- continue;
- } else if (tab.pinned) {
- pinnedContainer.insertBefore(tab, pinnedContainer.lastChild);
- continue;
- }
- // before to the last child (perifery)
- defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild);
- }
+ const tabs = gBrowser.tabContainer.allTabs;
+ const workspaces = this.getWorkspaces();
+ for (const workspace of workspaces) {
+ await this.#createWorkspaceTabsSection(workspace, tabs);
+ }
+ if (tabs.length) {
+ const defaultSelectedContainer = this.workspaceElement(this.activeWorkspace)?.querySelector(
+ '.zen-workspace-normal-tabs-section'
+ );
+ const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector(
+ '.zen-workspace-pinned-tabs-section'
+ );
+ // New profile with no workspaces does not have a default selected container
+ if (defaultSelectedContainer) {
+ for (const tab of tabs) {
+ if (tab.hasAttribute('zen-essential')) {
+ this.getEssentialsSection(tab).appendChild(tab);
+ continue;
+ } else if (tab.pinned) {
+ pinnedContainer.insertBefore(tab, pinnedContainer.lastChild);
+ continue;
}
- gBrowser.tabContainer._invalidateCachedTabs();
+ // before to the last child (perifery)
+ defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild);
}
- perifery.setAttribute('hidden', 'true');
- this._hasInitializedTabsStrip = true;
- this.registerPinnedResizeObserver();
- this._fixIndicatorsNames(workspaces);
- this._resolveSectionsInitialized();
- resolve();
- }, 0);
- });
+ }
+ gBrowser.tabContainer._invalidateCachedTabs();
+ }
+ perifery.setAttribute('hidden', 'true');
+ this._hasInitializedTabsStrip = true;
+ this.registerPinnedResizeObserver();
+ this._fixIndicatorsNames(workspaces);
}
getEssentialsSection(container = 0) {
@@ -485,7 +443,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return this.getEssentialsSection(currentWorkspace?.containerTabId);
}
- async _createWorkspaceTabsSection(workspace, tabs = []) {
+ async #createWorkspaceTabsSection(workspace, tabs = []) {
const workspaceWrapper = document.createXULElement('zen-workspace');
const container = document.getElementById('tabbrowser-arrowscrollbox');
workspaceWrapper.id = workspace.uuid;
@@ -608,7 +566,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
toolbox.addEventListener(
'wheel',
- async (event) => {
+ (event) => {
if (this.privateWindowOrDisabled) return;
// Only process non-gesture scrolls
@@ -634,7 +592,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
const currentTime = Date.now();
- if (currentTime - this._lastScrollTime < scrollCooldown) return;
+ if (currentTime - this.#lastScrollTime < scrollCooldown) return;
//this decides which delta to use
const delta = isVerticalScroll ? event.deltaY : event.deltaX;
@@ -646,7 +604,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
let direction = this.naturalScroll ? -1 : 1;
this.changeWorkspaceShortcut(rawDirection * direction);
- this._lastScrollTime = currentTime;
+ this.#lastScrollTime = currentTime;
},
{ passive: true }
);
@@ -701,7 +659,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
_handleSwipeMayStart(event) {
- if (this.privateWindowOrDisabled || this._inChangingWorkspace) return;
+ if (this.privateWindowOrDisabled || this.#inChangingWorkspace) return;
if (
event.target.closest('#zen-sidebar-foot-buttons') ||
event.target.closest('#urlbar[zen-floating-urlbar="true"]')
@@ -786,38 +744,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
get activeWorkspace() {
- return this._activeWorkspace;
+ return this.#activeWorkspace;
}
set activeWorkspace(value) {
- this._activeWorkspace = value;
+ if (value === this.#activeWorkspace) {
+ return;
+ }
+ const spaces = this.getWorkspaces();
+ if (!spaces.some((ws) => ws.uuid === value)) {
+ value = spaces[0]?.uuid || '';
+ }
+ this.#activeWorkspace = value;
if (this.privateWindowOrDisabled) {
return;
}
Services.prefs.setStringPref('zen.workspaces.active', value);
}
- async observe(subject, topic, data) {
- if (topic === 'weave:engine:sync:finish' && data === 'workspaces') {
- try {
- const lastChangeTimestamp = await ZenWorkspacesStorage.getLastChangeTimestamp();
-
- if (
- !this._workspaceCache ||
- !this._workspaceCache.lastChangeTimestamp ||
- lastChangeTimestamp > this._workspaceCache.lastChangeTimestamp
- ) {
- await this._propagateWorkspaceData();
-
- const currentWorkspace = await this.getActiveWorkspace();
- await gZenThemePicker.onWorkspaceChange(currentWorkspace);
- }
- } catch (error) {
- console.error('Error updating workspaces after sync:', error);
- }
- }
- }
-
get shouldHaveWorkspaces() {
if (typeof this._shouldHaveWorkspaces === 'undefined') {
let chromeFlags = window.docShell.treeOwner
@@ -835,8 +779,16 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return PrivateBrowsingUtils.isWindowPrivate(window);
}
+ get currentWindowIsSyncing() {
+ return (
+ !document.documentElement.hasAttribute('zen-unsynced-window') &&
+ window._zenStartupSyncFlag !== 'unsynced' &&
+ !this.isPrivateWindow
+ );
+ }
+
get privateWindowOrDisabled() {
- return this.isPrivateWindow || !this.shouldHaveWorkspaces;
+ return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing;
}
get workspaceEnabled() {
@@ -854,50 +806,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
getWorkspaceFromId(id) {
try {
- return this._workspaceCache.workspaces.find((workspace) => workspace.uuid === id);
+ return this.getWorkspaces().find((workspace) => workspace.uuid === id);
} catch {
return null;
}
}
- async _workspaces() {
- if (this._workspaceCache) {
- return this._workspaceCache;
+ getWorkspaces(lieToMe = false) {
+ if (lieToMe) {
+ const { ZenSessionStore } = ChromeUtils.importESModule(
+ 'resource:///modules/zen/ZenSessionManager.sys.mjs'
+ );
+ return ZenSessionStore.getClonedSpaces();
}
-
- if (this.isPrivateWindow) {
- this._workspaceCache = {
- workspaces: this._privateWorkspace ? [this._privateWorkspace] : [],
- lastChangeTimestamp: 0,
- };
- this._activeWorkspace = this._privateWorkspace?.uuid;
+ if (!this.currentWindowIsSyncing) {
+ this._workspaceCache = this._tempWorkspace ? [this._tempWorkspace] : [];
+ this.#activeWorkspace = this._tempWorkspace?.uuid;
return this._workspaceCache;
}
-
- const [workspaces, lastChangeTimestamp] = await Promise.all([
- ZenWorkspacesStorage.getWorkspaces(),
- ZenWorkspacesStorage.getLastChangeTimestamp(),
- ]);
-
- this._workspaceCache = { workspaces, lastChangeTimestamp };
- // Get the active workspace ID from preferences
- const activeWorkspaceId = this.activeWorkspace;
-
- if (activeWorkspaceId) {
- const activeWorkspace = this.getWorkspaceFromId(activeWorkspaceId);
- // Set the active workspace ID to the first one if the one with selected id doesn't exist
- if (!activeWorkspace) {
- this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
- }
- } else {
- // Set the active workspace ID to the first one if active workspace doesn't exist
- this.activeWorkspace = this._workspaceCache.workspaces[0]?.uuid;
- }
- // sort by position
- this._workspaceCache.workspaces.sort(
- (a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)
- );
-
return this._workspaceCache;
}
@@ -924,8 +850,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return this._workspaceCache;
}
+ async restoreWorkspacesFromSessionStore(aWinData) {
+ this._workspaceCache = aWinData.spaces || [
+ await this.createAndSaveWorkspace('Space', undefined, true),
+ ];
+ this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid;
+ await this.initializeWorkspaces();
+ this.#hasInitialized = true;
+ }
+
async initializeWorkspaces() {
- let activeWorkspace = await this.getActiveWorkspace();
+ let activeWorkspace = this.getActiveWorkspace();
this.activeWorkspace = activeWorkspace?.uuid;
await gZenSessionStore.promiseInitialized;
try {
@@ -936,15 +871,15 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
} catch (e) {
console.error('gZenWorkspaces: Error initializing theme picker', e);
}
+ await this.#initializeTabsStripSections();
+ this.#initializeEmptyTab();
await this.workspaceBookmarks();
- await this.initializeTabsStripSections();
- this._initializeEmptyTab();
- await gZenPinnedTabManager.refreshPinnedTabs({ init: true });
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
this._resolveInitialized();
this.#clearAnyZombieTabs(); // Dont call with await
+ delete this._resolveInitialized;
const tabUpdateListener = this.updateTabsContainers.bind(this);
window.addEventListener('TabOpen', tabUpdateListener);
@@ -955,8 +890,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
window.addEventListener('TabUnpinned', tabUpdateListener);
window.addEventListener('aftercustomization', tabUpdateListener);
window.addEventListener('TabSelect', this.onLocationChange.bind(this));
-
window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this));
+
+ this.#updateWorkspacesChangeContextMenu();
}
async selectStartPage() {
@@ -1112,19 +1048,21 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
shouldCloseWindow() {
return (
- !window.toolbar.visible || Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab')
+ !window.toolbar.visible ||
+ Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab') ||
+ this.privateWindowOrDisabled
);
}
async #clearAnyZombieTabs() {
const tabs = this.allStoredTabs;
- const workspaces = await this._workspaces();
+ const workspaces = this.getWorkspaces();
for (let tab of tabs) {
const workspaceID = tab.getAttribute('zen-workspace-id');
if (
(workspaceID &&
!tab.hasAttribute('zen-essential') &&
- !workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)) ||
+ !workspaces.find((workspace) => workspace.uuid === workspaceID)) ||
// Also remove empty tabs that are supposed to be from parent folders but
// they dont exist anymore
(tab.pinned && tab.hasAttribute('zen-empty-tab') && !tab.group)
@@ -1206,6 +1144,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
);
}
+ generateMenuItemForWorkspace(workspace) {
+ const item = document.createXULElement('menuitem');
+ item.className = 'zen-workspace-context-menu-item';
+ item.setAttribute('zen-workspace-id', workspace.uuid);
+ item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
+ let name = workspace.name;
+ const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
+ if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
+ name = `${workspace.icon} ${name}`;
+ }
+ item.setAttribute('label', name);
+ if (iconIsSvg) {
+ item.setAttribute('image', workspace.icon);
+ item.classList.add('zen-workspace-context-icon');
+ }
+ return item;
+ }
+
#contextMenuData = null;
updateWorkspaceActionsMenu(event) {
if (event.target.id !== 'zenWorkspaceMoreActions') {
@@ -1248,21 +1204,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
if (!this.#contextMenuData.workspaceId) {
separator.hidden = false;
- for (const workspace of [...this._workspaceCache.workspaces].reverse()) {
- const item = document.createXULElement('menuitem');
- item.className = 'zen-workspace-context-menu-item';
- item.setAttribute('zen-workspace-id', workspace.uuid);
- item.setAttribute('disabled', workspace.uuid === this.activeWorkspace);
- let name = workspace.name;
- const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
- if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
- name = `${workspace.icon} ${name}`;
- }
- item.setAttribute('label', name);
- if (iconIsSvg) {
- item.setAttribute('image', workspace.icon);
- item.classList.add('zen-workspace-context-icon');
- }
+ for (const workspace of [...this._workspaceCache].reverse()) {
+ const item = this.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (e) => {
this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id'));
});
@@ -1295,48 +1238,35 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
});
}
- async saveWorkspace(workspaceData, preventPropagation = false) {
+ saveWorkspace(workspaceData) {
if (this.privateWindowOrDisabled) {
return;
}
- await ZenWorkspacesStorage.saveWorkspace(workspaceData);
- if (!preventPropagation) {
- await this._propagateWorkspaceData();
- await this._updateWorkspacesChangeContextMenu();
+ const workspacesData = this.getWorkspaces();
+ const index = workspacesData.findIndex((ws) => ws.uuid === workspaceData.uuid);
+ if (index !== -1) {
+ workspacesData[index] = workspaceData;
+ } else {
+ workspacesData.push(workspaceData);
}
+ this.#propagateWorkspaceData();
}
- async removeWorkspace(windowID) {
- let workspacesData = await this._workspaces();
- await this.changeWorkspace(
- workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID)
- );
- await this.#deleteAllTabsInWorkspace(windowID);
- delete this._lastSelectedWorkspaceTabs[windowID];
- await ZenWorkspacesStorage.removeWorkspace(windowID);
+ removeWorkspace(windowID) {
+ let workspacesData = this.getWorkspaces();
// Remove the workspace from the cache
- this._workspaceCache.workspaces = this._workspaceCache.workspaces.filter(
- (workspace) => workspace.uuid !== windowID
- );
- await this._propagateWorkspaceData();
- await this._updateWorkspacesChangeContextMenu();
- this.workspaceElement(windowID)?.remove();
- this.onWindowResize();
- this.registerPinnedResizeObserver();
+ workspacesData = workspacesData.filter((workspace) => workspace.uuid !== windowID);
+ this.#propagateWorkspaceData(workspacesData);
}
isWorkspaceActive(workspace) {
return workspace.uuid === this.activeWorkspace;
}
- async getActiveWorkspace() {
- const workspaces = await this._workspaces();
- return (
- workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ??
- workspaces.workspaces[0]
- );
+ getActiveWorkspace() {
+ return this.getActiveWorkspaceFromCache();
}
- // Workspaces dialog UI management
+
workspaceHasIcon(workspace) {
return workspace.icon && workspace.icon !== '';
}
@@ -1359,65 +1289,75 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
);
}
- async _propagateWorkspaceDataForWindow(browser, { ignoreStrip = false, clearCache = true } = {}) {
- if (clearCache) {
- browser.gZenWorkspaces._workspaceCache = null;
- browser.gZenWorkspaces._workspaceBookmarksCache = null;
- }
- let workspaces = await browser.gZenWorkspaces._workspaces();
- browser.document
- .getElementById('cmd_zenCtxDeleteWorkspace')
- .setAttribute('disabled', workspaces.workspaces.length <= 1);
- if (clearCache) {
- browser.dispatchEvent(
- new CustomEvent('ZenWorkspacesUIUpdate', {
- bubbles: true,
- detail: { activeIndex: browser.gZenWorkspaces.activeWorkspace },
- })
- );
- for (const workspace of workspaces.workspaces) {
- // Add workspace elements if they dont exist on other windows
- if (!browser.gZenWorkspaces.workspaceElement(workspace.uuid)) {
- await browser.gZenWorkspaces._createWorkspaceTabsSection(workspace);
- }
- }
- }
- await browser.gZenWorkspaces.workspaceBookmarks();
- if (!ignoreStrip) {
- browser.gZenWorkspaces._fixIndicatorsNames(workspaces);
+ #propagateWorkspaceData(aSpaceData = null) {
+ if (!this.#hasInitialized || this.privateWindowOrDisabled) {
+ return;
}
+ window.gZenWindowSync.propagateWorkspacesToAllWindows(aSpaceData ?? this._workspaceCache);
}
- async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) {
- const currentWindowIsPrivate = this.isPrivateWindow;
- if (onInit) {
- if (currentWindowIsPrivate) return;
- return await this._propagateWorkspaceDataForWindow(this.ownerWindow, {
- ignoreStrip,
- clearCache,
- });
- }
- await this.foreachWindowAsActive(async (browser) => {
- // Do not update the window if workspaces are not enabled in it.
- // For example, when the window is in private browsing mode.
+ propagateWorkspaces(aWorkspaces) {
+ const previousWorkspaces = this._workspaceCache || [];
+ this._workspaceCache = aWorkspaces;
+ let hasChanged = false;
+ // Remove any workspace elements here that no longer exist
+ for (const previousWorkspace of previousWorkspaces) {
if (
- !browser.gZenWorkspaces.workspaceEnabled ||
- browser.gZenWorkspaces.isPrivateWindow !== currentWindowIsPrivate
+ this.workspaceElement(previousWorkspace.uuid) &&
+ !aWorkspaces.find((w) => w.uuid === previousWorkspace.uuid)
) {
- return;
+ if (this.isWorkspaceActive(previousWorkspace)) {
+ // If the removed workspace was active, switch to another one
+ const newActiveWorkspace =
+ aWorkspaces.find((w) => w.uuid !== previousWorkspace.uuid) || null;
+ this.changeWorkspace(newActiveWorkspace);
+ }
+ this.workspaceElement(previousWorkspace.uuid)?.remove();
+ delete this.lastSelectedWorkspaceTabs[previousWorkspace.uuid];
+ hasChanged = true;
+ }
+ }
+ // Add any new workspace elements here
+ for (const workspace of aWorkspaces) {
+ if (!this.workspaceElement(workspace.uuid)) {
+ this.#createWorkspaceTabsSection(workspace).catch((e) => {
+ console.error('Error creating workspace tabs section:', e);
+ });
+ hasChanged = true;
+ }
+ }
+ // Order the workspace elements correctly
+ let previousElement = null;
+ for (const workspace of aWorkspaces) {
+ const workspaceElement = this.workspaceElement(workspace.uuid);
+ if (workspaceElement) {
+ if (previousElement === null) {
+ gZenUIManager.tabsWrapper.insertBefore(
+ workspaceElement,
+ gZenUIManager.tabsWrapper.firstChild
+ );
+ hasChanged = true;
+ } else if (previousElement.nextSibling !== workspaceElement) {
+ gZenUIManager.tabsWrapper.insertBefore(workspaceElement, previousElement.nextSibling);
+ hasChanged = true;
+ }
+ previousElement = workspaceElement;
}
- this._propagateWorkspaceDataForWindow(browser, {
- ignoreStrip,
- clearCache,
- }).catch(console.error);
+ }
+ if (hasChanged) {
+ this.#fireSpaceUIUpdate();
+ }
+ this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache()).finally(() => {
+ this.updateTabsContainers();
});
+ this.#updateWorkspacesChangeContextMenu();
}
async reorderWorkspace(id, newPosition) {
if (this.privateWindowOrDisabled) {
return;
}
- const workspaces = (await this._workspaces()).workspaces;
+ const workspaces = this.getWorkspaces();
const workspace = workspaces.find((w) => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
@@ -1436,26 +1376,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
workspaces.splice(newPosition, 0, workspace);
- // Update the positions in the storage
- await ZenWorkspacesStorage.updateWorkspacePositions(workspaces);
// Propagate the changes
- await this._propagateWorkspaceData();
- }
-
- async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) {
- const workspaces = (await this._workspaces()).workspaces;
- const draggedIndex = workspaces.findIndex((w) => w.uuid === draggedWorkspaceId);
- const draggedWorkspace = workspaces.splice(draggedIndex, 1)[0];
- const targetIndex = workspaces.findIndex((w) => w.uuid === targetWorkspaceId);
- workspaces.splice(targetIndex, 0, draggedWorkspace);
-
- await ZenWorkspacesStorage.updateWorkspacePositions(workspaces);
- await this._propagateWorkspaceData();
+ this.#propagateWorkspaceData();
}
async openWorkspaceCreation() {
let createForm;
- const previousWorkspace = await this.getActiveWorkspace();
+ const previousWorkspace = this.getActiveWorkspace();
document.documentElement.setAttribute('zen-creating-workspace', 'true');
await this.createAndSaveWorkspace('Space', undefined, false, 0, {
beforeChangeCallback: async (workspace) => {
@@ -1469,27 +1396,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
createForm.finishSetup();
}
- // Workspaces management
-
- async #deleteAllTabsInWorkspace(workspaceID) {
- const tabs = Array.from(this.allStoredTabs).filter(
- (tab) =>
- tab.getAttribute('zen-workspace-id') === workspaceID &&
- !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,
- closeWindowWithLastTab: false,
- });
- }
-
#unpinnedTabsInWorkspace(workspaceID) {
return Array.from(this.allStoredTabs).filter(
(tab) => tab.getAttribute('zen-workspace-id') === workspaceID && tab.visible && !tab.pinned
@@ -1630,19 +1536,19 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async changeWorkspace(workspace, ...args) {
if (
!this.workspaceEnabled ||
- this._inChangingWorkspace ||
+ this.#inChangingWorkspace ||
gNavToolbox.hasAttribute('movingtab')
) {
return;
}
- this._inChangingWorkspace = true;
+ this.#inChangingWorkspace = true;
try {
this.log('Changing workspace to', workspace?.uuid);
await this._performWorkspaceChange(workspace, ...args);
} catch (e) {
console.error('gZenWorkspaces: Error changing workspace', e);
}
- this._inChangingWorkspace = false;
+ this.#inChangingWorkspace = false;
}
_cancelSwipeAnimation() {
@@ -1653,7 +1559,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
workspace,
{ onInit = false, alwaysChange = false, whileScrolling = false } = {}
) {
- const previousWorkspace = await this.getActiveWorkspace();
+ const previousWorkspace = this.getActiveWorkspace();
alwaysChange = alwaysChange || onInit;
this.activeWorkspace = workspace.uuid;
if (previousWorkspace && previousWorkspace.uuid === workspace.uuid && !alwaysChange) {
@@ -1661,11 +1567,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
- const workspaces = await this._workspaces();
+ const workspaces = this.getWorkspaces();
gZenFolders.cancelPopupTimer();
// Refresh tab cache
- for (const otherWorkspace of workspaces.workspaces) {
+ for (const otherWorkspace of workspaces) {
const container = this.workspaceElement(otherWorkspace.uuid);
container.active = otherWorkspace.uuid === workspace.uuid;
}
@@ -1687,9 +1593,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
gBrowser.warmupTab(tabToSelect);
// Update UI and state
- const previousWorkspaceIndex = workspaces.workspaces.findIndex(
- (w) => w.uuid === previousWorkspace.uuid
- );
+ const previousWorkspaceIndex = workspaces.findIndex((w) => w.uuid === previousWorkspace.uuid);
await this._updateWorkspaceState(workspace, onInit, tabToSelect, {
previousWorkspaceIndex,
previousWorkspace,
@@ -1747,7 +1651,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
) {
if (
workspaceElement &&
- !(this._inChangingWorkspace && !forAnimation && !this._alwaysAnimatePaddingTop)
+ !(this.#inChangingWorkspace && !forAnimation && !this._alwaysAnimatePaddingTop)
) {
delete this._alwaysAnimatePaddingTop;
const essentialsHeight = essentialContainer.getBoundingClientRect().height;
@@ -1777,8 +1681,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
this._organizingWorkspaceStrip = true;
- const workspaces = await this._workspaces();
- let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid);
+ const workspaces = this.getWorkspaces();
+ let workspaceIndex = workspaces.findIndex((w) => w.uuid === workspace.uuid);
if (!justMove) {
this._fixIndicatorsNames(workspaces);
}
@@ -1787,10 +1691,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
);
const workspaceContextId = workspace.containerTabId;
const nextWorkspaceContextId =
- workspaces.workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId;
- for (const otherWorkspace of workspaces.workspaces) {
+ workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId;
+ for (const otherWorkspace of workspaces) {
const element = this.workspaceElement(otherWorkspace.uuid);
- const newTransform = -(workspaceIndex - workspaces.workspaces.indexOf(otherWorkspace)) * 100;
+ const newTransform = -(workspaceIndex - workspaces.indexOf(otherWorkspace)) * 100;
element.style.transform = `translateX(${newTransform + offsetPixels / 2}%)`;
}
// Hide other essentials with different containerTabId
@@ -1824,7 +1728,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
if (offsetPixels) {
// Find the next workspace we are scrolling to
- const nextWorkspace = workspaces.workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)];
+ const nextWorkspace = workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)];
if (nextWorkspace) {
const {
gradient: nextGradient,
@@ -1860,7 +1764,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const grainValue =
minGrain +
(maxGrain - minGrain) * (existingGrain > nextGrain ? 1 - percentage : percentage);
- if (!this._inChangingWorkspace) {
+ if (!this.#inChangingWorkspace) {
gZenThemePicker.updateNoise(grainValue);
}
}
@@ -1895,7 +1799,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
_fixIndicatorsNames(workspaces) {
- for (const workspace of workspaces.workspaces) {
+ for (const workspace of workspaces) {
const workspaceIndicator = this.workspaceElement(workspace.uuid)?.indicator;
this.updateWorkspaceIndicator(workspace, workspaceIndicator);
}
@@ -1911,12 +1815,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const kGlobalAnimationDuration = 0.2;
this._animatingChange = true;
const animations = [];
- const workspaces = await this._workspaces();
- const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
+ const workspaces = this.getWorkspaces();
+ const newWorkspaceIndex = workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex;
const clonedEssentials = [];
if (shouldAnimate && this.shouldAnimateEssentials && previousWorkspace) {
- for (const workspace of workspaces.workspaces) {
+ for (const workspace of workspaces) {
const essentialsContainer = this.getEssentialsSection(workspace.containerTabId);
if (clonedEssentials[clonedEssentials.length - 1]?.contextId == workspace.containerTabId) {
clonedEssentials[clonedEssentials.length - 1].repeat++;
@@ -1984,9 +1888,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
const existingTransform = element.style.transform;
const elementWorkspaceId = element.id;
- const elementWorkspaceIndex = workspaces.workspaces.findIndex(
- (w) => w.uuid === elementWorkspaceId
- );
+ const elementWorkspaceIndex = workspaces.findIndex((w) => w.uuid === elementWorkspaceId);
const offset = -(newWorkspaceIndex - elementWorkspaceIndex) * 100;
const newTransform = `translateX(${offset}%)`;
if (shouldAnimate) {
@@ -2023,10 +1925,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Get a list of essentials containers that are in between the first and last workspace
const essentialsContainersInBetween = clonedEssentials.filter((cloned) => {
const essentialsWorkspaces = cloned.workspaces;
- const firstIndex = workspaces.workspaces.findIndex(
- (w) => w.uuid === essentialsWorkspaces[0].uuid
- );
- const lastIndex = workspaces.workspaces.findIndex(
+ const firstIndex = workspaces.findIndex((w) => w.uuid === essentialsWorkspaces[0].uuid);
+ const lastIndex = workspaces.findIndex(
(w) => w.uuid === essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
);
@@ -2055,10 +1955,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// will slide in from the right
// Get the index from first and last workspace
- const firstWorkspaceIndex = workspaces.workspaces.findIndex(
+ const firstWorkspaceIndex = workspaces.findIndex(
(w) => w.uuid === essentialsWorkspaces[0].uuid
);
- const lastWorkspaceIndex = workspaces.workspaces.findIndex(
+ const lastWorkspaceIndex = workspaces.findIndex(
(w) => w.uuid === essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
);
cloned.originalContainer.style.removeProperty('transform');
@@ -2214,7 +2114,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
tab,
currentWorkspace.uuid,
currentWorkspace.containerTabId,
- await this._workspaces()
+ this.getWorkspaces()
);
}
@@ -2249,9 +2149,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return (
!tabContextId ||
tabContextId === '0' ||
- !workspaces.workspaces.some(
- (workspace) => workspace.containerTabId === parseInt(tabContextId, 10)
- )
+ !workspaces.some((workspace) => workspace.containerTabId === parseInt(tabContextId, 10))
);
}
}
@@ -2270,14 +2168,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async _handleTabSelection(workspace, onInit, previousWorkspaceId) {
const currentSelectedTab = gBrowser.selectedTab;
const oldWorkspaceId = previousWorkspaceId;
- const lastSelectedTab = this._lastSelectedWorkspaceTabs[workspace.uuid];
+ const lastSelectedTab = this.lastSelectedWorkspaceTabs[workspace.uuid];
const containerId = workspace.containerTabId?.toString();
- const workspaces = await this._workspaces();
+ const workspaces = this.getWorkspaces();
// Save current tab as last selected for old workspace if it shouldn't be visible in new workspace
if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) {
- this._lastSelectedWorkspaceTabs[oldWorkspaceId] =
+ this.lastSelectedWorkspaceTabs[oldWorkspaceId] =
gZenGlanceManager.getTabOrGlanceParent(currentSelectedTab);
}
@@ -2333,9 +2231,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
gBrowser.tabContainer.arrowScrollbox = this.activeScrollbox;
// Update workspace UI
- await this._updateWorkspacesChangeContextMenu();
- await this._propagateWorkspaceData({ clearCache: false, onInit });
-
gZenThemePicker.onWorkspaceChange(workspace);
gZenUIManager.tabsWrapper.scrollbarWidth = 'none';
@@ -2380,15 +2275,19 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
tab.setAttribute('zen-workspace-id', workspace.uuid);
}
}
- window.dispatchEvent(
- new CustomEvent('ZenWorkspacesUIUpdate', {
- bubbles: true,
- detail: { activeIndex: workspace.uuid },
- })
- );
+ this.#fireSpaceUIUpdate();
}
}
+ #fireSpaceUIUpdate() {
+ window.dispatchEvent(
+ new CustomEvent('ZenWorkspacesUIUpdate', {
+ bubbles: true,
+ detail: { activeIndex: this.activeWorkspace },
+ })
+ );
+ }
+
async _fixCtrlTabBehavior() {
ctrlTab.uninit();
ctrlTab.readPref();
@@ -2404,9 +2303,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
}
- async _updateWorkspacesChangeContextMenu() {
+ #updateWorkspacesChangeContextMenu() {
if (gZenWorkspaces.privateWindowOrDisabled) return;
- const workspaces = await this._workspaces();
+ const workspaces = this.getWorkspaces();
const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup');
if (!menuPopup) {
@@ -2414,9 +2313,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
menuPopup.innerHTML = '';
- const activeWorkspace = await this.getActiveWorkspace();
+ const activeWorkspace = this.getActiveWorkspace();
- for (let workspace of workspaces.workspaces) {
+ for (let workspace of workspaces) {
const menuItem = document.createXULElement('menuitem');
menuItem.setAttribute('label', workspace.name);
menuItem.setAttribute('zen-workspace-id', workspace.uuid);
@@ -2440,7 +2339,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
};
if (moveTabs) {
this.#prepareNewWorkspace(workspace);
- await this._createWorkspaceTabsSection(workspace, tabs);
+ await this.#createWorkspaceTabsSection(workspace, tabs);
await this._organizeWorkspaceStripLocations(workspace);
}
return workspace;
@@ -2456,8 +2355,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (!this.workspaceEnabled) {
return;
}
- if (this.isPrivateWindow) {
- name = 'Private ' + name;
+ if (!this.currentWindowIsSyncing) {
+ name = this.isPrivateWindow ? 'Private ' + name : 'Temporary';
}
// get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace
const extraTabs = Array.from(gBrowser.tabContainer.arrowScrollbox.children).filter(
@@ -2474,10 +2373,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!dontChange,
containerTabId
);
- if (this.isPrivateWindow) {
- this._privateWorkspace = workspaceData;
+ if (!this.currentWindowIsSyncing) {
+ this._tempWorkspace = workspaceData;
} else {
- await this.saveWorkspace(workspaceData, dontChange);
+ this.saveWorkspace(workspaceData);
}
if (!dontChange) {
if (beforeChangeCallback) {
@@ -2582,9 +2481,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const workspacesIds = [];
if (entry.target.closest('#zen-essentials')) {
// Get all workspaces that have the same userContextId
- const activeWorkspace = await this.getActiveWorkspace();
+ const activeWorkspace = this.getActiveWorkspace();
const userContextId = activeWorkspace.containerTabId;
- const workspaces = this._workspaceCache.workspaces.filter(
+ const workspaces = this._workspaceCache.filter(
(w) => w.containerTabId === userContextId && w.uuid !== originalWorkspaceId
);
workspacesIds.push(...workspaces.map((w) => w.uuid));
@@ -2637,7 +2536,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (workspaceID) {
if (tab.hasAttribute('change-workspace') && this.moveTabToWorkspace(tab, workspaceID)) {
- this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
+ this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
tab.removeAttribute('change-workspace');
const workspace = this.getWorkspaceFromId(workspaceID);
setTimeout(() => {
@@ -2647,7 +2546,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
- let activeWorkspace = await this.getActiveWorkspace();
+ let activeWorkspace = this.getActiveWorkspace();
if (!activeWorkspace) {
return;
}
@@ -2669,7 +2568,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async onLocationChange(event) {
let tab = event.target;
this.#changeToEmptyTab();
- if (!this.workspaceEnabled || this._inChangingWorkspace || this._isClosingWindow) {
+ if (!this.workspaceEnabled || this.#inChangingWorkspace || this._isClosingWindow) {
return;
}
@@ -2686,14 +2585,14 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
if (!isEssential) {
- const activeWorkspace = await this.getActiveWorkspace();
+ const activeWorkspace = this.getActiveWorkspace();
if (!activeWorkspace) {
return;
}
// Only update last selected tab for non-essential tabs in their workspace
if (workspaceID === activeWorkspace.uuid) {
- this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
+ this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(tab);
}
// Switch workspace if needed
@@ -2710,16 +2609,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Context menu management
async contextChangeContainerTab(event) {
this._organizingWorkspaceStrip = true;
- let workspaces = await this._workspaces();
- let workspace = workspaces.workspaces.find(
+ let workspaces = this.getWorkspaces();
+ let workspace = workspaces.find(
(workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace)
);
let userContextId = parseInt(event.target.getAttribute('data-usercontextid'));
workspace.containerTabId = userContextId + 0; // +0 to convert to number
- await this.saveWorkspace(workspace);
- await this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache(), true);
- await this.updateTabsContainers();
- this.tabContainer._invalidateCachedTabs();
+ this.saveWorkspace(workspace);
}
async closeAllUnpinnedTabs() {
@@ -2751,7 +2647,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
},
]);
if (Services.prompt.confirm(null, title, body)) {
- await this.removeWorkspace(workspaceId);
+ this.removeWorkspace(workspaceId);
}
}
@@ -2764,21 +2660,21 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async changeWorkspaceShortcut(offset = 1, whileScrolling = false) {
// Cycle through workspaces
- let workspaces = await this._workspaces();
- let activeWorkspace = await this.getActiveWorkspace();
- let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace);
+ let workspaces = this.getWorkspaces();
+ let activeWorkspace = this.getActiveWorkspace();
+ let workspaceIndex = workspaces.indexOf(activeWorkspace);
// note: offset can be negative
let targetIndex = workspaceIndex + offset;
if (this.shouldWrapAroundNavigation) {
// Add length to handle negative indices and loop
- targetIndex = (targetIndex + workspaces.workspaces.length) % workspaces.workspaces.length;
+ targetIndex = (targetIndex + workspaces.length) % workspaces.length;
} else {
// Clamp within bounds to disable looping
- targetIndex = Math.max(0, Math.min(workspaces.workspaces.length - 1, targetIndex));
+ targetIndex = Math.max(0, Math.min(workspaces.length - 1, targetIndex));
}
- let nextWorkspace = workspaces.workspaces[targetIndex];
+ let nextWorkspace = workspaces[targetIndex];
await this.changeWorkspace(nextWorkspace, { whileScrolling });
}
@@ -2818,20 +2714,18 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const previousWorkspaceID = document.documentElement.getAttribute('zen-workspace-id');
for (let tab of tabs) {
this.moveTabToWorkspace(tab, workspaceID);
- if (this._lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) {
+ if (this.lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) {
// This tab is no longer the last selected tab in the previous workspace because it's being moved to
// the current workspace
- delete this._lastSelectedWorkspaceTabs[previousWorkspaceID];
+ delete this.lastSelectedWorkspaceTabs[previousWorkspaceID];
}
}
// Make sure we select the last tab in the new workspace
- this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(
+ this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(
tabs[tabs.length - 1]
);
- const workspaces = await this._workspaces();
- await this.changeWorkspace(
- workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)
- );
+ const workspaces = this.getWorkspaces();
+ await this.changeWorkspace(workspaces.find((workspace) => workspace.uuid === workspaceID));
}
// Tab browser utilities
@@ -2844,11 +2738,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (
this.shouldForceContainerTabsToWorkspace &&
typeof userContextId !== 'undefined' &&
- this._workspaceCache?.workspaces &&
+ this._workspaceCache &&
!fromExternal
) {
// Find all workspaces that match the given userContextId
- const matchingWorkspaces = this._workspaceCache.workspaces.filter(
+ const matchingWorkspaces = this._workspaceCache.filter(
(workspace) => workspace.containerTabId === userContextId
);
@@ -2887,12 +2781,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async shortcutSwitchTo(index) {
- const workspaces = await this._workspaces();
+ const workspaces = this.getWorkspaces();
// The index may be out of bounds, if it doesnt exist, don't do anything
- if (index >= workspaces.workspaces.length || index < 0) {
+ if (index >= workspaces.length || index < 0) {
return;
}
- const workspaceToSwitch = workspaces.workspaces[index];
+ const workspaceToSwitch = workspaces[index];
await this.changeWorkspace(workspaceToSwitch);
}
@@ -2936,7 +2830,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
pinnedContainers = [document.getElementById('pinned-tabs-container')];
normalContainers = [this.activeWorkspaceStrip];
} else {
- let workspaces = Array.from(this._workspaceCache?.workspaces || []);
+ let workspaces = Array.from(this._workspaceCache || []);
// Make the active workspace first
workspaces = workspaces.sort((a, b) =>
a.uuid === this.activeWorkspace ? -1 : b.uuid === this.activeWorkspace ? 1 : 0
@@ -2982,7 +2876,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
const pinnedContainers = [];
const normalContainers = [];
- for (const workspace of this._workspaceCache.workspaces) {
+ for (const workspace of this._workspaceCache) {
const container = this.workspaceElement(workspace.uuid);
if (container) {
pinnedContainers.push(container.pinnedTabsContainer);
@@ -3080,7 +2974,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Find first workspace with the same container
const containerTabId = parseInt(tab.parentNode.getAttribute('container'));
// +0 to convert to number
- workspaceToSwitch = this._workspaceCache.workspaces.find(
+ workspaceToSwitch = this._workspaceCache.find(
(workspace) => workspace.containerTabId + 0 === containerTabId
);
} else {
@@ -3094,7 +2988,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this._workspaceChangeInProgress = true;
try {
- this._lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] =
+ this.lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] =
gZenGlanceManager.getTabOrGlanceParent(tab);
await this.changeWorkspace(workspaceToSwitch);
} finally {
@@ -3118,7 +3012,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return 0;
}
const activeWorkspace = this.activeWorkspace;
- const workspace = workspaces.workspaces.find((workspace) => workspace.uuid === activeWorkspace);
+ const workspace = workspaces.find((workspace) => workspace.uuid === activeWorkspace);
return workspace.containerTabId;
}
diff --git a/src/zen/workspaces/ZenWorkspacesStorage.mjs b/src/zen/workspaces/ZenWorkspacesStorage.mjs
index 43d183d39f..63097e2405 100644
--- a/src/zen/workspaces/ZenWorkspacesStorage.mjs
+++ b/src/zen/workspaces/ZenWorkspacesStorage.mjs
@@ -2,437 +2,20 @@
// 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/.
-window.ZenWorkspacesStorage = {
+// Integration of workspace-specific bookmarks into Places
+window.ZenWorkspaceBookmarksStorage = {
lazy: {},
async init() {
ChromeUtils.defineESModuleGetters(this.lazy, {
PlacesUtils: 'resource://gre/modules/PlacesUtils.sys.mjs',
- Weave: 'resource://services-sync/main.sys.mjs',
});
-
if (!window.gZenWorkspaces) return;
await this._ensureTable();
- await ZenWorkspaceBookmarksStorage.init();
},
async _ensureTable() {
await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage._ensureTable',
- async (db) => {
- // Create the main workspaces table if it doesn't exist
- await db.execute(`
- CREATE TABLE IF NOT EXISTS zen_workspaces (
- id INTEGER PRIMARY KEY,
- uuid TEXT UNIQUE NOT NULL,
- name TEXT NOT NULL,
- icon TEXT,
- container_id INTEGER,
- position INTEGER NOT NULL DEFAULT 0,
- created_at INTEGER NOT NULL,
- updated_at INTEGER NOT NULL
- )
- `);
-
- // Add new columns if they don't exist
- // SQLite doesn't have a direct "ADD COLUMN IF NOT EXISTS" syntax,
- // so we need to check if the columns exist first
- const columns = await db.execute(`PRAGMA table_info(zen_workspaces)`);
- 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_workspaces ADD COLUMN ${columnName} ${definition}`);
- }
- };
-
- // Add each new column if it doesn't exist
- await addColumnIfNotExists('theme_type', 'TEXT');
- await addColumnIfNotExists('theme_colors', 'TEXT');
- await addColumnIfNotExists('theme_opacity', 'REAL');
- await addColumnIfNotExists('theme_rotation', 'INTEGER');
- await addColumnIfNotExists('theme_texture', 'REAL');
-
- // Create an index on the uuid column
- await db.execute(`
- CREATE INDEX IF NOT EXISTS idx_zen_workspaces_uuid ON zen_workspaces(uuid)
- `);
-
- // Create the changes tracking table if it doesn't exist
- await db.execute(`
- CREATE TABLE IF NOT EXISTS zen_workspaces_changes (
- uuid TEXT PRIMARY KEY,
- timestamp INTEGER NOT NULL
- )
- `);
-
- // Create an index on the uuid column for changes tracking table
- await db.execute(`
- CREATE INDEX IF NOT EXISTS idx_zen_workspaces_changes_uuid ON zen_workspaces_changes(uuid)
- `);
-
- if (!this.lazy.Weave.Service.engineManager.get('workspaces')) {
- this.lazy.Weave.Service.engineManager.register(ZenWorkspacesEngine);
- await ZenWorkspacesStorage.migrateWorkspacesFromJSON();
- }
-
- gZenWorkspaces._resolveDBInitialized();
- }
- );
- },
-
- async migrateWorkspacesFromJSON() {
- const oldWorkspacesPath = PathUtils.join(
- PathUtils.profileDir,
- 'zen-workspaces',
- 'Workspaces.json'
- );
- if (await IOUtils.exists(oldWorkspacesPath)) {
- console.info('ZenWorkspacesStorage: Migrating workspaces from JSON...');
- const oldWorkspaces = await IOUtils.readJSON(oldWorkspacesPath);
- if (oldWorkspaces.workspaces) {
- for (const workspace of oldWorkspaces.workspaces) {
- await this.saveWorkspace(workspace);
- }
- }
- await IOUtils.remove(oldWorkspacesPath);
- }
- },
-
- /**
- * Private helper method to notify observers with a list of changed UUIDs.
- * @param {string} event - The observer event name.
- * @param {Array} uuids - Array of changed workspace UUIDs.
- */
- _notifyWorkspacesChanged(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 saveWorkspace(workspace, notifyObservers = true) {
- const changedUUIDs = new Set();
-
- await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage.saveWorkspace',
- async (db) => {
- await db.executeTransaction(async () => {
- const now = Date.now();
-
- let newPosition;
- if ('position' in workspace && Number.isFinite(workspace.position)) {
- newPosition = workspace.position;
- } else {
- // Get the maximum position
- const maxPositionResult = await db.execute(
- `SELECT MAX("position") as max_position FROM zen_workspaces`
- );
- const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
- newPosition = maxPosition + 1000; // Add a large increment to avoid frequent reordering
- }
-
- // Insert or replace the workspace
- await db.executeCached(
- `
- INSERT OR REPLACE INTO zen_workspaces (
- uuid, name, icon, container_id, created_at, updated_at, "position",
- theme_type, theme_colors, theme_opacity, theme_rotation, theme_texture
- ) VALUES (
- :uuid, :name, :icon, :container_id,
- COALESCE((SELECT created_at FROM zen_workspaces WHERE uuid = :uuid), :now),
- :now,
- :position,
- :theme_type, :theme_colors, :theme_opacity, :theme_rotation, :theme_texture
- )
- `,
- {
- uuid: workspace.uuid,
- name: workspace.name,
- icon: workspace.icon || null,
- container_id: workspace.containerTabId || null,
- now,
- position: newPosition,
- theme_type: workspace.theme?.type || null,
- theme_colors: workspace.theme ? JSON.stringify(workspace.theme.gradientColors) : null,
- theme_opacity: workspace.theme?.opacity || null,
- theme_rotation: workspace.theme?.rotation || null,
- theme_texture: workspace.theme?.texture || null,
- }
- );
-
- // Record the change
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: workspace.uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
-
- changedUUIDs.add(workspace.uuid);
-
- await this.updateLastChangeTimestamp(db);
- });
- }
- );
-
- if (notifyObservers) {
- this._notifyWorkspacesChanged('zen-workspace-updated', Array.from(changedUUIDs));
- }
- },
-
- async getWorkspaces() {
- const db = await this.lazy.PlacesUtils.promiseDBConnection();
- const rows = await db.executeCached(`
- SELECT * FROM zen_workspaces ORDER BY created_at ASC
- `);
- return rows.map((row) => ({
- uuid: row.getResultByName('uuid'),
- name: row.getResultByName('name'),
- icon: row.getResultByName('icon'),
- containerTabId: row.getResultByName('container_id') ?? 0,
- position: row.getResultByName('position'),
- theme: row.getResultByName('theme_type')
- ? {
- type: row.getResultByName('theme_type'),
- gradientColors: JSON.parse(row.getResultByName('theme_colors')),
- opacity: row.getResultByName('theme_opacity'),
- rotation: row.getResultByName('theme_rotation'),
- texture: row.getResultByName('theme_texture'),
- }
- : null,
- }));
- },
-
- async removeWorkspace(uuid, notifyObservers = true) {
- const changedUUIDs = [uuid];
-
- await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage.removeWorkspace',
- async (db) => {
- await db.execute(
- `
- DELETE FROM zen_workspaces WHERE uuid = :uuid
- `,
- { uuid }
- );
-
- // Record the removal as a change
- const now = Date.now();
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
-
- await this.updateLastChangeTimestamp(db);
- }
- );
-
- if (notifyObservers) {
- this._notifyWorkspacesChanged('zen-workspace-removed', changedUUIDs);
- }
- },
-
- async wipeAllWorkspaces() {
- await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage.wipeAllWorkspaces',
- async (db) => {
- await db.execute(`DELETE FROM zen_workspaces`);
- await db.execute(`DELETE FROM zen_workspaces_changes`);
- await this.updateLastChangeTimestamp(db);
- }
- );
- },
-
- async markChanged(uuid) {
- await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage.markChanged',
- async (db) => {
- const now = Date.now();
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
- }
- );
- },
-
- async saveWorkspaceTheme(uuid, theme, notifyObservers = true) {
- const changedUUIDs = [uuid];
- await this.lazy.PlacesUtils.withConnectionWrapper('saveWorkspaceTheme', async (db) => {
- await db.execute(
- `
- UPDATE zen_workspaces
- SET
- theme_type = :type,
- theme_colors = :colors,
- theme_opacity = :opacity,
- theme_rotation = :rotation,
- theme_texture = :texture,
- updated_at = :now
- WHERE uuid = :uuid
- `,
- {
- type: theme.type,
- colors: JSON.stringify(theme.gradientColors),
- opacity: theme.opacity,
- rotation: theme.rotation,
- texture: theme.texture,
- now: Date.now(),
- uuid,
- }
- );
-
- await this.markChanged(uuid);
- await this.updateLastChangeTimestamp(db);
- });
-
- if (notifyObservers) {
- this._notifyWorkspacesChanged('zen-workspace-updated', changedUUIDs);
- }
- },
-
- async getChangedIDs() {
- const db = await this.lazy.PlacesUtils.promiseDBConnection();
- const rows = await db.execute(`
- SELECT uuid, timestamp FROM zen_workspaces_changes
- `);
- const changes = {};
- for (const row of rows) {
- changes[row.getResultByName('uuid')] = row.getResultByName('timestamp');
- }
- return changes;
- },
-
- async clearChangedIDs() {
- await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage.clearChangedIDs',
- async (db) => {
- await db.execute(`DELETE FROM zen_workspaces_changes`);
- }
- );
- },
-
- shouldReorderWorkspaces(before, current, after) {
- const minGap = 1; // Minimum allowed gap between positions
- return (
- (before !== null && current - before < minGap) || (after !== null && after - current < minGap)
- );
- },
-
- async reorderAllWorkspaces(db, changedUUIDs) {
- const workspaces = await db.execute(`
- SELECT uuid
- FROM zen_workspaces
- ORDER BY "position" ASC
- `);
-
- for (let i = 0; i < workspaces.length; i++) {
- const newPosition = (i + 1) * 1000; // Use large increments
- await db.execute(
- `
- UPDATE zen_workspaces
- SET "position" = :newPosition
- WHERE uuid = :uuid
- `,
- { newPosition, uuid: workspaces[i].getResultByName('uuid') }
- );
- changedUUIDs.add(workspaces[i].getResultByName('uuid'));
- }
- },
-
- async updateLastChangeTimestamp(db) {
- const now = Date.now();
- await db.execute(
- `
- INSERT OR REPLACE INTO moz_meta (key, value)
- VALUES ('zen_workspaces_last_change', :now)
- `,
- { now }
- );
- },
-
- async getLastChangeTimestamp() {
- const db = await this.lazy.PlacesUtils.promiseDBConnection();
- const result = await db.executeCached(`
- SELECT value FROM moz_meta WHERE key = 'zen_workspaces_last_change'
- `);
- return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
- },
-
- async updateWorkspacePositions(workspaces) {
- const changedUUIDs = new Set();
-
- await this.lazy.PlacesUtils.withConnectionWrapper(
- 'ZenWorkspacesStorage.updateWorkspacePositions',
- async (db) => {
- await db.executeTransaction(async () => {
- const now = Date.now();
-
- for (let i = 0; i < workspaces.length; i++) {
- const workspace = workspaces[i];
- const newPosition = (i + 1) * 1000;
-
- await db.execute(
- `
- UPDATE zen_workspaces
- SET "position" = :newPosition
- WHERE uuid = :uuid
- `,
- { newPosition, uuid: workspace.uuid }
- );
-
- changedUUIDs.add(workspace.uuid);
-
- // Record the change
- await db.execute(
- `
- INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp)
- VALUES (:uuid, :timestamp)
- `,
- {
- uuid: workspace.uuid,
- timestamp: Math.floor(now / 1000),
- }
- );
- }
-
- await this.updateLastChangeTimestamp(db);
- });
- }
- );
-
- this._notifyWorkspacesChanged('zen-workspace-updated', Array.from(changedUUIDs));
- },
-};
-
-// Integration of workspace-specific bookmarks into Places
-window.ZenWorkspaceBookmarksStorage = {
- async init() {
- await this._ensureTable();
- },
-
- async _ensureTable() {
- await ZenWorkspacesStorage.lazy.PlacesUtils.withConnectionWrapper(
'ZenWorkspaceBookmarksStorage.init',
async (db) => {
// Create table using GUIDs instead of IDs
@@ -498,7 +81,7 @@ window.ZenWorkspaceBookmarksStorage = {
* @returns {Promise} The timestamp of the last change.
*/
async getLastChangeTimestamp() {
- const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection();
+ const db = await this.lazy.PlacesUtils.promiseDBConnection();
const result = await db.executeCached(`
SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change'
`);
@@ -506,7 +89,7 @@ window.ZenWorkspaceBookmarksStorage = {
},
async getBookmarkWorkspaces(bookmarkGuid) {
- const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection();
+ const db = await this.lazy.PlacesUtils.promiseDBConnection();
const rows = await db.execute(
`
@@ -531,7 +114,7 @@ window.ZenWorkspaceBookmarksStorage = {
* }
*/
async getBookmarkGuidsByWorkspace() {
- const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection();
+ const db = await this.lazy.PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids
@@ -554,7 +137,7 @@ window.ZenWorkspaceBookmarksStorage = {
* @returns {Promise