Compare commits

..

69 Commits

Author SHA1 Message Date
mr. m
5ce257de83 feat: Allow unload to run instantly and fix closing windows on mac, b=no-bug, c=no-component 2025-12-10 18:09:07 +01:00
mr. m
d97404d03b Merge branch 'dev' into window-sync 2025-12-08 12:10:44 +01:00
Mr. M
60c0afc5ac Merge branch 'window-sync' of https://github.com/zen-browser/desktop into window-sync 2025-12-07 18:58:09 +01:00
Mr. M
307e8fc0ef feat: Make sure to not lose any tabs when opening from a private window, b=no-bug, c=workspaces 2025-12-07 18:58:04 +01:00
mr. m
cd8f8fdb6f Merge branch 'dev' into window-sync 2025-12-07 18:36:06 +01:00
Mr. M
f720dce237 feat: Make sure to properly flush all windows when making a new one and fix removing progress listeners, b=no-bug, c=workspaces 2025-12-06 18:49:26 +01:00
Mr. M
cde32ca1e0 feat: Implemented 'Move To...' Button for unsynced windows, b=no-bug, c=workspaces, common 2025-12-05 14:02:09 +01:00
Mr. M
aecea1f565 feat: Improved support for unsynced windows support, b=no-bug, c=workspaces, folders 2025-12-05 09:35:22 +01:00
Mr. M
73f408c9e1 feat: Stop rendering sub-layers when swaping browsers, b=no-bug, c=common 2025-12-04 17:05:39 +01:00
Mr. M
899d590329 fix: Move back active views when closing a window, b=no-bug, c=no-component 2025-12-03 23:43:23 +01:00
Mr. M
22f2b1f29b feat: Early support for unsynced windoiws, b=no-bug, c=workspaces 2025-12-03 17:57:46 +01:00
Mr. M
5d964511aa chore: Update patches to ff 146, b=no-bug, c=no-component 2025-12-03 16:49:37 +01:00
mr. m
a0268ace63 Discard changes to src/browser/components/tabbrowser/content/tab-js.patch 2025-12-03 16:47:45 +01:00
Mr. M
8be603ac88 Merge branch 'dev' of https://github.com/zen-browser/desktop into window-sync 2025-12-03 16:45:04 +01:00
Mr. M
8bf790df4c feat: Start syncing folders as well, b=no-bug, c=folders 2025-12-02 17:33:39 +01:00
Mr. M
a84bd5b0ce fix: Fixed going back to a different window not allowing to type on inputs, b=no-bug, c=no-component 2025-12-02 14:32:16 +01:00
Mr. M
ae62f5c41d feat: Run session saves right before writing and quiting, b=no-bug, c=common 2025-11-28 17:39:54 +01:00
Mr. M
7dd3cf5817 feat: Leave a screenshot of the page behind when switching windows or tabs, b=no-bug, c=common 2025-11-28 13:55:23 +01:00
Mr. M
722ffa5e97 chore: Remove extra patch, b=no-bug, c=no-component 2025-11-23 19:09:45 +01:00
Mr. M
57233a18f6 feat: Start moving browser views to the selected windows, b=no-bug, c=no-component 2025-11-23 19:08:39 +01:00
Mr. M
04ab9a4b4e feat: Listen for more tab events and properly sync them, b=no-bug, c=common, folders, tabs 2025-11-22 18:15:18 +01:00
Mr. M
1fcef12d0a feat: Listen to new tab opens for new sync system, b=no-bug, c=common, folders, tabs 2025-11-22 13:58:15 +01:00
Mr. M
c6cd912262 feat: Start making use of IDs instead of sync identifiers, b=no-bug, c=folders 2025-11-21 14:29:58 +01:00
Mr. M
a765d3642f Merge branch 'window-sync' of https://github.com/zen-browser/desktop into window-sync 2025-11-21 13:19:30 +01:00
Mr. M
40bc51f904 feat: Run session saver before opening a new window, b=no-bug, c=no-component 2025-11-21 13:16:57 +01:00
Mr. M
8a963a9257 feat: Move window sync to its own JS module, b=no-bug, c=workspaces 2025-11-21 13:16:43 +01:00
Mr. M
9c5bf19ae3 feat: Clone the previous state, b=no-bug, c=no-component 2025-11-21 13:12:22 +01:00
Mr. M
6fbba04dab feat: Run session saver before opening a new winodw, b=no-bug, c=tabs 2025-11-21 13:12:22 +01:00
mr. m
c5df63711b Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs 2025-11-21 13:11:56 +01:00
mr. m
a61846bf9a Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch 2025-11-21 13:11:56 +01:00
mr. m
c2a5066181 Discard changes to src/browser/components/tabbrowser/content/tab-js.patch 2025-11-21 13:11:47 +01:00
mr. m
00417582fb Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch 2025-11-21 13:11:47 +01:00
Mr. M
32d603ced9 chore: Update patches to ff 145, b=no-bug, c=no-component 2025-11-21 13:11:46 +01:00
Mr. M
2706e8761d feat: Dont restore windows that are already initialized, b=no-bug, c=no-component 2025-11-21 13:11:39 +01:00
Mr. M
61659c63d9 feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces 2025-11-21 13:11:39 +01:00
Mr. M
34c725aad0 feat: Start doing out own session restore, b=no-bug, c=folders, tabs 2025-11-21 13:08:38 +01:00
mr. m
435762c682 Discard changes to prefs/browser.yaml 2025-11-21 13:06:46 +01:00
Mr. M
c60990c283 feat: Properly handle tab moves, b=no-bug, c=workspaces 2025-11-21 13:06:46 +01:00
Mr. M
1d26040fc6 feat: Start on new session restore, b=no-bug, c=no-component 2025-11-21 13:06:46 +01:00
Mr. M
acb3708936 feat: Dont session duplicate the tabs, b=no-bug, c=workspaces 2025-11-21 13:06:46 +01:00
mr. m
c3b9c3c526 feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces 2025-11-21 13:06:45 +01:00
Mr. M
0adf260ddc feat: Full cross-window workspace syncing, b=no-bug, c=workspaces 2025-11-21 13:03:46 +01:00
Mr. M
ce986beb2f feat: Run session saver before opening a new window, b=no-bug, c=no-component 2025-11-17 18:51:14 +01:00
Mr. M
c86875b7b0 feat: Move window sync to its own JS module, b=no-bug, c=workspaces 2025-11-17 13:36:57 +01:00
Mr. M
eefc8cb20c feat: Clone the previous state, b=no-bug, c=no-component 2025-11-13 17:03:27 +01:00
Mr. M
12c921fd87 feat: Run session saver before opening a new winodw, b=no-bug, c=tabs 2025-11-13 14:40:52 +01:00
mr. m
e419c4dc9f Merge branch 'dev' into window-sync
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2025-11-13 13:58:38 +01:00
mr. m
68b37ac736 Discard changes to src/zen/tabs/ZenPinnedTabsStorage.mjs 2025-11-13 13:58:18 +01:00
mr. m
7f225ac3ee Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch 2025-11-13 13:57:55 +01:00
mr. m
3e39ef2538 Discard changes to src/browser/components/tabbrowser/content/tab-js.patch 2025-11-13 13:57:42 +01:00
mr. m
e5517eb164 Discard changes to src/browser/components/sessionstore/SessionStore-sys-mjs.patch 2025-11-13 13:57:32 +01:00
Mr. M
c4dd470864 chore: Update patches to ff 145, b=no-bug, c=no-component 2025-11-13 13:56:31 +01:00
Mr. M
bf1b0dcd48 feat: Dont restore windows that are already initialized, b=no-bug, c=no-component 2025-11-01 13:40:14 +01:00
Mr. M
76acc8b0e4 feat: Stop using pinned manager and use zen session sidebar, b=no-bug, c=common, folders, tabs, workspaces 2025-10-31 23:07:38 +01:00
mr. m
1b83b77cad Merge branch 'dev' into window-sync
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2025-10-31 21:56:19 +01:00
Mr. M
79ff574978 feat: Start doing out own session restore, b=no-bug, c=folders, tabs 2025-10-29 23:57:20 +01:00
mr. m
af20a65fa1 Merge branch 'dev' into window-sync 2025-10-19 23:11:11 +02:00
mr. m
4a7f8fc9c0 Merge branch 'dev' into window-sync 2025-10-19 12:53:06 +02:00
mr. m
a738a829de Merge branch 'dev' into window-sync 2025-10-18 19:53:04 +02:00
mr. m
240a031e38 Discard changes to prefs/browser.yaml 2025-10-18 19:52:53 +02:00
Mr. M
9bc7b9ce4e Merge branch 'window-sync' of https://github.com/zen-browser/desktop into window-sync 2025-09-28 23:45:17 +02:00
Mr. M
86006c8891 feat: Start on new session restore, b=no-bug, c=no-component 2025-09-28 23:45:13 +02:00
Mr. M
a55b1c7495 feat: Properly handle tab moves, b=no-bug, c=workspaces 2025-09-27 17:54:28 +02:00
mr. m
6e6337a95b Merge branch 'dev' into window-sync 2025-09-27 16:58:29 +02:00
mr. m
6b12153c8a Merge branch 'dev' into window-sync 2025-09-27 09:35:44 +02:00
mr. m
f6922ef2ba Merge branch 'dev' into window-sync 2025-09-03 13:06:44 +02:00
Mr. M
91f5d58fbc feat: Dont session duplicate the tabs, b=no-bug, c=workspaces 2025-09-02 16:21:04 +02:00
mr. m
7a4cdaa45c feat: Also change icons and labels if the tab is pending, b=no-bug, c=tabs, workspaces 2025-09-01 16:09:59 +02:00
Mr. M
81e854a89f feat: Full cross-window workspace syncing, b=no-bug, c=workspaces 2025-08-27 23:25:07 +02:00
41 changed files with 1704 additions and 1580 deletions

