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