View File

@@ -34,7 +34,7 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `146.0`! 🚀
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `145.0.2`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 146.0`!
### Contributing

View File

@@ -1 +1 @@
8908c8eb566c64521e2a777ad8a80b62bd6aa193
acd4663cd1530303fcca85b420a78f4686c4628b

View File

@@ -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

View File

@@ -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: true

View File

@@ -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: true

View File

@@ -45,7 +45,6 @@
# Scripts used all over the browser
<script type="module" src="chrome://browser/content/zen-components/ZenFolder.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenPinnedTabsStorage.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenWorkspacesStorage.mjs"></script>
<script type="module" src="chrome://browser/content/zen-components/ZenMediaController.mjs"></script>

View File

@@ -82,23 +82,21 @@
# They must all go from the middle to the right side. They must always stay verically centered.
# And reach to 180 on the right side, meaning we must divide the width in 16 segments.
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="337.5,180" style="background: rgb(224, 224, 224);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="337.5,180" style="background: rgb(224, 224, 224);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="315,180" style="background: rgb(192, 192, 192);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="292.5,180" style="background: rgb(160, 160, 160);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="270,180" style="background: rgb(128, 128, 128);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="247.5,180" style="background: rgb(96, 96, 96);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="225,180" style="background: rgb(64, 64, 64);"></box>
data-position="180,180" style="background: rgb(0, 0, 0);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="202.5,180" style="background: rgb(32, 32, 32);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="180,180" style="background: rgb(0, 0, 0);"></box>
data-position="225,180" style="background: rgb(64, 64, 64);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="247.5,180" style="background: rgb(96, 96, 96);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="270,180" style="background: rgb(128, 128, 128);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="292.5,180" style="background: rgb(160, 160, 160);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="315,180" style="background: rgb(192, 192, 192);"></box>
<box data-type="explicit-black-white" data-algo="float" data-num-dots="1"
data-position="337.5,180" style="background: rgb(224, 224, 224);"></box>
</hbox>
</hbox>
<toolbarbutton id="PanelUI-zen-gradient-generator-color-page-right" class="toolbarbutton-1" />
@@ -122,12 +120,10 @@
</defs>
</svg>
</box>
<html:input type="range" value="0.4" step="0.001" id="PanelUI-zen-gradient-generator-opacity"
<html:input type="range" max="0.9" value="0.4" step="0.001" id="PanelUI-zen-gradient-generator-opacity"
#ifdef XP_MACOSX
max="0.75"
min="0.35"
min="0.25"
#else
max="0.9"
min="0.35"
#endif
/>

View File

@@ -59,3 +59,8 @@
<menuseparator />
<menuitem id="context_zenOpenSiteSettings" data-l10n-id="zen-site-data-site-settings"/>
</menupopup>
<menupopup id="zenMoveTabsToSyncedWorkspacePopup">
# Popup to move tabs to a synced workspace.
# This would be automatically populated with the list of available synced workspaces.
</menupopup>

View File

@@ -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;
},

View File

@@ -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);
},

View File

@@ -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;

View File

@@ -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..e1d95e97819718d70fab23850a8810dd216db7a8 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,7 +28,42 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
@@ -2384,11 +2388,9 @@ var SessionStoreInternal = {
@@ -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)
@@ -2384,11 +2401,9 @@ var SessionStoreInternal = {
tabbrowser.selectedTab.label;
}
@@ -32,7 +75,25 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// 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 = {
@@ -2465,7 +2480,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);
}
@@ -2486,7 +2501,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) {
@@ -3373,7 +3388,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -41,12 +102,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
return;
}
@@ -4089,6 +4091,12 @@ var SessionStoreInternal = {
@@ -4089,6 +4104,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 +115,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
@@ -4525,6 +4533,7 @@ var SessionStoreInternal = {
@@ -4525,6 +4546,7 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -62,7 +123,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
@@ -5374,7 +5383,7 @@ var SessionStoreInternal = {
@@ -5374,7 +5396,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -71,7 +132,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
removableTabs.push(tab);
}
}
@@ -5434,7 +5443,7 @@ var SessionStoreInternal = {
@@ -5434,7 +5456,7 @@ var SessionStoreInternal = {
}
let workspaceID = aWindow.getWorkspaceID();
@@ -80,7 +141,16 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
winData.workspaceID = workspaceID;
}
},
@@ -5625,11 +5634,12 @@ var SessionStoreInternal = {
@@ -5483,7 +5505,7 @@ 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 +5647,12 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -94,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
@@ -5640,6 +5650,7 @@ var SessionStoreInternal = {
@@ -5640,6 +5663,7 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
@@ -102,7 +172,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
@@ -5652,7 +5663,7 @@ var SessionStoreInternal = {
@@ -5652,7 +5676,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 +181,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
@@ -5765,8 +5776,8 @@ var SessionStoreInternal = {
@@ -5765,8 +5789,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -122,7 +192,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
selectTab = Math.min(selectTab, winData.tabs.length);
}
@@ -5809,6 +5820,8 @@ var SessionStoreInternal = {
@@ -5809,6 +5833,8 @@ var SessionStoreInternal = {
winData.tabs,
winData.groups ?? []
);
@@ -131,7 +201,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
this._log.debug(
`restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs`
);
@@ -6372,6 +6385,25 @@ var SessionStoreInternal = {
@@ -6372,6 +6398,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -145,8 +215,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 +227,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -7290,7 +7322,7 @@ var SessionStoreInternal = {
@@ -7290,7 +7335,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -166,7 +236,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..61bfa1b530c407dd1236543f785eb221
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7305,7 +7337,7 @@ var SessionStoreInternal = {
@@ -7305,7 +7350,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;

View File

@@ -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..e1d2c5ca0cbe5431df01f8d6411f88e3325e0ee9 100644
--- a/browser/components/sessionstore/TabState.sys.mjs
+++ b/browser/components/sessionstore/TabState.sys.mjs
@@ -85,7 +85,22 @@ class _TabState {
@@ -85,7 +85,23 @@ 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,7 @@ 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._zenIsActiveTab = tab._zenContentsVisible;
+
tabData.searchMode = tab.ownerGlobal.gURLBar.getSearchMode(browser, true);
+ if (tabData.searchMode?.source === tab.ownerGlobal.UrlbarUtils.RESULT_SOURCE.ZEN_ACTIONS) {

View File

@@ -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,
});

View File

@@ -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..9a619171c7f1f7ba43c2c661bdad370f83733bf0 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._labelIsInitialTitle) {
+ return false;
+ }
+ gZenPinnedTabManager.onTabLabelChanged(aTab);
+ if (!aLabel || aLabel.includes("about:reader?") || aTab.hasAttribute("zen-has-static-label")) {
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 <browser> 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,27 @@ 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 @@
@@ -6213,7 +6372,8 @@
AppConstants.BROWSER_CHROME_URL,
"_blank",
options,
- aTab
+ aTab,
+ zenForceSync ? "zen-synced" : "zen-unsynced"
);
}
@@ -6319,7 +6479,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
@@ -694,7 +744,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
}
/**
@@ -6404,8 +6555,8 @@
@@ -6404,8 +6564,8 @@
}
// Don't allow mixing pinned and unpinned tabs.
@@ -705,7 +755,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else {
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
}
@@ -6431,10 +6582,16 @@
@@ -6431,10 +6591,16 @@
this.#handleTabMove(
element,
() => {
@@ -724,7 +774,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
if (neighbor && this.isTab(element) && tabIndex > element._tPos) {
neighbor.after(element);
} else {
@@ -6492,23 +6649,28 @@
@@ -6492,23 +6658,28 @@
#moveTabNextTo(element, targetElement, moveBefore = false, metricsContext) {
if (this.isTabGroupLabel(targetElement)) {
targetElement = targetElement.group;
@@ -759,7 +809,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 +6692,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 +845,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
element.pinned
? this.tabContainer.pinnedTabsContainer
: this.tabContainer;
@@ -6537,7 +6719,7 @@
@@ -6537,7 +6728,7 @@
element,
() => {
if (moveBefore) {
@@ -804,7 +854,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
} else if (targetElement) {
targetElement.after(element);
} else {
@@ -6607,10 +6789,10 @@
@@ -6607,10 +6798,10 @@
* @param {TabMetricsContext} [metricsContext]
*/
moveTabToGroup(aTab, aGroup, metricsContext) {
@@ -817,7 +867,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
return;
}
if (aTab.group && aTab.group.id === aGroup.id) {
@@ -6656,6 +6838,7 @@
@@ -6656,6 +6847,7 @@
let state = {
tabIndex: tab._tPos,
@@ -825,7 +875,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
};
if (tab.visible) {
state.elementIndex = tab.elementIndex;
@@ -6682,7 +6865,7 @@
@@ -6682,7 +6874,7 @@
let changedTabGroup =
previousTabState.tabGroupId != currentTabState.tabGroupId;
@@ -834,7 +884,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
tab.dispatchEvent(
new CustomEvent("TabMove", {
bubbles: true,
@@ -6723,6 +6906,10 @@
@@ -6723,6 +6915,10 @@
moveActionCallback();
@@ -845,7 +895,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 +7819,7 @@
// preventDefault(). It will still raise the window if appropriate.
break;
}
@@ -854,7 +904,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
window.focus();
aEvent.preventDefault();
break;
@@ -7640,7 +7827,6 @@
@@ -7640,7 +7836,6 @@
}
case "TabGroupCollapse":
aEvent.target.tabs.forEach(tab => {
@@ -862,7 +912,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
});
break;
case "TabGroupCreateByUser":
@@ -8589,6 +8775,7 @@
@@ -8589,6 +8784,7 @@
aWebProgress.isTopLevel
) {
this.mTab.setAttribute("busy", "true");
@@ -870,7 +920,7 @@ index 42027bfa55eab8ea9298a7d425f2ded45188f7f3..6aa725c0e5a6a29dc33fcb162f83522f
gBrowser._tabAttrModified(this.mTab, ["busy"]);
this.mTab._notselectedsinceload = !this.mTab.selected;
}
@@ -9623,7 +9810,7 @@ var TabContextMenu = {
@@ -9623,7 +9819,7 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
@@ -879,11 +929,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(

View File

@@ -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

View File

@@ -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');

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -263,7 +263,7 @@
& .urlbar-background {
--zen-urlbar-background-base: light-dark(
#fbfbfb,
color-mix(in srgb, hsl(0, 0%, 6.7%), var(--zen-colors-primary) 30%)
color-mix(in srgb, hsl(0, 0%, 1%), var(--zen-colors-primary) 30%)
);
@media -moz-pref('zen.theme.acrylic-elements') {
--zen-urlbar-background-transparent: color-mix(
@@ -505,8 +505,8 @@
--urlbar-container-height: 62px !important;
--urlbar-margin-inline: 10px !important;
min-width: min(90%, 62rem) !important;
width: var(--zen-urlbar-width, min(90%, 62rem)) !important;
min-width: min(90%, 50rem) !important;
width: var(--zen-urlbar-width, min(90%, 50rem)) !important;
font-size: 1.15em !important;
@media (-moz-platform: macos) {
font-size: 1.5em !important;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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(
@@ -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;
}

View File

@@ -13,4 +13,5 @@ DIRS += [
"tests",
"urlbar",
"toolkit",
"sessionstore",
]

View File

@@ -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

View File

@@ -0,0 +1,226 @@
/* 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 { ...this.#sidebar };
}
set data(data) {
console.log(data);
this.#sidebar = data;
}
}
export class nsZenSessionManager {
#file;
#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);
}
}
async readFile() {
console.log(await this.#file.load());
try {
this.#sidebar = (await this.#file.load()) || {};
} catch (e) {
console.error('ZenSessionManager: Failed to read session file', e);
this.#sidebar = {};
}
}
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);
return;
}
// 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.
this.#file.data = this.#sidebar;
this.#file.saveSoon();
this.log(`Saving Zen session data with ${this.#sidebar.tabs?.length || 0} tabs`);
}
/**
* 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;
}
restoreWindowData(aWindowData) {
const sidebar = this.#sidebar;
console.log(sidebar);
if (!sidebar) {
return;
}
aWindowData.tabs = sidebar.tabs || [];
aWindowData.splitViewData = sidebar.splitViewData;
aWindowData.folders = sidebar.folders;
aWindowData.groups = sidebar.groups;
}
restoreNewWindow(aWindow, SessionStoreInternal) {
if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) {
return;
}
this.log('Restoring new window with Zen session data');
aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => {
lazy.setTimeout(() => {
const state = lazy.SessionStore.getCurrentState(true);
const windows = (state.windows || []).find(
(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 || []);
delete newWindow.selected;
const newState = { windows: [newWindow] };
this.log(`Cloning window with ${newWindow.tabs.length} tabs`);
SessionStoreInternal.restoreWindows(aWindow, newState, {
firstWindow: true,
});
resolve();
});
});
}
}
export const ZenSessionStore = new nsZenSessionManager();

View File

@@ -0,0 +1,908 @@
/* 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 = ['unload'];
const EVENTS = [
'TabOpen',
'TabClose',
'ZenTabIconChanged',
'ZenTabLabelChanged',
'TabMove',
'TabPinned',
'TabUnpinned',
'TabAddedToEssentials',
'TabRemovedFromEssentials',
'TabGroupUpdate',
'TabGroupCreate',
'TabGroupRemoved',
'TabGroupMoved',
'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 wan't to keep a stack of these in order
* to handle consequtive 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);
}
lazy.SessionStore.promiseAllWindowsRestored.then(() => {
this.#onSessionStoreInitialized();
});
}
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;
for (let arg of aWindow.arguments) {
if (arg === 'zen-synced') {
forcedSync = true;
} else if (arg === 'zen-unsynced') {
hasUnsyncedArg = true;
}
}
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);
}
}
/** * Generates a unique tab ID.
*
* @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)}`;
}
/**
* Called when the session store has finished initializing for a window.
*
* @param {Window} aWindow - The browser window that has initialized session store.
*/
#onSessionStoreInitialized() {
// For every tab we have in where there's no sync ID, we need to
// assign one and sync it to other windows.
// This should only happen really when updating from an older version
// that didn't have this feature.
this.#runOnAllWindows(null, (aWindow) => {
const { gBrowser } = aWindow;
for (let tab of gBrowser.tabs) {
if (!tab.id) {
tab.id = this.#newTabSyncId;
lazy.TabStateFlusher.flush(tab.linkedBrowser);
}
}
});
}
/**
* 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) {
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) {
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(() => {
try {
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}`;
if (typeof this[handler] === 'function') {
this[handler](aEvent);
} else {
console.warn(`ZenWindowSync: No handler for event type: ${aEvent.type}`);
}
}
/**
* 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 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)) {
gBrowser._setTabLabel(aTargetItem, aOriginalItem.label);
} else if (gBrowser.isTabGroup(aOriginalItem)) {
aTargetItem.label = aOriginalItem.label;
}
}
if (flags & SYNC_FLAG_MOVE && !aTargetItem.hasAttribute('zen-empty-tab')) {
const workspaceId = aOriginalItem.getAttribute('zen-workspace-id');
if (workspaceId) {
aTargetItem.setAttribute('zen-workspace-id', workspaceId);
} else {
aTargetItem.removeAttribute('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);
}
}
}
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.
*/
#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');
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, false, 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) {
// 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') {
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,
() => {
aOurTab.ownerGlobal.gBrowser.swapBrowsersAndCloseOther(aOurTab, aOtherTab, false);
},
onClose
);
const kAttributesToRemove = ['muted', 'soundplaying', 'sharing', 'pictureinpicture'];
// 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 contentx 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 recieving invalid history changes from the the incorrect
// browser view that was just swapped out.
lazy.TabStateFlusher.flush(aOurTab.linkedBrowser);
}
/**
* 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 {boolean} onClose - Indicates if the styling is done during a tab close operation.
*/
async #styleSwapedBrowsers(aOurTab, aOtherTab, onClose = false) {
const ourBrowser = aOurTab.linkedBrowser;
const otherBrowser = aOtherTab.linkedBrowser;
if (!onClose) {
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');
}
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.#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, /* onClose =*/ true);
}
}
}
/**
* 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.
*/
#onTabSwitchOrWindowFocus(aWindow, aPreviousTab = null) {
const selectedTab = aWindow.gBrowser.selectedTab;
if (aPreviousTab?._zenContentsVisible) {
const otherTabToShow = this.#getActiveTabFromOtherWindows(
aWindow,
aPreviousTab.id,
(tab) => tab?.selected
);
if (otherTabToShow) {
otherTabToShow._zenContentsVisible = true;
delete aPreviousTab._zenContentsVisible;
this.#swapBrowserDocShellsAsync(otherTabToShow, aPreviousTab);
}
}
if (selectedTab._zenContentsVisible) {
return;
}
const otherSelectedTab = this.#getActiveTabFromOtherWindows(aWindow, selectedTab.id);
selectedTab._zenContentsVisible = true;
if (otherSelectedTab) {
delete otherSelectedTab._zenContentsVisible;
this.#swapBrowserDocShellsAsync(selectedTab, otherSelectedTab);
}
}
/**
* 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);
}
/* Mark: Public API */
shouldLoadTab(aTab) {
if (!lazy.gWindowSyncEnabled) {
// Since we are never going to sync the tab, we can always load it.
return true;
}
if (aTab._zenContentsVisible) {
// This tab is already active in this window.
return true;
}
// We don't want to trigger a new browser kick-off if there's
// another window where this tab is already active.
return !this.#getActiveTabFromOtherWindows(
aTab.ownerGlobal,
aTab.id,
(tab) => tab?._zenContentsVisible
);
}
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 = (allowSelected = false) => {
const { gBrowser, gZenWorkspaces } = win;
win.focus();
let tabIndex = 0;
let success = true;
for (const tab of tabsToMove) {
if (tab !== selectedTab || allowSelected) {
const newTab = gBrowser.adoptTab(tab, { tabIndex });
if (!newTab) {
// The adoption failed. Restore "fadein" and don't increase the index.
tab.setAttribute('fadein', 'true');
success = false;
continue;
}
gZenWorkspaces.moveTabToWorkspace(newTab, aWorkspaceId);
++tabIndex;
}
}
if (success) {
aWindow.close();
}
};
if (!win) {
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.selected) {
tab._zenContentsVisible = true;
}
if (tab.id) {
// This tab was opened as part of a sync operation.
return;
}
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
);
});
}
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) {
return this.on_TabMove(aEvent);
}
on_TabUnpinned(aEvent) {
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);
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;
this.#onTabSwitchOrWindowFocus(aEvent.target.ownerGlobal, previousTab);
}
on_unload(aEvent) {
const window = aEvent.target.ownerGlobal;
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) {
// This tab group was opened as part of a sync operation.
console.log('Duplicate!');
}
const window = tabGroup.ownerGlobal;
const isFolder = tabGroup.isZenFolder;
const isSplitView = tabGroup.hasAttribute('split-view-group');
// Tab groups already have an ID upon creation.
this.#runOnAllWindows(window, (win) => {
const newGroup = isFolder
? win.gZenFolders.createFolder([], {})
: win.gBrowser.addTabGroup({ splitView: isSplitView });
newGroup.id = tabGroup.id;
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);
}
}
export const ZenWindowSync = new nsZenWindowSync();

View File

@@ -0,0 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.zen += [
"ZenSessionManager.sys.mjs",
"ZenWindowSync.sys.mjs",
]

View File

@@ -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,7 +60,6 @@ class ZenPinnedTabsObserver {
}
class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
hasInitializedPins = false;
promiseInitializedPinned = new Promise((resolve) => {
this._resolvePinnedInitializedInternal = resolve;
});
@@ -103,19 +86,10 @@ 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})`);
}
@@ -150,260 +124,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,238 +133,22 @@ 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 },
})
);
}
async _onTabClick(e) {
const tab = e.target?.closest('tab');
if (e.button === 1 && tab) {
@@ -672,106 +176,10 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return;
}
const browser = tab.linkedBrowser;
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (!pin) {
return;
}
const userContextId = tab.getAttribute('usercontextid');
pin.title = tab.label || browser.contentTitle;
pin.url = browser.currentURI.spec;
pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
await this.savePin(pin);
this.resetPinChangedUrl(tab);
await this.refreshPinnedTabs();
gZenUIManager.showToast('zen-pinned-tab-replaced');
}
async _setPinnedAttributes(tab) {
if (
tab.hasAttribute('zen-pin-id') ||
!this._hasFinishedLoading ||
tab.hasAttribute('zen-empty-tab')
) {
return;
}
this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
const browser = tab.linkedBrowser;
const uuid = gZenUIManager.generateUuidv4();
const userContextId = tab.getAttribute('usercontextid');
let entry = null;
if (tab.getAttribute('zen-pinned-entry')) {
entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
}
await this.savePin({
uuid,
title: entry?.title || tab.label || browser.contentTitle,
url: entry?.url || browser.currentURI.spec,
containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
workspaceUuid: tab.getAttribute('zen-workspace-id'),
isEssential: tab.getAttribute('zen-essential') === 'true',
parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
position: tab._pPos,
});
tab.setAttribute('zen-pin-id', uuid);
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabCreated', {
detail: { tab },
})
);
// This is used while migrating old pins to new system - we don't want to refresh when migrating
if (tab.getAttribute('zen-pinned-entry')) {
tab.removeAttribute('zen-pinned-entry');
return;
}
this.onLocationChange(browser);
await this.refreshPinnedTabs();
}
async _removePinnedAttributes(tab, isClosing = false) {
tab.removeAttribute('zen-has-static-label');
if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
return;
}
if (Services.startup.shuttingDown || window.skipNextCanClose) {
return;
}
this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
this.resetPinChangedUrl(tab);
if (!isClosing) {
tab.removeAttribute('zen-pin-id');
tab.removeAttribute('zen-essential'); // Just in case
if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
const workspace = await gZenWorkspaces.getActiveWorkspace();
tab.setAttribute('zen-workspace-id', workspace.uuid);
}
}
await this.refreshPinnedTabs();
tab.dispatchEvent(
new CustomEvent('ZenPinnedTabRemoved', {
detail: { tab },
})
);
}
_initClosePinnedTabShortcut() {
let cmdClose = document.getElementById('cmd_close');
@@ -780,21 +188,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 +234,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
switch (behavior) {
case 'close': {
for (const tab of pinnedTabs) {
this._removePinnedAttributes(tab, true);
gBrowser.removeTab(tab, { animate: true });
}
break;
@@ -1017,12 +409,6 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
tab.removeAttribute('zen-workspace-id');
}
if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
if (pin) {
pin.isEssential = true;
pin.workspaceUuid = null;
this.savePin(pin);
}
gBrowser.zenHandleTabMove(tab, () => {
if (tab.ownerGlobal !== window) {
tab = gBrowser.adoptTab(tab, {
@@ -1415,11 +801,8 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
}
async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
async updatePinTitle(tab, newTitle, isEdited = true) {
const uuid = tab.getAttribute('zen-pin-id');
await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
await this.refreshPinnedTabs();
const browsers = Services.wm.getEnumerator('navigator:browser');
@@ -1564,6 +947,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
async onTabLabelChanged(tab) {
tab.dispatchEvent(new CustomEvent('ZenTabLabelChanged', { bubbles: true, detail: { tab } }));
if (!this._pinsCache) {
return;
}

View File

@@ -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<string>} uuids - Array of changed workspace UUIDs.
*/
_notifyPinsChanged(event, uuids) {
if (uuids.length === 0) return; // No changes to notify
// Convert the array of UUIDs to a JSON string
const data = JSON.stringify(uuids);
Services.obs.notifyObservers(null, event, data);
},
async savePin(pin, notifyObservers = true) {
// 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<string>} The UUID of the created group
*/
async createGroup(
title,
icon = null,
isCollapsed = false,
workspaceUuid = null,
parentUuid = null,
position = null,
notifyObservers = true
) {
if (!title || typeof title !== 'string') {
throw new Error('Group title is required and must be a string');
}
const groupUuid = gZenUIManager.generateUuidv4();
const groupPin = {
uuid: groupUuid,
title,
folderIcon: icon || null,
isFolderCollapsed: isCollapsed || false,
workspaceUuid,
parentUuid,
position,
isGroup: true,
isEssential: false,
editedTitle: true, // Group titles are always considered edited
};
await this.savePin(groupPin, notifyObservers);
return groupUuid;
},
/**
* Add an existing tab/pin to a group
* @param {string} tabUuid - The UUID of the tab to add to the group
* @param {string} groupUuid - The UUID of the target group
* @param {number} position - The position within the group (optional, will append if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
*/
async addTabToGroup(tabUuid, groupUuid, position = null, notifyObservers = true) {
if (!tabUuid || !groupUuid) {
throw new Error('Both tabUuid and groupUuid are required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.addTabToGroup', async (db) => {
await db.executeTransaction(async () => {
// Verify the group exists and is actually a group
const groupCheck = await db.execute(
`SELECT is_group FROM zen_pins WHERE uuid = :groupUuid`,
{ groupUuid }
);
if (groupCheck.length === 0) {
throw new Error(`Group with UUID ${groupUuid} does not exist`);
}
if (!groupCheck[0].getResultByName('is_group')) {
throw new Error(`Pin with UUID ${groupUuid} is not a group`);
}
const tabCheck = await db.execute(`SELECT uuid FROM zen_pins WHERE uuid = :tabUuid`, {
tabUuid,
});
if (tabCheck.length === 0) {
throw new Error(`Tab with UUID ${tabUuid} does not exist`);
}
const now = Date.now();
let newPosition;
if (position !== null && Number.isFinite(position)) {
newPosition = position;
} else {
// Get the maximum position within the group
const maxPositionResult = await db.execute(
`SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid = :groupUuid`,
{ groupUuid }
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
newPosition = maxPosition + 1000;
}
await db.execute(
`
UPDATE zen_pins
SET folder_parent_uuid = :groupUuid,
position = :newPosition,
updated_at = :now
WHERE uuid = :tabUuid
`,
{
tabUuid,
groupUuid,
newPosition,
now,
}
);
changedUUIDs.add(tabUuid);
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: tabUuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
/**
* Remove a tab from its group (move to root level)
* @param {string} tabUuid - The UUID of the tab to remove from its group
* @param {number} newPosition - The new position at root level (optional, will append if not provided)
* @param {boolean} notifyObservers - Whether to notify observers (default: true)
*/
async removeTabFromGroup(tabUuid, newPosition = null, notifyObservers = true) {
if (!tabUuid) {
throw new Error('tabUuid is required');
}
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper(
'ZenPinnedTabsStorage.removeTabFromGroup',
async (db) => {
await db.executeTransaction(async () => {
// Verify the tab exists and is in a group
const tabCheck = await db.execute(
`SELECT folder_parent_uuid FROM zen_pins WHERE uuid = :tabUuid`,
{ tabUuid }
);
if (tabCheck.length === 0) {
throw new Error(`Tab with UUID ${tabUuid} does not exist`);
}
if (!tabCheck[0].getResultByName('folder_parent_uuid')) {
return;
}
const now = Date.now();
let finalPosition;
if (newPosition !== null && Number.isFinite(newPosition)) {
finalPosition = newPosition;
} else {
// Get the maximum position at root level (where folder_parent_uuid is null)
const maxPositionResult = await db.execute(
`SELECT MAX("position") as max_position FROM zen_pins WHERE folder_parent_uuid IS NULL`
);
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
finalPosition = maxPosition + 1000;
}
// Update the tab to be at root level
await db.execute(
`
UPDATE zen_pins
SET folder_parent_uuid = NULL,
position = :newPosition,
updated_at = :now
WHERE uuid = :tabUuid
`,
{
tabUuid,
newPosition: finalPosition,
now,
}
);
changedUUIDs.add(tabUuid);
// Record the change
await db.execute(
`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`,
{
uuid: tabUuid,
timestamp: Math.floor(now / 1000),
}
);
await this.updateLastChangeTimestamp(db);
});
}
);
if (notifyObservers) {
this._notifyPinsChanged('zen-pin-updated', Array.from(changedUUIDs));
}
},
async removePin(uuid, notifyObservers = true) {
const 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();
});

View File

@@ -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)

View File

@@ -131,6 +131,7 @@
========================================================================== */
.pinned-tabs-container-separator {
height: 22px;
padding-bottom: 2px;
border: none;
width: 100%;
transition:
@@ -141,7 +142,6 @@
position: relative;
opacity: 1;
align-items: center;
padding: 0 5px;
& toolbarseparator {
height: 1px;
@@ -488,6 +488,7 @@
#navigator-toolbox[zen-sidebar-expanded='true'] {
--zen-toolbox-min-width: fit-content;
--tab-icon-end-margin: 8.5px;
padding: var(--zen-toolbox-padding);
padding-left: 0;
padding-top: 0;
@@ -565,8 +566,8 @@
& #tabbrowser-arrowscrollbox-periphery > toolbarbutton {
width: 100% !important;
border-radius: var(--border-radius-medium) !important;
padding-left: var(--tab-inline-padding) !important;
padding-right: var(--tab-inline-padding) !important;
padding-left: var(--toolbarbutton-inner-padding) !important;
padding-right: var(--toolbarbutton-inner-padding) !important;
& label {
display: flex;
@@ -599,7 +600,6 @@
& #tabbrowser-tabs {
--tab-inline-padding: 8px;
--tab-icon-end-margin: 8.5px;
& .tabbrowser-tab {
& .tab-label-container {
@@ -1110,22 +1110,14 @@
max-height 0.3s ease-out,
grid-template-columns 0.3s ease-out;
opacity: 1;
--min-essentials-width-wrap: calc(var(--tab-min-height) + 4px);
grid-template-columns: repeat(
auto-fit,
minmax(max(23.7%, var(--min-essentials-width-wrap)), 1fr)
);
grid-template-columns: repeat(auto-fit, minmax(max(23.7%, 48px), 1fr));
&[data-hack-type='1'] {
grid-template-columns: repeat(
auto-fit,
minmax(max(30%, var(--min-essentials-width-wrap)), auto)
);
grid-template-columns: repeat(auto-fit, minmax(max(30%, 48px), auto));
}
&[data-hack-type='2'] {
grid-template-columns: repeat(
auto-fit,
minmax(max(23%, var(--min-essentials-width-wrap)), 1fr)
minmax(max(23%, var(--min-essentials-width-wrap)), 1fr)
minmax(max(23%, 48px), 1fr) minmax(max(23%, 48px), 1fr)
);
}
&[data-hack-type='3'] {
@@ -1168,6 +1160,7 @@
& .tab-background {
border-radius: var(--border-radius-medium) !important;
transition: background 0.1s ease-in-out;
}
--tab-selected-bgcolor: light-dark(rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.2));

View File

@@ -41,15 +41,8 @@ function parseSinePath(pathStr) {
return points;
}
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, 'MAX_OPACITY', () => {
return parseFloat(document.getElementById('PanelUI-zen-gradient-generator-opacity').max);
});
ChromeUtils.defineLazyGetter(lazy, 'MIN_OPACITY', () => {
return parseFloat(document.getElementById('PanelUI-zen-gradient-generator-opacity').min);
});
const MAX_OPACITY = 0.9;
const MIN_OPACITY = AppConstants.platform === 'macosx' ? 0.25 : 0.35;
const EXPLICIT_LIGHTNESS_TYPE = 'explicit-lightness';
const EXPLICIT_BLACKWHITE_TYPE = 'explicit-black-white';
@@ -447,7 +440,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
const rect = gradient.getBoundingClientRect();
const padding = 30; // each side
const dotHalfSize = 29; // half the size of the dot. -11 for correct centering
const dotHalfSize = 38 / 2; // half the size of the dot
x += dotHalfSize;
y += dotHalfSize;
rect.width += padding * 2; // Adjust width and height for padding
@@ -1102,17 +1095,15 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
colorToBlendOpacity = 0.35;
} else if (AppConstants.platform === 'macosx') {
colorToBlend = [255, 255, 255];
colorToBlendOpacity = 0.35;
colorToBlendOpacity = 0.3;
}
if (colorToBlend) {
const blendedAlpha = Math.min(
1,
opacity + lazy.MIN_OPACITY + colorToBlendOpacity * (1 - (opacity + lazy.MIN_OPACITY))
opacity + MIN_OPACITY + colorToBlendOpacity * (1 - (opacity + MIN_OPACITY))
);
baseColor = this.blendColors(baseColor, colorToBlend, blendedAlpha * 100);
if (AppConstants.platform !== 'macosx') {
opacity += colorToBlendOpacity * (1 - opacity);
}
opacity += colorToBlendOpacity * (1 - opacity);
}
return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${opacity})`;
}
@@ -1395,13 +1386,13 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
const [_, secondStop, thirdStop] = document.querySelectorAll(
'#PanelUI-zen-gradient-generator-slider-wave-gradient stop'
);
// Opacity can only be between lazy.MIN_OPACITY to lazy.MAX_OPACITY. Make opacity relative to that range
if (opacity < lazy.MIN_OPACITY) {
// Opacity can only be between MIN_OPACITY to MAX_OPACITY. Make opacity relative to that range
if (opacity < MIN_OPACITY) {
opacity = 0;
} else if (opacity > lazy.MAX_OPACITY) {
} else if (opacity > MAX_OPACITY) {
opacity = 1;
} else {
opacity = (opacity - lazy.MIN_OPACITY) / (lazy.MAX_OPACITY - lazy.MIN_OPACITY);
opacity = (opacity - MIN_OPACITY) / (MAX_OPACITY - MIN_OPACITY);
}
if (isDefaultTheme) {
opacity = 1; // If it's the default theme, we want the wave to be

View File

@@ -37,6 +37,14 @@ class nsZenWorkspace extends MozXULElement {
`;
}
static get moveTabToButtonMarkup() {
return `
<toolbarbutton class="toolbarbutton-1 chromeclass-toolbar-additional zen-workspaces-actions"
tooltip="dynamic-shortcut-tooltip"
data-l10n-id="zen-move-tab-to-workspace-button" />
`;
}
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 = (await gZenWorkspaces.getWorkspaces()).workspaces;
let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid);
workspaceData.name = newName;
await gZenWorkspaces.saveWorkspace(workspaceData);
@@ -256,6 +278,37 @@ 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 = '';
gZenWorkspaces.getWorkspaces(true).then((workspaces) => {
for (const workspace of workspaces.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);

View File

@@ -126,7 +126,7 @@ class nsZenWorkspaceIcons extends MozXULElement {
}
async #updateIcons() {
const workspaces = await gZenWorkspaces._workspaces();
const workspaces = await gZenWorkspaces.getWorkspaces();
this.innerHTML = '';
for (const workspace of workspaces.workspaces) {
const button = this.#createWorkspaceIcon(workspace);

View File

@@ -128,7 +128,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this.addPopupListeners();
await this.#waitForPromises();
await this._workspaces();
await this.getWorkspaces();
await this.afterLoadInit();
}
@@ -318,7 +318,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async _createDefaultWorkspaceIfNeeded() {
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
if (!workspaces.workspaces.length) {
await this.createAndSaveWorkspace('Space', null, true);
this._workspaceCache = null;
@@ -413,7 +413,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
await new Promise((resolve) => {
setTimeout(async () => {
const tabs = gBrowser.tabContainer.allTabs;
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
for (const workspace of workspaces.workspaces) {
await this._createWorkspaceTabsSection(workspace, tabs);
}
@@ -835,8 +835,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return PrivateBrowsingUtils.isWindowPrivate(window);
}
get currentWindowIsSyncing() {
return !document.documentElement.hasAttribute('zen-unsynced-window') && !this.isPrivateWindow;
}
get privateWindowOrDisabled() {
return this.isPrivateWindow || !this.shouldHaveWorkspaces;
return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing;
}
get workspaceEnabled() {
@@ -860,17 +864,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
}
async _workspaces() {
if (this._workspaceCache) {
async getWorkspaces(lieToMe = false) {
if (this._workspaceCache && !lieToMe) {
return this._workspaceCache;
}
if (this.isPrivateWindow) {
if (!this.currentWindowIsSyncing && !lieToMe) {
this._workspaceCache = {
workspaces: this._privateWorkspace ? [this._privateWorkspace] : [],
workspaces: this._tempWorkspace ? [this._tempWorkspace] : [],
lastChangeTimestamp: 0,
};
this._activeWorkspace = this._privateWorkspace?.uuid;
this._activeWorkspace = this._tempWorkspace?.uuid;
return this._workspaceCache;
}
@@ -883,21 +887,23 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// 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) {
if (!lieToMe) {
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;
}
} 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;
}
@@ -928,6 +934,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
let activeWorkspace = await this.getActiveWorkspace();
this.activeWorkspace = activeWorkspace?.uuid;
await gZenSessionStore.promiseInitialized;
await window._zenPromiseNewWindowRestored;
delete window._zenPromiseNewWindowRestored;
try {
if (activeWorkspace) {
window.gZenThemePicker = new nsZenThemePicker();
@@ -939,7 +947,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
await this.workspaceBookmarks();
await this.initializeTabsStripSections();
this._initializeEmptyTab();
await gZenPinnedTabManager.refreshPinnedTabs({ init: true });
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
@@ -1112,13 +1119,15 @@ 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 = await this.getWorkspaces();
for (let tab of tabs) {
const workspaceID = tab.getAttribute('zen-workspace-id');
if (
@@ -1206,6 +1215,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') {
@@ -1249,20 +1276,7 @@ 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');
}
const item = this.generateMenuItemForWorkspace(workspace);
item.addEventListener('command', (e) => {
this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id'));
});
@@ -1307,7 +1321,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async removeWorkspace(windowID) {
let workspacesData = await this._workspaces();
let workspacesData = await this.getWorkspaces();
await this.changeWorkspace(
workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID)
);
@@ -1330,7 +1344,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async getActiveWorkspace() {
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
return (
workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ??
workspaces.workspaces[0]
@@ -1364,7 +1378,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
browser.gZenWorkspaces._workspaceCache = null;
browser.gZenWorkspaces._workspaceBookmarksCache = null;
}
let workspaces = await browser.gZenWorkspaces._workspaces();
let workspaces = await browser.gZenWorkspaces.getWorkspaces();
browser.document
.getElementById('cmd_zenCtxDeleteWorkspace')
.setAttribute('disabled', workspaces.workspaces.length <= 1);
@@ -1389,7 +1403,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) {
const currentWindowIsPrivate = this.isPrivateWindow;
const currentWindowIsPrivate = !this.currentWindowIsSyncing;
if (onInit) {
if (currentWindowIsPrivate) return;
return await this._propagateWorkspaceDataForWindow(this.ownerWindow, {
@@ -1402,7 +1416,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// For example, when the window is in private browsing mode.
if (
!browser.gZenWorkspaces.workspaceEnabled ||
browser.gZenWorkspaces.isPrivateWindow !== currentWindowIsPrivate
!browser.gZenWorkspaces.currentWindowIsSyncing !== currentWindowIsPrivate
) {
return;
}
@@ -1417,7 +1431,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
if (this.privateWindowOrDisabled) {
return;
}
const workspaces = (await this._workspaces()).workspaces;
const workspaces = (await this.getWorkspaces()).workspaces;
const workspace = workspaces.find((w) => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
@@ -1443,7 +1457,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) {
const workspaces = (await this._workspaces()).workspaces;
const workspaces = (await this.getWorkspaces()).workspaces;
const draggedIndex = workspaces.findIndex((w) => w.uuid === draggedWorkspaceId);
const draggedWorkspace = workspaces.splice(draggedIndex, 1)[0];
const targetIndex = workspaces.findIndex((w) => w.uuid === targetWorkspaceId);
@@ -1478,11 +1492,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!tab.hasAttribute('zen-empty-tab') &&
!tab.hasAttribute('zen-essential')
);
for (const tab of tabs) {
if (tab.pinned) {
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
}
}
gBrowser.removeTabs(tabs, {
animate: false,
skipSessionStore: true,
@@ -1661,7 +1670,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
gZenFolders.cancelPopupTimer();
// Refresh tab cache
@@ -1777,7 +1786,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
return;
}
this._organizingWorkspaceStrip = true;
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid);
if (!justMove) {
this._fixIndicatorsNames(workspaces);
@@ -1911,7 +1920,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const kGlobalAnimationDuration = 0.2;
this._animatingChange = true;
const animations = [];
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid);
const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex;
const clonedEssentials = [];
@@ -2214,7 +2223,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
tab,
currentWorkspace.uuid,
currentWorkspace.containerTabId,
await this._workspaces()
await this.getWorkspaces()
);
}
@@ -2273,7 +2282,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
const lastSelectedTab = this._lastSelectedWorkspaceTabs[workspace.uuid];
const containerId = workspace.containerTabId?.toString();
const workspaces = await this._workspaces();
const workspaces = await 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) {
@@ -2406,7 +2415,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async _updateWorkspacesChangeContextMenu() {
if (gZenWorkspaces.privateWindowOrDisabled) return;
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup');
if (!menuPopup) {
@@ -2456,8 +2465,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,8 +2483,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
!dontChange,
containerTabId
);
if (this.isPrivateWindow) {
this._privateWorkspace = workspaceData;
if (!this.currentWindowIsSyncing) {
this._tempWorkspace = workspaceData;
} else {
await this.saveWorkspace(workspaceData, dontChange);
}
@@ -2710,7 +2719,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
// Context menu management
async contextChangeContainerTab(event) {
this._organizingWorkspaceStrip = true;
let workspaces = await this._workspaces();
let workspaces = await this.getWorkspaces();
let workspace = workspaces.workspaces.find(
(workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace)
);
@@ -2764,7 +2773,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
async changeWorkspaceShortcut(offset = 1, whileScrolling = false) {
// Cycle through workspaces
let workspaces = await this._workspaces();
let workspaces = await this.getWorkspaces();
let activeWorkspace = await this.getActiveWorkspace();
let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace);
@@ -2828,7 +2837,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
this._lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent(
tabs[tabs.length - 1]
);
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
await this.changeWorkspace(
workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)
);
@@ -2887,7 +2896,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature {
}
async shortcutSwitchTo(index) {
const workspaces = await this._workspaces();
const workspaces = await this.getWorkspaces();
// The index may be out of bounds, if it doesnt exist, don't do anything
if (index >= workspaces.workspaces.length || index < 0) {
return;

View File

@@ -33,7 +33,7 @@ zen-workspace-creation {
width: calc(100% - 10px);
margin: auto;
gap: 3.2rem;
margin-top: 1.2rem;
margin-top: 0.6rem;
height: 100%;
& .zen-workspace-creation-form {

View File

@@ -181,7 +181,11 @@
height: calc(100% - var(--zen-toolbox-padding) * 2);
}
:root:not([zen-private-window]) & {
:root[zen-private-window] & {
pointer-events: none;
}
:root:not([zen-unsynced-window]) & {
&:hover,
&[open='true'] {
&::before {
@@ -228,15 +232,19 @@
font-weight: 600;
align-items: center;
margin: 0;
:root[zen-unsynced-window] & {
pointer-events: none;
}
}
.zen-workspaces-actions {
margin-left: auto !important;
opacity: 0;
visibility: collapse;
transition: opacity 0.1s;
order: 5;
--toolbarbutton-inner-padding: 6px !important;
opacity: 0;
visibility: collapse;
& image {
border-radius: max(calc(var(--border-radius-medium) - 4px), 4px) !important;
@@ -248,9 +256,24 @@
:root[zen-renaming-tab='true'] & {
display: none;
}
:root[zen-unsynced-window] & {
.toolbarbutton-text {
display: flex;
font-size: 10px;
min-height: 22px;
}
.toolbarbutton-icon {
display: none;
}
}
}
:root:not([zen-private-window]) &:hover .zen-workspaces-actions,
:root[zen-unsynced-window='true']
#navigator-toolbox[zen-has-implicit-hover='true']
& .zen-workspaces-actions,
:root:not([zen-unsynced-window]) &:hover .zen-workspaces-actions,
& .zen-workspaces-actions[open='true'] {
visibility: visible;
pointer-events: auto;

View File

@@ -26,7 +26,6 @@ export default [
'ZEN_KEYSET_ID',
'gZenPinnedTabManager',
'ZenPinnedTabsStorage',
'gZenEmojiPicker',
'gZenSessionStore',

View File

@@ -5,7 +5,7 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "146.0",
"version": "145.0.2",
"candidate": "146.0"
},
"buildOptions": {
@@ -19,7 +19,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.17.13b",
"displayVersion": "1.17.12b",
"github": {
"repo": "zen-browser/desktop"
},