From 6367a5aba01bf1a5c4469e5970961c77ed144e7a Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Mon, 15 Dec 2025 19:19:52 +0100 Subject: [PATCH] feat: Implement workspace sync store into the session file, b=closes #10857, c=common, tabs, tests, workspaces --- .../base/content/zen-preloaded.inc.xhtml | 1 - .../places/content/editBookmark-js.patch | 34 +- .../sessionstore/SessionStore-sys-mjs.patch | 25 +- src/zen/common/modules/ZenStartup.mjs | 8 +- src/zen/common/zen-sets.js | 255 ++++----- .../sessionstore/ZenSessionManager.sys.mjs | 73 ++- src/zen/sessionstore/ZenWindowSync.sys.mjs | 40 +- src/zen/tabs/ZenPinnedTabManager.mjs | 2 +- .../browser_container_auto_switch.js | 6 +- .../browser_container_specific_essentials.js | 6 +- .../workspaces/browser_basic_workspaces.js | 6 +- .../workspaces/browser_change_to_empty.js | 2 +- .../workspaces/browser_workspace_bookmarks.js | 6 +- src/zen/urlbar/ZenUBGlobalActions.sys.mjs | 4 +- src/zen/workspaces/ZenGradientGenerator.mjs | 11 +- src/zen/workspaces/ZenWorkspace.mjs | 43 +- src/zen/workspaces/ZenWorkspaceCreation.mjs | 6 +- src/zen/workspaces/ZenWorkspaceIcons.mjs | 9 +- src/zen/workspaces/ZenWorkspaces.mjs | 538 +++++++----------- src/zen/workspaces/ZenWorkspacesStorage.mjs | 433 +------------- src/zen/workspaces/ZenWorkspacesSync.mjs | 459 --------------- src/zen/workspaces/jar.inc.mn | 1 - src/zen/zen.globals.js | 1 - 23 files changed, 498 insertions(+), 1471 deletions(-) delete mode 100644 src/zen/workspaces/ZenWorkspacesSync.mjs diff --git a/src/browser/base/content/zen-preloaded.inc.xhtml b/src/browser/base/content/zen-preloaded.inc.xhtml index ab5354092..76f32fd5f 100644 --- a/src/browser/base/content/zen-preloaded.inc.xhtml +++ b/src/browser/base/content/zen-preloaded.inc.xhtml @@ -6,7 +6,6 @@ # the window is fully loaded. # Make sure they are loaded before the global-scripts.inc file. - diff --git a/src/browser/components/places/content/editBookmark-js.patch b/src/browser/components/places/content/editBookmark-js.patch index 100ee182d..92392184f 100644 --- a/src/browser/components/places/content/editBookmark-js.patch +++ b/src/browser/components/places/content/editBookmark-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/places/content/editBookmark.js b/browser/components/places/content/editBookmark.js -index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4016594b9 100644 +index f562f19741d882d92365da531b55e2810a0e79ea..a68ce8191314845c589f3a9f14b56028e0532628 100644 --- a/browser/components/places/content/editBookmark.js +++ b/browser/components/places/content/editBookmark.js @@ -387,6 +387,10 @@ var gEditItemOverlay = { @@ -31,34 +31,11 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4 } break; } -@@ -1280,6 +1288,148 @@ var gEditItemOverlay = { +@@ -1280,6 +1288,128 @@ var gEditItemOverlay = { get bookmarkState() { return this._bookmarkState; }, + -+ async _initWorkspaceSelector() { -+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") { -+ return; -+ } -+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces(); -+ -+ const selectElement = this._workspaceSelect; -+ -+ // Clear any existing options -+ while (selectElement.firstChild) { -+ selectElement.removeChild(selectElement.firstChild); -+ } -+ -+ // For each workspace, create an option element -+ for (let workspace of this._workspaces) { -+ const option = document.createElementNS("http://www.w3.org/1999/xhtml", "option"); -+ option.textContent = workspace.name; -+ option.value = workspace.uuid; -+ selectElement.appendChild(option); -+ } -+ -+ selectElement.disabled = this.readOnly; -+ }, + async onWorkspaceSelectionChange(event) { + if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") { + return; @@ -129,7 +106,10 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4 + if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") { + return; + } -+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces(); ++ const { ZenSessionStore } = ChromeUtils.importESModule( ++ "resource:///modules/zen/ZenSessionManager.sys.mjs" ++ ); ++ this._workspaces = ZenSessionStore.getClonedSpaces(); + const workspaceList = this._workspaceList; + if(aInfo.node?.bookmarkGuid) { + this._selectedWorkspaces = await ZenWorkspaceBookmarksStorage.getBookmarkWorkspaces(aInfo.node.bookmarkGuid); @@ -180,7 +160,7 @@ index f562f19741d882d92365da531b55e2810a0e79ea..9339e1158b074c41fc19bf91cbfde3c4 }; ChromeUtils.defineLazyGetter(gEditItemOverlay, "_folderTree", () => { -@@ -1318,6 +1468,9 @@ for (let elt of [ +@@ -1318,6 +1448,9 @@ for (let elt of [ "locationField", "keywordField", "tagsField", diff --git a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch index 1d9c6ab95..c8cbd23a3 100644 --- a/src/browser/components/sessionstore/SessionStore-sys-mjs.patch +++ b/src/browser/components/sessionstore/SessionStore-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs -index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9bbfa3d264 100644 +index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..5ce4eb0b21cf4ed2b8e7c6ad0c57e77416a2ab48 100644 --- a/browser/components/sessionstore/SessionStore.sys.mjs +++ b/browser/components/sessionstore/SessionStore.sys.mjs @@ -127,6 +127,8 @@ const TAB_EVENTS = [ @@ -56,9 +56,9 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b + for (let window of this._browserWindows) { + windowPromises.push(lazy.TabStateFlusher.flushWindow(window)); + } -+ aWindow._zenPromiseNewWindowRestored = new Promise((resolve) => Promise.all(windowPromises).finally(() => { -+ lazy.ZenSessionStore.restoreNewWindow(aWindow, this, resolve); -+ })); ++ Promise.all(windowPromises).finally(() => { ++ lazy.ZenSessionStore.restoreNewWindow(aWindow, this); ++ }); + } if (this._restoreLastWindow && aWindow.toolbar.visible) { @@ -137,7 +137,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b // window data is still in _statesToRestore continue; } -@@ -5625,11 +5650,15 @@ var SessionStoreInternal = { +@@ -5625,11 +5650,16 @@ var SessionStoreInternal = { } let tabbrowser = aWindow.gBrowser; @@ -150,11 +150,12 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b + winData.activeZenSpace = aWindow.gZenWorkspaces?.activeWorkspace || null; + winData.splitViewData = aWindow.gZenViewSplitter?.storeDataForSessionStore(); + winData.folders = aWindow.gZenFolders?.storeDataForSessionStore() || []; ++ winData.spaces = aWindow.gZenWorkspaces?.getWorkspaces(); + // update the internal state data for this window for (let tab of tabs) { if (tab == aWindow.FirefoxViewHandler.tab) { -@@ -5652,7 +5681,7 @@ var SessionStoreInternal = { +@@ -5652,7 +5682,7 @@ var SessionStoreInternal = { // a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab, // since it's only inserted into the tab strip after it's selected). if (aWindow.FirefoxViewHandler.tab?.selected) { @@ -163,7 +164,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b winData.title = tabbrowser.tabs[0].label; } winData.selected = selectedIndex; -@@ -5765,8 +5794,8 @@ var SessionStoreInternal = { +@@ -5765,8 +5795,8 @@ var SessionStoreInternal = { // selectTab represents. let selectTab = 0; if (overwriteTabs) { @@ -174,17 +175,17 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b selectTab = Math.min(selectTab, winData.tabs.length); } -@@ -5809,6 +5838,9 @@ var SessionStoreInternal = { +@@ -5809,6 +5839,9 @@ var SessionStoreInternal = { winData.tabs, winData.groups ?? [] ); + aWindow.gZenFolders?.restoreDataFromSessionStore(winData.folders); + aWindow.gZenViewSplitter?.restoreDataFromSessionStore(winData.splitViewData); -+ aWindow.gZenWorkspaces.activeWorkspace = winData.activeZenSpace || null; ++ aWindow.gZenWorkspaces?.restoreWorkspacesFromSessionStore(winData); this._log.debug( `restoreWindow, createTabsForSessionRestore returned ${tabs.length} tabs` ); -@@ -6372,6 +6404,25 @@ var SessionStoreInternal = { +@@ -6372,6 +6405,25 @@ var SessionStoreInternal = { // Most of tabData has been restored, now continue with restoring // attributes that may trigger external events. @@ -210,7 +211,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b if (tabData.pinned) { tabbrowser.pinTab(tab); -@@ -7290,7 +7341,7 @@ var SessionStoreInternal = { +@@ -7290,7 +7342,7 @@ var SessionStoreInternal = { let groupsToSave = new Map(); for (let tIndex = 0; tIndex < window.tabs.length; ) { @@ -219,7 +220,7 @@ index 2c2f43bf743ef458b378e85e9ed44a971711e1d9..cc9185308c83abfa173041a10edbcb9b // Adjust window.selected if (tIndex + 1 < window.selected) { window.selected -= 1; -@@ -7305,7 +7356,7 @@ var SessionStoreInternal = { +@@ -7305,7 +7357,7 @@ var SessionStoreInternal = { ); // We don't want to increment tIndex here. continue; diff --git a/src/zen/common/modules/ZenStartup.mjs b/src/zen/common/modules/ZenStartup.mjs index 942dd27ee..d85531296 100644 --- a/src/zen/common/modules/ZenStartup.mjs +++ b/src/zen/common/modules/ZenStartup.mjs @@ -12,12 +12,7 @@ class ZenStartup { isReady = false; - async init() { - // important: We do this to ensure that some firefox components - // are initialized before we start our own initialization. - // please, do not remove this line and if you do, make sure to - // test the startup process. - await new Promise((resolve) => setTimeout(resolve, 0)); + init() { this.openWatermark(); this.#initBrowserBackground(); this.#changeSidebarLocation(); @@ -97,6 +92,7 @@ class ZenStartup { // Just in case we didn't get the right size. gZenUIManager.updateTabsToolbar(); this.closeWatermark(); + document.getElementById('tabbrowser-arrowscrollbox').setAttribute('orient', 'vertical'); this.isReady = true; }); } diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index b7b6c00ad..25cfe4b01 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -6,138 +6,135 @@ document.addEventListener( 'MozBeforeInitialXULLayout', () => { // defined in browser-sets.inc - document - .getElementById('zenCommandSet') - - .addEventListener('command', (event) => { - switch (event.target.id) { - case 'cmd_zenCompactModeToggle': - gZenCompactModeManager.toggle(); - break; - case 'cmd_zenCompactModeShowSidebar': - gZenCompactModeManager.toggleSidebar(); - break; - case 'cmd_toggleCompactModeIgnoreHover': - gZenCompactModeManager.toggle(true); - break; - case 'cmd_zenWorkspaceForward': - gZenWorkspaces.changeWorkspaceShortcut(); - break; - case 'cmd_zenWorkspaceBackward': - gZenWorkspaces.changeWorkspaceShortcut(-1); - break; - case 'cmd_zenSplitViewGrid': - gZenViewSplitter.toggleShortcut('grid'); - break; - case 'cmd_zenSplitViewVertical': - gZenViewSplitter.toggleShortcut('vsep'); - break; - case 'cmd_zenSplitViewHorizontal': - gZenViewSplitter.toggleShortcut('hsep'); - break; - case 'cmd_zenSplitViewUnsplit': - gZenViewSplitter.toggleShortcut('unsplit'); - break; - case 'cmd_zenSplitViewContextMenu': - gZenViewSplitter.contextSplitTabs(); - break; - case 'cmd_zenCopyCurrentURLMarkdown': - gZenCommonActions.copyCurrentURLAsMarkdownToClipboard(); - break; - case 'cmd_zenCopyCurrentURL': - gZenCommonActions.copyCurrentURLToClipboard(); - break; - case 'cmd_zenPinnedTabReset': - gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab); - break; - case 'cmd_zenPinnedTabResetNoTab': - gZenPinnedTabManager.resetPinnedTab(); - break; - case 'cmd_zenToggleSidebar': - gZenVerticalTabsManager.toggleExpand(); - break; - case 'cmd_zenOpenZenThemePicker': - gZenThemePicker.openThemePicker(event); - break; - case 'cmd_zenChangeWorkspaceTab': - gZenWorkspaces.changeTabWorkspace( - event.sourceEvent.target.getAttribute('zen-workspace-id') - ); - break; - case 'cmd_zenToggleTabsOnRight': - gZenVerticalTabsManager.toggleTabsOnRight(); - break; - case 'cmd_zenSplitViewLinkInNewTab': - gZenViewSplitter.splitLinkInNewTab(); - break; - case 'cmd_zenNewEmptySplit': - setTimeout(() => { - gZenViewSplitter.createEmptySplit(); - }, 0); - break; - case 'cmd_zenReplacePinnedUrlWithCurrent': - gZenPinnedTabManager.replacePinnedUrlWithCurrent(); - break; - case 'cmd_contextZenAddToEssentials': - gZenPinnedTabManager.addToEssentials(); - break; - case 'cmd_contextZenRemoveFromEssentials': - gZenPinnedTabManager.removeEssentials(); - break; - case 'cmd_zenCtxDeleteWorkspace': - gZenWorkspaces.contextDeleteWorkspace(event); - break; - case 'cmd_zenChangeWorkspaceName': - gZenVerticalTabsManager.renameTabStart({ - target: gZenWorkspaces.activeWorkspaceIndicator.querySelector( - '.zen-current-workspace-indicator-name' - ), - }); - break; - case 'cmd_zenChangeWorkspaceIcon': - gZenWorkspaces.changeWorkspaceIcon(); - break; - case 'cmd_zenReorderWorkspaces': - gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', { - timeout: 9000, - descriptionId: 'zen-workspaces-how-to-reorder-desc', - }); - break; - case 'cmd_zenOpenWorkspaceCreation': - gZenWorkspaces.openWorkspaceCreation(event); - break; - case 'cmd_zenOpenFolderCreation': - gZenFolders.createFolder([], { - renameFolder: true, - }); - break; - case 'cmd_zenTogglePinTab': { - const currentTab = gBrowser.selectedTab; - if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) { - if (currentTab.pinned) { - gBrowser.unpinTab(currentTab); - } else { - gBrowser.pinTab(currentTab); - } + document.getElementById('zenCommandSet').addEventListener('command', (event) => { + switch (event.target.id) { + case 'cmd_zenCompactModeToggle': + gZenCompactModeManager.toggle(); + break; + case 'cmd_zenCompactModeShowSidebar': + gZenCompactModeManager.toggleSidebar(); + break; + case 'cmd_toggleCompactModeIgnoreHover': + gZenCompactModeManager.toggle(true); + break; + case 'cmd_zenWorkspaceForward': + gZenWorkspaces.changeWorkspaceShortcut(); + break; + case 'cmd_zenWorkspaceBackward': + gZenWorkspaces.changeWorkspaceShortcut(-1); + break; + case 'cmd_zenSplitViewGrid': + gZenViewSplitter.toggleShortcut('grid'); + break; + case 'cmd_zenSplitViewVertical': + gZenViewSplitter.toggleShortcut('vsep'); + break; + case 'cmd_zenSplitViewHorizontal': + gZenViewSplitter.toggleShortcut('hsep'); + break; + case 'cmd_zenSplitViewUnsplit': + gZenViewSplitter.toggleShortcut('unsplit'); + break; + case 'cmd_zenSplitViewContextMenu': + gZenViewSplitter.contextSplitTabs(); + break; + case 'cmd_zenCopyCurrentURLMarkdown': + gZenCommonActions.copyCurrentURLAsMarkdownToClipboard(); + break; + case 'cmd_zenCopyCurrentURL': + gZenCommonActions.copyCurrentURLToClipboard(); + break; + case 'cmd_zenPinnedTabReset': + gZenPinnedTabManager.resetPinnedTab(gBrowser.selectedTab); + break; + case 'cmd_zenPinnedTabResetNoTab': + gZenPinnedTabManager.resetPinnedTab(); + break; + case 'cmd_zenToggleSidebar': + gZenVerticalTabsManager.toggleExpand(); + break; + case 'cmd_zenOpenZenThemePicker': + gZenThemePicker.openThemePicker(event); + break; + case 'cmd_zenChangeWorkspaceTab': + gZenWorkspaces.changeTabWorkspace( + event.sourceEvent.target.getAttribute('zen-workspace-id') + ); + break; + case 'cmd_zenToggleTabsOnRight': + gZenVerticalTabsManager.toggleTabsOnRight(); + break; + case 'cmd_zenSplitViewLinkInNewTab': + gZenViewSplitter.splitLinkInNewTab(); + break; + case 'cmd_zenNewEmptySplit': + setTimeout(() => { + gZenViewSplitter.createEmptySplit(); + }, 0); + break; + case 'cmd_zenReplacePinnedUrlWithCurrent': + gZenPinnedTabManager.replacePinnedUrlWithCurrent(); + break; + case 'cmd_contextZenAddToEssentials': + gZenPinnedTabManager.addToEssentials(); + break; + case 'cmd_contextZenRemoveFromEssentials': + gZenPinnedTabManager.removeEssentials(); + break; + case 'cmd_zenCtxDeleteWorkspace': + gZenWorkspaces.contextDeleteWorkspace(event); + break; + case 'cmd_zenChangeWorkspaceName': + gZenVerticalTabsManager.renameTabStart({ + target: gZenWorkspaces.activeWorkspaceIndicator.querySelector( + '.zen-current-workspace-indicator-name' + ), + }); + break; + case 'cmd_zenChangeWorkspaceIcon': + gZenWorkspaces.changeWorkspaceIcon(); + break; + case 'cmd_zenReorderWorkspaces': + gZenUIManager.showToast('zen-workspaces-how-to-reorder-title', { + timeout: 9000, + descriptionId: 'zen-workspaces-how-to-reorder-desc', + }); + break; + case 'cmd_zenOpenWorkspaceCreation': + gZenWorkspaces.openWorkspaceCreation(event); + break; + case 'cmd_zenOpenFolderCreation': + gZenFolders.createFolder([], { + renameFolder: true, + }); + break; + case 'cmd_zenTogglePinTab': { + const currentTab = gBrowser.selectedTab; + if (currentTab && !currentTab.hasAttribute('zen-empty-tab')) { + if (currentTab.pinned) { + gBrowser.unpinTab(currentTab); + } else { + gBrowser.pinTab(currentTab); } - break; } - case 'cmd_zenCloseUnpinnedTabs': - gZenWorkspaces.closeAllUnpinnedTabs(); - break; - case 'cmd_zenUnloadWorkspace': { - gZenWorkspaces.unloadWorkspace(); - break; - } - default: - gZenGlanceManager.handleMainCommandSet(event); - if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) { - const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1; - gZenWorkspaces.shortcutSwitchTo(index); - } - break; + break; } - }); + case 'cmd_zenCloseUnpinnedTabs': + gZenWorkspaces.closeAllUnpinnedTabs(); + break; + case 'cmd_zenUnloadWorkspace': { + gZenWorkspaces.unloadWorkspace(); + break; + } + default: + gZenGlanceManager.handleMainCommandSet(event); + if (event.target.id.startsWith('cmd_zenWorkspaceSwitch')) { + const index = parseInt(event.target.id.replace('cmd_zenWorkspaceSwitch', ''), 10) - 1; + gZenWorkspaces.shortcutSwitchTo(index); + } + break; + } + }); }, { once: true } ); diff --git a/src/zen/sessionstore/ZenSessionManager.sys.mjs b/src/zen/sessionstore/ZenSessionManager.sys.mjs index f4e57c09c..724087d98 100644 --- a/src/zen/sessionstore/ZenSessionManager.sys.mjs +++ b/src/zen/sessionstore/ZenSessionManager.sys.mjs @@ -76,6 +76,39 @@ export class nsZenSessionManager { } } + /** + * Gets the spaces data from the Places database for migration. + * This is only called once during the first run after updating + * to a version that uses the new session manager. + */ + async #getSpacesFromDBForMigration() { + try { + const { PlacesUtils } = ChromeUtils.importESModule( + 'resource://gre/modules/PlacesUtils.sys.mjs' + ); + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.executeCached('SELECT * FROM zen_workspaces ORDER BY created_at ASC'); + this._migrationSpaceData = rows.map((row) => ({ + uuid: row.getResultByName('uuid'), + name: row.getResultByName('name'), + icon: row.getResultByName('icon'), + containerTabId: row.getResultByName('container_id') ?? 0, + position: row.getResultByName('position'), + theme: row.getResultByName('theme_type') + ? { + type: row.getResultByName('theme_type'), + gradientColors: JSON.parse(row.getResultByName('theme_colors')), + opacity: row.getResultByName('theme_opacity'), + rotation: row.getResultByName('theme_rotation'), + texture: row.getResultByName('theme_texture'), + } + : null, + })); + } catch { + /* ignore errors during migration */ + } + } + /** * Reads the session file and populates the sidebar object. * This should be only called once at startup. @@ -83,7 +116,12 @@ export class nsZenSessionManager { */ async readFile() { try { - await this.#file.load(); + let promises = []; + promises.push(this.#file.load()); + if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) { + promises.push(this.#getSpacesFromDBForMigration()); + } + await Promise.all(promises); } catch (e) { console.error('ZenSessionManager: Failed to read session file', e); } @@ -104,6 +142,10 @@ export class nsZenSessionManager { // gotten the opportunity to save the session yet. if (!Services.prefs.getBoolPref(MIGRATION_PREF, false)) { Services.prefs.setBoolPref(MIGRATION_PREF, true); + for (const winData of initialState.windows || []) { + winData.spaces = this._migrationSpaceData || []; + } + delete this._migrationSpaceData; return; } // If there's no initial state, nothing to restore. This would @@ -226,6 +268,7 @@ export class nsZenSessionManager { sidebarData.folders = state.windows[0].folders; sidebarData.splitViewData = state.windows[0].splitViewData; sidebarData.groups = state.windows[0].groups; + sidebarData.spaces = state.windows[0].spaces; } /** @@ -245,6 +288,7 @@ export class nsZenSessionManager { aWindowData.splitViewData = sidebar.splitViewData; aWindowData.folders = sidebar.folders; aWindowData.groups = sidebar.groups; + aWindowData.spaces = sidebar.spaces; } /** @@ -263,9 +307,9 @@ export class nsZenSessionManager { * into a race condition if we try to restore the window synchronously * here. */ - restoreNewWindow(aWindow, SessionStoreInternal, resolvePromise) { + restoreNewWindow(aWindow, SessionStoreInternal) { if (aWindow.gZenWorkspaces?.privateWindowOrDisabled) { - return resolvePromise(); + return; } this.log('Restoring new window with Zen session data'); const state = lazy.SessionStore.getCurrentState(true); @@ -300,17 +344,24 @@ export class nsZenSessionManager { const newState = { windows: [newWindow] }; this.log(`Cloning window with ${newWindow.tabs.length} tabs`); - aWindow.addEventListener( - 'SSWindowRestored', - () => { - lazy.setTimeout(resolvePromise); - }, - { once: true } - ); - SessionStoreInternal._deferredInitialState = newState; SessionStoreInternal.initializeWindow(aWindow, newState); } + + /** + * Gets the cloned spaces data from the sidebar object. + * This is used during migration to restore spaces into + * the initial session state. + * + * @returns {Array} The cloned spaces data. + */ + getClonedSpaces() { + const sidebar = this.#sidebar; + if (!sidebar || !sidebar.spaces) { + return []; + } + return Cu.cloneInto(sidebar.spaces, {}); + } } export const ZenSessionStore = new nsZenSessionManager(); diff --git a/src/zen/sessionstore/ZenWindowSync.sys.mjs b/src/zen/sessionstore/ZenWindowSync.sys.mjs index 72944ad0e..c5eef9f15 100644 --- a/src/zen/sessionstore/ZenWindowSync.sys.mjs +++ b/src/zen/sessionstore/ZenWindowSync.sys.mjs @@ -742,24 +742,11 @@ class nsZenWindowSync { /* 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 - ); - } - + /** + * Sets the initial pinned state for a tab across all windows. + * + * @param {Object} aTab - The tab to set the pinned state for. + */ setPinnedTabState(aTab) { const state = this.#getTabState(aTab); const initialState = { @@ -774,6 +761,23 @@ class nsZenWindowSync { }); } + /** + * Propagates the workspaces to all windows. + * @param {Array} aWorkspaces - The workspaces to propagate. + */ + propagateWorkspacesToAllWindows(aWorkspaces) { + this.#runOnAllWindows(null, (win) => { + win.gZenWorkspaces.propagateWorkspaces(aWorkspaces); + }); + } + + /** + * Moves all tabs from a window to a synced workspace in another window. + * If no synced window exists, creates a new one. + * + * @param {Window} aWindow - The window to move tabs from. + * @param {string} aWorkspaceId - The ID of the workspace to move tabs to. + */ moveTabsToSyncedWorkspace(aWindow, aWorkspaceId) { const tabsToMove = aWindow.gZenWorkspaces.allStoredTabs.filter( (tab) => !tab.hasAttribute('zen-empty-tab') diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index c39c439d4..182fa63eb 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -64,7 +64,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { this._resolvePinnedInitializedInternal = resolve; }); - async init() { + init() { if (!this.enabled) { return; } diff --git a/src/zen/tests/container_essentials/browser_container_auto_switch.js b/src/zen/tests/container_essentials/browser_container_auto_switch.js index b46279668..1e9a94826 100644 --- a/src/zen/tests/container_essentials/browser_container_auto_switch.js +++ b/src/zen/tests/container_essentials/browser_container_auto_switch.js @@ -6,7 +6,7 @@ add_task(async function test_Container_Essentials_Auto_Swithc() { await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); + ok(workspaces.length === 2, 'Two workspaces should exist.'); let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', { skipAnimation: true, @@ -27,11 +27,11 @@ add_task(async function test_Container_Essentials_Auto_Swithc() { const newWorkspaceUUID = gZenWorkspaces.activeWorkspace; Assert.equal( gZenWorkspaces.activeWorkspace, - workspaces.workspaces[1].uuid, + workspaces[1].uuid, 'The new workspace should be active.' ); // Change to the original workspace, there should be no essential tabs - await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]); + await gZenWorkspaces.changeWorkspace(workspaces[0]); await gZenWorkspaces.removeWorkspace(newWorkspaceUUID); }); diff --git a/src/zen/tests/container_essentials/browser_container_specific_essentials.js b/src/zen/tests/container_essentials/browser_container_specific_essentials.js index a376c0165..3d0b82ec8 100644 --- a/src/zen/tests/container_essentials/browser_container_specific_essentials.js +++ b/src/zen/tests/container_essentials/browser_container_specific_essentials.js @@ -6,9 +6,9 @@ add_task(async function test_Check_Creation() { await gZenWorkspaces.createAndSaveWorkspace('Container Profile 1', undefined, false, 1); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); + ok(workspaces.length === 2, 'Two workspaces should exist.'); - await gZenWorkspaces.changeWorkspace(workspaces.workspaces[1]); + await gZenWorkspaces.changeWorkspace(workspaces[1]); let newTab = BrowserTestUtils.addTab(gBrowser, 'about:blank', { skipAnimation: true, userContextId: 1, @@ -28,7 +28,7 @@ add_task(async function test_Check_Creation() { const newWorkspaceUUID = gZenWorkspaces.activeWorkspace; // Change to the original workspace, there should be no essential tabs - await gZenWorkspaces.changeWorkspace(workspaces.workspaces[0]); + await gZenWorkspaces.changeWorkspace(workspaces[0]); ok( !gBrowser.tabs.find( (t) => t.hasAttribute('zen-essential') && t.getAttribute('usercontextid') == 1 diff --git a/src/zen/tests/workspaces/browser_basic_workspaces.js b/src/zen/tests/workspaces/browser_basic_workspaces.js index 2a6ac2017..dc0bc7767 100644 --- a/src/zen/tests/workspaces/browser_basic_workspaces.js +++ b/src/zen/tests/workspaces/browser_basic_workspaces.js @@ -9,9 +9,9 @@ add_task(async function test_Check_Creation() { const currentWorkspaceUUID = gZenWorkspaces.activeWorkspace; await gZenWorkspaces.createAndSaveWorkspace('Test Workspace 2'); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); + ok(workspaces.length === 2, 'Two workspaces should exist.'); ok( - currentWorkspaceUUID !== workspaces.workspaces[1].uuid, + currentWorkspaceUUID !== workspaces[1].uuid, 'The new workspace should be different from the current one.' ); @@ -26,7 +26,7 @@ add_task(async function test_Check_Creation() { const workspacesAfterRemove = await gZenWorkspaces._workspaces(); ok(workspacesAfterRemove.workspaces.length === 1, 'One workspace should exist.'); ok( - workspacesAfterRemove.workspaces[0].uuid === currentWorkspaceUUID, + workspacesAfterRemove[0].uuid === currentWorkspaceUUID, 'The workspace should be the one we started with.' ); ok(gBrowser.tabs.length === 2, 'There should be one tab.'); diff --git a/src/zen/tests/workspaces/browser_change_to_empty.js b/src/zen/tests/workspaces/browser_change_to_empty.js index c7f6f90b7..9913a988f 100644 --- a/src/zen/tests/workspaces/browser_change_to_empty.js +++ b/src/zen/tests/workspaces/browser_change_to_empty.js @@ -19,6 +19,6 @@ add_task(async function test_Change_To_Empty() { ); const workspacesAfterRemove = await gZenWorkspaces._workspaces(); - ok(workspacesAfterRemove.workspaces.length === 1, 'One workspace should exist.'); + ok(workspacesAfterRemove.length === 1, 'One workspace should exist.'); ok(gBrowser.tabs.length === 2, 'There should be two tabs.'); }); diff --git a/src/zen/tests/workspaces/browser_workspace_bookmarks.js b/src/zen/tests/workspaces/browser_workspace_bookmarks.js index 02734f5dd..4a9e7ec48 100644 --- a/src/zen/tests/workspaces/browser_workspace_bookmarks.js +++ b/src/zen/tests/workspaces/browser_workspace_bookmarks.js @@ -107,9 +107,9 @@ add_task(async function test_workspace_bookmark() { await withBookmarksShowing(async () => { await gZenWorkspaces.createAndSaveWorkspace('Test Workspace 2'); const workspaces = await gZenWorkspaces._workspaces(); - ok(workspaces.workspaces.length === 2, 'Two workspaces should exist.'); - const firstWorkspace = workspaces.workspaces[0]; - const secondWorkspace = workspaces.workspaces[1]; + ok(workspaces.length === 2, 'Two workspaces should exist.'); + const firstWorkspace = workspaces[0]; + const secondWorkspace = workspaces[1]; ok( firstWorkspace.uuid !== secondWorkspace.uuid, 'The new workspace should be different from the current one.' diff --git a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs index 12d288346..bc161200a 100644 --- a/src/zen/urlbar/ZenUBGlobalActions.sys.mjs +++ b/src/zen/urlbar/ZenUBGlobalActions.sys.mjs @@ -76,7 +76,7 @@ const globalActionsTemplate = [ command: 'cmd_zenWorkspaceForward', icon: 'chrome://browser/skin/zen-icons/forward.svg', isAvailable: (window) => { - return window.gZenWorkspaces._workspaceCache.workspaces.length > 1; + return window.gZenWorkspaces._workspaceCache.length > 1; }, }, { @@ -85,7 +85,7 @@ const globalActionsTemplate = [ icon: 'chrome://browser/skin/zen-icons/back.svg', isAvailable: (window) => { // This also covers the case of being in private mode - return window.gZenWorkspaces._workspaceCache.workspaces.length > 1; + return window.gZenWorkspaces._workspaceCache.length > 1; }, }, { diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs index 6e52f5a78..a1495d4ed 100644 --- a/src/zen/workspaces/ZenGradientGenerator.mjs +++ b/src/zen/workspaces/ZenGradientGenerator.mjs @@ -1332,7 +1332,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { } // Do not rebuild if the workspace is not the same as the current one - const windowWorkspace = await browser.gZenWorkspaces.getActiveWorkspace(); + const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace(); if (windowWorkspace.uuid !== uuid) { return; } @@ -1630,13 +1630,12 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { }; }); const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture); - let currentWorkspace = await gZenWorkspaces.getActiveWorkspace(); + let currentWorkspace = gZenWorkspaces.getActiveWorkspace(); if (!skipSave) { - await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient); - await gZenWorkspaces._propagateWorkspaceData(); + currentWorkspace.theme = gradient; + gZenWorkspaces.saveWorkspace(currentWorkspace); gZenUIManager.showToast('zen-panel-ui-gradient-generator-saved-message'); - currentWorkspace = await gZenWorkspaces.getActiveWorkspace(); } await this.onWorkspaceChange(currentWorkspace, skipSave, skipSave ? gradient : null); @@ -1691,7 +1690,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature { invalidateGradientCache() { this.#gradientsCache = {}; - window.dispatchEvent(new Event('ZenGradientCacheChanged')); + window.dispatchEvent(new Event('ZenGradientCacheChanged', { bubbles: true })); } getGradientForWorkspace(workspace) { diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs index 05078a0b0..7cb1af16d 100644 --- a/src/zen/workspaces/ZenWorkspace.mjs +++ b/src/zen/workspaces/ZenWorkspace.mjs @@ -231,7 +231,7 @@ class nsZenWorkspace extends MozXULElement { if (newName === '') { return; } - let workspaces = (await gZenWorkspaces.getWorkspaces()).workspaces; + let workspaces = gZenWorkspaces.getWorkspaces(); let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid); workspaceData.name = newName; await gZenWorkspaces.saveWorkspace(workspaceData); @@ -286,28 +286,27 @@ class nsZenWorkspace extends MozXULElement { 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); - } + const workspaces = gZenWorkspaces.getWorkspaces(true); + for (const workspace of workspaces) { + const item = gZenWorkspaces.generateMenuItemForWorkspace(workspace); + item.addEventListener('command', async () => { + const { ZenWindowSync } = ChromeUtils.importESModule( + 'resource:///modules/zen/ZenWindowSync.sys.mjs' + ); + ZenWindowSync.moveTabsToSyncedWorkspace(window, workspace.uuid); + }); + popup.appendChild(item); + } - button.setAttribute('open', 'true'); - popup.addEventListener( - 'popuphidden', - () => { - button.removeAttribute('open'); - }, - { once: true } - ); - popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */); - }); + button.setAttribute('open', 'true'); + popup.addEventListener( + 'popuphidden', + () => { + button.removeAttribute('open'); + }, + { once: true } + ); + popup.openPopup(button, 'after_start', 0, 0, true /* isContextMenu */); } } diff --git a/src/zen/workspaces/ZenWorkspaceCreation.mjs b/src/zen/workspaces/ZenWorkspaceCreation.mjs index cbaeed4c2..d037bade7 100644 --- a/src/zen/workspaces/ZenWorkspaceCreation.mjs +++ b/src/zen/workspaces/ZenWorkspaceCreation.mjs @@ -199,7 +199,7 @@ class nsZenWorkspaceCreation extends MozXULElement { } async onCreateButtonCommand() { - const workspace = await gZenWorkspaces.getActiveWorkspace(); + const workspace = gZenWorkspaces.getActiveWorkspace(); workspace.name = this.inputName.value.trim(); workspace.icon = this.inputIcon.image || this.inputIcon.label || undefined; workspace.containerTabId = this.currentProfile; @@ -320,8 +320,8 @@ class nsZenWorkspaceCreation extends MozXULElement { this.remove(); gZenUIManager.updateTabsToolbar(); - const workspace = await gZenWorkspaces.getActiveWorkspace(); - await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true); + const workspace = gZenWorkspaces.getActiveWorkspace(); + await gZenWorkspaces._organizeWorkspaceStripLocations(workspace); await gZenWorkspaces.updateTabsContainers(); await gZenUIManager.motion.animate( diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs index d56f6ecea..9672c4a27 100644 --- a/src/zen/workspaces/ZenWorkspaceIcons.mjs +++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs @@ -126,13 +126,13 @@ class nsZenWorkspaceIcons extends MozXULElement { } async #updateIcons() { - const workspaces = await gZenWorkspaces.getWorkspaces(); + const workspaces = gZenWorkspaces.getWorkspaces(); this.innerHTML = ''; - for (const workspace of workspaces.workspaces) { + for (const workspace of workspaces) { const button = this.#createWorkspaceIcon(workspace); this.appendChild(button); } - if (workspaces.workspaces.length <= 1) { + if (workspaces.length <= 1) { this.setAttribute('dont-show', 'true'); } else { this.removeAttribute('dont-show'); @@ -168,6 +168,9 @@ class nsZenWorkspaceIcons extends MozXULElement { } i++; } + if (selected == -1) { + return; + } buttons[selected].setAttribute('active', true); this.scrollLeft = buttons[selected].offsetLeft - 10; this.setAttribute('selected', selected); diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs index 6ef191205..dc1f29623 100644 --- a/src/zen/workspaces/ZenWorkspaces.mjs +++ b/src/zen/workspaces/ZenWorkspaces.mjs @@ -2,10 +2,9 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs'; import { nsZenThemePicker } from 'chrome://browser/content/zen-components/ZenGradientGenerator.mjs'; -class nsZenWorkspaces extends nsZenMultiWindowFeature { +class nsZenWorkspaces { /** * Stores workspace IDs and their last selected tabs. */ @@ -13,6 +12,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { #inChangingWorkspace = false; draggedElement = null; + #hasInitialized = false; + #canDebug = Services.prefs.getBoolPref('zen.workspaces.debug', false); #activeWorkspace = ''; @@ -32,18 +33,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { 'BMB_mobileBookmarks', ]; - promiseDBInitialized = new Promise((resolve) => { - this._resolveDBInitialized = resolve; - }); - promisePinnedInitialized = new Promise((resolve) => { this._resolvePinnedInitialized = resolve; }); - promiseSectionsInitialized = new Promise((resolve) => { - this._resolveSectionsInitialized = resolve; - }); - promiseInitialized = new Promise((resolve) => { this._resolveInitialized = resolve; }); @@ -52,11 +45,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (this.privateWindowOrDisabled) { return; } - await Promise.all([ - this.promiseDBInitialized, - this.promisePinnedInitialized, - SessionStore.promiseAllWindowsRestored, - ]); + await Promise.all([this.promisePinnedInitialized, SessionStore.promiseAllWindowsRestored]); } async init() { @@ -129,8 +118,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.addPopupListeners(); await this.#waitForPromises(); - await this.getWorkspaces(); - await this.afterLoadInit(); } @@ -145,8 +132,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { await this.delayedStartup(); } this._initializeWorkspaceTabContextMenus(); - await this.initializeWorkspaces(); - await this.promiseSectionsInitialized; // Non UI related initializations if ( @@ -157,24 +142,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.initializeGestureHandlers(); this.initializeWorkspaceNavigation(); } - - if (!this.privateWindowOrDisabled) { - const observerFunction = async () => { - this._workspaceBookmarksCache = null; - await this.workspaceBookmarks(); - this._invalidateBookmarkContainers(); - }; - Services.obs.addObserver(this, 'weave:engine:sync:finish'); - Services.obs.addObserver(observerFunction, 'workspace-bookmarks-updated'); - window.addEventListener( - 'unload', - () => { - Services.obs.removeObserver(this, 'weave:engine:sync:finish'); - Services.obs.removeObserver(observerFunction, 'workspace-bookmarks-updated'); - }, - { once: true } - ); - } } // Validate browser state before tab operations @@ -315,14 +282,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }); }; this._pinnedTabsResizeObserver = new ResizeObserver(onResize); - await this._createDefaultWorkspaceIfNeeded(); - } - - async _createDefaultWorkspaceIfNeeded() { - const workspaces = await this.getWorkspaces(); - if (!workspaces.workspaces.length) { - await this.createAndSaveWorkspace('Space', null, true); - this._workspaceCache = null; + if (this.privateWindowOrDisabled) { + await this.restoreWorkspacesFromSessionStore({}); } } @@ -411,47 +372,38 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { await SessionStore.promiseAllWindowsRestored; const perifery = document.getElementById('tabbrowser-arrowscrollbox-periphery'); perifery.setAttribute('hidden', 'true'); - await new Promise((resolve) => { - setTimeout(() => { - setTimeout(async () => { - await window._zenPromiseNewWindowRestored; - const tabs = gBrowser.tabContainer.allTabs; - const workspaces = await this.getWorkspaces(); - for (const workspace of workspaces.workspaces) { - await this._createWorkspaceTabsSection(workspace, tabs); + const tabs = gBrowser.tabContainer.allTabs; + const workspaces = this.getWorkspaces(); + for (const workspace of workspaces) { + await this.#createWorkspaceTabsSection(workspace, tabs); + } + if (tabs.length) { + const defaultSelectedContainer = this.workspaceElement(this.activeWorkspace)?.querySelector( + '.zen-workspace-normal-tabs-section' + ); + const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector( + '.zen-workspace-pinned-tabs-section' + ); + // New profile with no workspaces does not have a default selected container + if (defaultSelectedContainer) { + for (const tab of tabs) { + if (tab.hasAttribute('zen-essential')) { + this.getEssentialsSection(tab).appendChild(tab); + continue; + } else if (tab.pinned) { + pinnedContainer.insertBefore(tab, pinnedContainer.lastChild); + continue; } - if (tabs.length) { - const defaultSelectedContainer = this.workspaceElement( - this.activeWorkspace - )?.querySelector('.zen-workspace-normal-tabs-section'); - const pinnedContainer = this.workspaceElement(this.activeWorkspace).querySelector( - '.zen-workspace-pinned-tabs-section' - ); - // New profile with no workspaces does not have a default selected container - if (defaultSelectedContainer) { - for (const tab of tabs) { - if (tab.hasAttribute('zen-essential')) { - this.getEssentialsSection(tab).appendChild(tab); - continue; - } else if (tab.pinned) { - pinnedContainer.insertBefore(tab, pinnedContainer.lastChild); - continue; - } - // before to the last child (perifery) - defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild); - } - } - gBrowser.tabContainer._invalidateCachedTabs(); - } - perifery.setAttribute('hidden', 'true'); - this._hasInitializedTabsStrip = true; - this.registerPinnedResizeObserver(); - this._fixIndicatorsNames(workspaces); - this._resolveSectionsInitialized(); - resolve(); - }); - }); - }); + // before to the last child (perifery) + defaultSelectedContainer.insertBefore(tab, defaultSelectedContainer.lastChild); + } + } + gBrowser.tabContainer._invalidateCachedTabs(); + } + perifery.setAttribute('hidden', 'true'); + this._hasInitializedTabsStrip = true; + this.registerPinnedResizeObserver(); + this._fixIndicatorsNames(workspaces); } getEssentialsSection(container = 0) { @@ -489,7 +441,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return this.getEssentialsSection(currentWorkspace?.containerTabId); } - async _createWorkspaceTabsSection(workspace, tabs = []) { + async #createWorkspaceTabsSection(workspace, tabs = []) { const workspaceWrapper = document.createXULElement('zen-workspace'); const container = document.getElementById('tabbrowser-arrowscrollbox'); workspaceWrapper.id = workspace.uuid; @@ -794,6 +746,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } set activeWorkspace(value) { + if (value === this.#activeWorkspace) { + return; + } + const spaces = this.getWorkspaces(); + if (!spaces.some((ws) => ws.uuid === value)) { + value = spaces[0]?.uuid || ''; + } this.#activeWorkspace = value; if (this.privateWindowOrDisabled) { return; @@ -801,27 +760,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { Services.prefs.setStringPref('zen.workspaces.active', value); } - async observe(subject, topic, data) { - if (topic === 'weave:engine:sync:finish' && data === 'workspaces') { - try { - const lastChangeTimestamp = await ZenWorkspacesStorage.getLastChangeTimestamp(); - - if ( - !this._workspaceCache || - !this._workspaceCache.lastChangeTimestamp || - lastChangeTimestamp > this._workspaceCache.lastChangeTimestamp - ) { - await this._propagateWorkspaceData(); - - const currentWorkspace = await this.getActiveWorkspace(); - await gZenThemePicker.onWorkspaceChange(currentWorkspace); - } - } catch (error) { - console.error('Error updating workspaces after sync:', error); - } - } - } - get shouldHaveWorkspaces() { if (typeof this._shouldHaveWorkspaces === 'undefined') { let chromeFlags = window.docShell.treeOwner @@ -866,52 +804,24 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { getWorkspaceFromId(id) { try { - return this._workspaceCache.workspaces.find((workspace) => workspace.uuid === id); + return this._workspaceCache.find((workspace) => workspace.uuid === id); } catch { return null; } } - async getWorkspaces(lieToMe = false) { - if (this._workspaceCache && !lieToMe) { - return this._workspaceCache; + getWorkspaces(lieToMe = false) { + if (lieToMe) { + const { ZenSessionStore } = ChromeUtils.importESModule( + 'resource:///modules/zen/ZenSessionManager.sys.mjs' + ); + return ZenSessionStore.getClonedSpaces(); } - - if (!this.currentWindowIsSyncing && !lieToMe) { - this._workspaceCache = { - workspaces: this._tempWorkspace ? [this._tempWorkspace] : [], - lastChangeTimestamp: 0, - }; + if (!this.currentWindowIsSyncing) { + this._workspaceCache = this._tempWorkspace ? [this._tempWorkspace] : []; this.#activeWorkspace = this._tempWorkspace?.uuid; return this._workspaceCache; } - - const [workspaces, lastChangeTimestamp] = await Promise.all([ - ZenWorkspacesStorage.getWorkspaces(), - ZenWorkspacesStorage.getLastChangeTimestamp(), - ]); - - this._workspaceCache = { workspaces, lastChangeTimestamp }; - // Get the active workspace ID from preferences - const activeWorkspaceId = this.activeWorkspace; - - if (!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; - } - } - - // sort by position - this._workspaceCache.workspaces.sort( - (a, b) => (a.position ?? Infinity) - (b.position ?? Infinity) - ); return this._workspaceCache; } @@ -938,8 +848,17 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return this._workspaceCache; } + async restoreWorkspacesFromSessionStore(aWinData) { + this._workspaceCache = aWinData.spaces || [ + await this.createAndSaveWorkspace('Space', undefined, true), + ]; + this.activeWorkspace = aWinData.activeZenSpace || this._workspaceCache[0].uuid; + await this.initializeWorkspaces(); + this.#hasInitialized = true; + } + async initializeWorkspaces() { - let activeWorkspace = await this.getActiveWorkspace(); + let activeWorkspace = this.getActiveWorkspace(); this.activeWorkspace = activeWorkspace?.uuid; await gZenSessionStore.promiseInitialized; try { @@ -950,15 +869,15 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } catch (e) { console.error('gZenWorkspaces: Error initializing theme picker', e); } - await this.workspaceBookmarks(); await this.#initializeTabsStripSections(); this.#initializeEmptyTab(); + await this.workspaceBookmarks(); await this.changeWorkspace(activeWorkspace, { onInit: true }); this.#fixTabPositions(); this.onWindowResize(); this._resolveInitialized(); this.#clearAnyZombieTabs(); // Dont call with await - delete window._zenPromiseNewWindowRestored; + delete this._resolveInitialized; const tabUpdateListener = this.updateTabsContainers.bind(this); window.addEventListener('TabOpen', tabUpdateListener); @@ -969,8 +888,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { window.addEventListener('TabUnpinned', tabUpdateListener); window.addEventListener('aftercustomization', tabUpdateListener); window.addEventListener('TabSelect', this.onLocationChange.bind(this)); - window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this)); + + this.#updateWorkspacesChangeContextMenu(); } async selectStartPage() { @@ -1134,13 +1054,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async #clearAnyZombieTabs() { const tabs = this.allStoredTabs; - const workspaces = await this.getWorkspaces(); + const workspaces = this.getWorkspaces(); for (let tab of tabs) { const workspaceID = tab.getAttribute('zen-workspace-id'); if ( (workspaceID && !tab.hasAttribute('zen-essential') && - !workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)) || + !workspaces.find((workspace) => workspace.uuid === workspaceID)) || // Also remove empty tabs that are supposed to be from parent folders but // they dont exist anymore (tab.pinned && tab.hasAttribute('zen-empty-tab') && !tab.group) @@ -1282,7 +1202,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } if (!this.#contextMenuData.workspaceId) { separator.hidden = false; - for (const workspace of [...this._workspaceCache.workspaces].reverse()) { + for (const workspace of [...this._workspaceCache].reverse()) { const item = this.generateMenuItemForWorkspace(workspace); item.addEventListener('command', (e) => { this.changeWorkspaceWithID(e.target.closest('menuitem').getAttribute('zen-workspace-id')); @@ -1316,48 +1236,35 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }); } - async saveWorkspace(workspaceData, preventPropagation = false) { + saveWorkspace(workspaceData) { if (this.privateWindowOrDisabled) { return; } - await ZenWorkspacesStorage.saveWorkspace(workspaceData); - if (!preventPropagation) { - await this._propagateWorkspaceData(); - await this._updateWorkspacesChangeContextMenu(); + const workspacesData = this.getWorkspaces(); + const index = workspacesData.findIndex((ws) => ws.uuid === workspaceData.uuid); + if (index !== -1) { + workspacesData[index] = workspaceData; + } else { + workspacesData.push(workspaceData); } + this.#propagateWorkspaceData(); } - async removeWorkspace(windowID) { - let workspacesData = await this.getWorkspaces(); - await this.changeWorkspace( - workspacesData.workspaces.find((workspace) => workspace.uuid !== windowID) - ); - await this.#deleteAllTabsInWorkspace(windowID); - delete this.lastSelectedWorkspaceTabs[windowID]; - await ZenWorkspacesStorage.removeWorkspace(windowID); + removeWorkspace(windowID) { + let workspacesData = this.getWorkspaces(); // Remove the workspace from the cache - this._workspaceCache.workspaces = this._workspaceCache.workspaces.filter( - (workspace) => workspace.uuid !== windowID - ); - await this._propagateWorkspaceData(); - await this._updateWorkspacesChangeContextMenu(); - this.workspaceElement(windowID)?.remove(); - this.onWindowResize(); - this.registerPinnedResizeObserver(); + workspacesData = workspacesData.filter((workspace) => workspace.uuid !== windowID); + this.#propagateWorkspaceData(workspacesData); } isWorkspaceActive(workspace) { return workspace.uuid === this.activeWorkspace; } - async getActiveWorkspace() { - const workspaces = await this.getWorkspaces(); - return ( - workspaces.workspaces.find((workspace) => workspace.uuid === this.activeWorkspace) ?? - workspaces.workspaces[0] - ); + getActiveWorkspace() { + return this.getActiveWorkspaceFromCache(); } - // Workspaces dialog UI management + workspaceHasIcon(workspace) { return workspace.icon && workspace.icon !== ''; } @@ -1380,65 +1287,75 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ); } - async _propagateWorkspaceDataForWindow(browser, { ignoreStrip = false, clearCache = true } = {}) { - if (clearCache) { - browser.gZenWorkspaces._workspaceCache = null; - browser.gZenWorkspaces._workspaceBookmarksCache = null; - } - let workspaces = await browser.gZenWorkspaces.getWorkspaces(); - browser.document - .getElementById('cmd_zenCtxDeleteWorkspace') - .setAttribute('disabled', workspaces.workspaces.length <= 1); - if (clearCache) { - browser.dispatchEvent( - new CustomEvent('ZenWorkspacesUIUpdate', { - bubbles: true, - detail: { activeIndex: browser.gZenWorkspaces.activeWorkspace }, - }) - ); - for (const workspace of workspaces.workspaces) { - // Add workspace elements if they dont exist on other windows - if (!browser.gZenWorkspaces.workspaceElement(workspace.uuid)) { - await browser.gZenWorkspaces._createWorkspaceTabsSection(workspace); - } - } - } - await browser.gZenWorkspaces.workspaceBookmarks(); - if (!ignoreStrip) { - browser.gZenWorkspaces._fixIndicatorsNames(workspaces); + #propagateWorkspaceData(aSpaceData = null) { + if (!this.#hasInitialized || this.privateWindowOrDisabled) { + return; } + window.gZenWindowSync.propagateWorkspacesToAllWindows(aSpaceData ?? this._workspaceCache); } - async _propagateWorkspaceData({ ignoreStrip = false, clearCache = true, onInit = false } = {}) { - const currentWindowIsPrivate = !this.currentWindowIsSyncing; - if (onInit) { - if (currentWindowIsPrivate) return; - return await this._propagateWorkspaceDataForWindow(this.ownerWindow, { - ignoreStrip, - clearCache, - }); - } - await this.foreachWindowAsActive(async (browser) => { - // Do not update the window if workspaces are not enabled in it. - // For example, when the window is in private browsing mode. + propagateWorkspaces(aWorkspaces) { + const previousWorkspaces = this._workspaceCache || []; + this._workspaceCache = aWorkspaces; + let hasChanged = false; + // Remove any workspace elements here that no longer exist + for (const previousWorkspace of previousWorkspaces) { if ( - !browser.gZenWorkspaces.workspaceEnabled || - !browser.gZenWorkspaces.currentWindowIsSyncing !== currentWindowIsPrivate + this.workspaceElement(previousWorkspace.uuid) && + !aWorkspaces.find((w) => w.uuid === previousWorkspace.uuid) ) { - return; + if (this.isWorkspaceActive(previousWorkspace)) { + // If the removed workspace was active, switch to another one + const newActiveWorkspace = + aWorkspaces.find((w) => w.uuid !== previousWorkspace.uuid) || null; + this.changeWorkspace(newActiveWorkspace); + } + this.workspaceElement(previousWorkspace.uuid)?.remove(); + delete this.lastSelectedWorkspaceTabs[previousWorkspace.uuid]; + hasChanged = true; } - this._propagateWorkspaceDataForWindow(browser, { - ignoreStrip, - clearCache, - }).catch(console.error); + } + // Add any new workspace elements here + for (const workspace of aWorkspaces) { + if (!this.workspaceElement(workspace.uuid)) { + this.#createWorkspaceTabsSection(workspace).catch((e) => { + console.error('Error creating workspace tabs section:', e); + }); + hasChanged = true; + } + } + // Order the workspace elements correctly + let previousElement = null; + for (const workspace of aWorkspaces) { + const workspaceElement = this.workspaceElement(workspace.uuid); + if (workspaceElement) { + if (previousElement === null) { + gZenUIManager.tabsWrapper.insertBefore( + workspaceElement, + gZenUIManager.tabsWrapper.firstChild + ); + hasChanged = true; + } else if (previousElement.nextSibling !== workspaceElement) { + gZenUIManager.tabsWrapper.insertBefore(workspaceElement, previousElement.nextSibling); + hasChanged = true; + } + previousElement = workspaceElement; + } + } + if (hasChanged) { + this.#fireSpaceUIUpdate(); + } + this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache()).finally(() => { + this.updateTabsContainers(); }); + this.#updateWorkspacesChangeContextMenu(); } async reorderWorkspace(id, newPosition) { if (this.privateWindowOrDisabled) { return; } - const workspaces = (await this.getWorkspaces()).workspaces; + const workspaces = this.getWorkspaces(); const workspace = workspaces.find((w) => w.uuid === id); if (!workspace) { console.warn(`Workspace with ID ${id} not found for reordering.`); @@ -1457,26 +1374,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } workspaces.splice(newPosition, 0, workspace); - // Update the positions in the storage - await ZenWorkspacesStorage.updateWorkspacePositions(workspaces); // Propagate the changes - await this._propagateWorkspaceData(); - } - - async moveWorkspace(draggedWorkspaceId, targetWorkspaceId) { - const workspaces = (await this.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); - workspaces.splice(targetIndex, 0, draggedWorkspace); - - await ZenWorkspacesStorage.updateWorkspacePositions(workspaces); - await this._propagateWorkspaceData(); + this.#propagateWorkspaceData(); } async openWorkspaceCreation() { let createForm; - const previousWorkspace = await this.getActiveWorkspace(); + const previousWorkspace = this.getActiveWorkspace(); document.documentElement.setAttribute('zen-creating-workspace', 'true'); await this.createAndSaveWorkspace('Space', undefined, false, 0, { beforeChangeCallback: async (workspace) => { @@ -1490,22 +1394,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { createForm.finishSetup(); } - // Workspaces management - - async #deleteAllTabsInWorkspace(workspaceID) { - const tabs = Array.from(this.allStoredTabs).filter( - (tab) => - tab.getAttribute('zen-workspace-id') === workspaceID && - !tab.hasAttribute('zen-empty-tab') && - !tab.hasAttribute('zen-essential') - ); - gBrowser.removeTabs(tabs, { - animate: false, - skipSessionStore: true, - closeWindowWithLastTab: false, - }); - } - #unpinnedTabsInWorkspace(workspaceID) { return Array.from(this.allStoredTabs).filter( (tab) => tab.getAttribute('zen-workspace-id') === workspaceID && tab.visible && !tab.pinned @@ -1669,7 +1557,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { workspace, { onInit = false, alwaysChange = false, whileScrolling = false } = {} ) { - const previousWorkspace = await this.getActiveWorkspace(); + const previousWorkspace = this.getActiveWorkspace(); alwaysChange = alwaysChange || onInit; this.activeWorkspace = workspace.uuid; if (previousWorkspace && previousWorkspace.uuid === workspace.uuid && !alwaysChange) { @@ -1677,11 +1565,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } - const workspaces = await this.getWorkspaces(); + const workspaces = this.getWorkspaces(); gZenFolders.cancelPopupTimer(); // Refresh tab cache - for (const otherWorkspace of workspaces.workspaces) { + for (const otherWorkspace of workspaces) { const container = this.workspaceElement(otherWorkspace.uuid); container.active = otherWorkspace.uuid === workspace.uuid; } @@ -1703,9 +1591,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { gBrowser.warmupTab(tabToSelect); // Update UI and state - const previousWorkspaceIndex = workspaces.workspaces.findIndex( - (w) => w.uuid === previousWorkspace.uuid - ); + const previousWorkspaceIndex = workspaces.findIndex((w) => w.uuid === previousWorkspace.uuid); await this._updateWorkspaceState(workspace, onInit, tabToSelect, { previousWorkspaceIndex, previousWorkspace, @@ -1793,8 +1679,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } this._organizingWorkspaceStrip = true; - const workspaces = await this.getWorkspaces(); - let workspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === workspace.uuid); + const workspaces = this.getWorkspaces(); + let workspaceIndex = workspaces.findIndex((w) => w.uuid === workspace.uuid); if (!justMove) { this._fixIndicatorsNames(workspaces); } @@ -1803,10 +1689,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { ); const workspaceContextId = workspace.containerTabId; const nextWorkspaceContextId = - workspaces.workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId; - for (const otherWorkspace of workspaces.workspaces) { + workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId; + for (const otherWorkspace of workspaces) { const element = this.workspaceElement(otherWorkspace.uuid); - const newTransform = -(workspaceIndex - workspaces.workspaces.indexOf(otherWorkspace)) * 100; + const newTransform = -(workspaceIndex - workspaces.indexOf(otherWorkspace)) * 100; element.style.transform = `translateX(${newTransform + offsetPixels / 2}%)`; } // Hide other essentials with different containerTabId @@ -1840,7 +1726,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } if (offsetPixels) { // Find the next workspace we are scrolling to - const nextWorkspace = workspaces.workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]; + const nextWorkspace = workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]; if (nextWorkspace) { const { gradient: nextGradient, @@ -1911,7 +1797,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } _fixIndicatorsNames(workspaces) { - for (const workspace of workspaces.workspaces) { + for (const workspace of workspaces) { const workspaceIndicator = this.workspaceElement(workspace.uuid)?.indicator; this.updateWorkspaceIndicator(workspace, workspaceIndicator); } @@ -1927,12 +1813,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const kGlobalAnimationDuration = 0.2; this._animatingChange = true; const animations = []; - const workspaces = await this.getWorkspaces(); - const newWorkspaceIndex = workspaces.workspaces.findIndex((w) => w.uuid === newWorkspace.uuid); + const workspaces = this.getWorkspaces(); + const newWorkspaceIndex = workspaces.findIndex((w) => w.uuid === newWorkspace.uuid); const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex; const clonedEssentials = []; if (shouldAnimate && this.shouldAnimateEssentials && previousWorkspace) { - for (const workspace of workspaces.workspaces) { + for (const workspace of workspaces) { const essentialsContainer = this.getEssentialsSection(workspace.containerTabId); if (clonedEssentials[clonedEssentials.length - 1]?.contextId == workspace.containerTabId) { clonedEssentials[clonedEssentials.length - 1].repeat++; @@ -2000,9 +1886,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } const existingTransform = element.style.transform; const elementWorkspaceId = element.id; - const elementWorkspaceIndex = workspaces.workspaces.findIndex( - (w) => w.uuid === elementWorkspaceId - ); + const elementWorkspaceIndex = workspaces.findIndex((w) => w.uuid === elementWorkspaceId); const offset = -(newWorkspaceIndex - elementWorkspaceIndex) * 100; const newTransform = `translateX(${offset}%)`; if (shouldAnimate) { @@ -2039,10 +1923,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Get a list of essentials containers that are in between the first and last workspace const essentialsContainersInBetween = clonedEssentials.filter((cloned) => { const essentialsWorkspaces = cloned.workspaces; - const firstIndex = workspaces.workspaces.findIndex( - (w) => w.uuid === essentialsWorkspaces[0].uuid - ); - const lastIndex = workspaces.workspaces.findIndex( + const firstIndex = workspaces.findIndex((w) => w.uuid === essentialsWorkspaces[0].uuid); + const lastIndex = workspaces.findIndex( (w) => w.uuid === essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid ); @@ -2071,10 +1953,10 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // will slide in from the right // Get the index from first and last workspace - const firstWorkspaceIndex = workspaces.workspaces.findIndex( + const firstWorkspaceIndex = workspaces.findIndex( (w) => w.uuid === essentialsWorkspaces[0].uuid ); - const lastWorkspaceIndex = workspaces.workspaces.findIndex( + const lastWorkspaceIndex = workspaces.findIndex( (w) => w.uuid === essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid ); cloned.originalContainer.style.removeProperty('transform'); @@ -2230,7 +2112,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { tab, currentWorkspace.uuid, currentWorkspace.containerTabId, - await this.getWorkspaces() + this.getWorkspaces() ); } @@ -2265,9 +2147,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return ( !tabContextId || tabContextId === '0' || - !workspaces.workspaces.some( - (workspace) => workspace.containerTabId === parseInt(tabContextId, 10) - ) + !workspaces.some((workspace) => workspace.containerTabId === parseInt(tabContextId, 10)) ); } } @@ -2289,7 +2169,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const lastSelectedTab = this.lastSelectedWorkspaceTabs[workspace.uuid]; const containerId = workspace.containerTabId?.toString(); - const workspaces = await this.getWorkspaces(); + const workspaces = this.getWorkspaces(); // Save current tab as last selected for old workspace if it shouldn't be visible in new workspace if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) { @@ -2349,9 +2229,6 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { gBrowser.tabContainer.arrowScrollbox = this.activeScrollbox; // Update workspace UI - await this._updateWorkspacesChangeContextMenu(); - await this._propagateWorkspaceData({ clearCache: false, onInit }); - gZenThemePicker.onWorkspaceChange(workspace); gZenUIManager.tabsWrapper.scrollbarWidth = 'none'; @@ -2396,15 +2273,19 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { tab.setAttribute('zen-workspace-id', workspace.uuid); } } - window.dispatchEvent( - new CustomEvent('ZenWorkspacesUIUpdate', { - bubbles: true, - detail: { activeIndex: workspace.uuid }, - }) - ); + this.#fireSpaceUIUpdate(); } } + #fireSpaceUIUpdate() { + window.dispatchEvent( + new CustomEvent('ZenWorkspacesUIUpdate', { + bubbles: true, + detail: { activeIndex: this.activeWorkspace }, + }) + ); + } + async _fixCtrlTabBehavior() { ctrlTab.uninit(); ctrlTab.readPref(); @@ -2420,9 +2301,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } } - async _updateWorkspacesChangeContextMenu() { + #updateWorkspacesChangeContextMenu() { if (gZenWorkspaces.privateWindowOrDisabled) return; - const workspaces = await this.getWorkspaces(); + const workspaces = this.getWorkspaces(); const menuPopup = document.getElementById('context-zen-change-workspace-tab-menu-popup'); if (!menuPopup) { @@ -2430,9 +2311,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } menuPopup.innerHTML = ''; - const activeWorkspace = await this.getActiveWorkspace(); + const activeWorkspace = this.getActiveWorkspace(); - for (let workspace of workspaces.workspaces) { + for (let workspace of workspaces) { const menuItem = document.createXULElement('menuitem'); menuItem.setAttribute('label', workspace.name); menuItem.setAttribute('zen-workspace-id', workspace.uuid); @@ -2456,7 +2337,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }; if (moveTabs) { this.#prepareNewWorkspace(workspace); - await this._createWorkspaceTabsSection(workspace, tabs); + await this.#createWorkspaceTabsSection(workspace, tabs); await this._organizeWorkspaceStripLocations(workspace); } return workspace; @@ -2493,7 +2374,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if (!this.currentWindowIsSyncing) { this._tempWorkspace = workspaceData; } else { - await this.saveWorkspace(workspaceData, dontChange); + this.saveWorkspace(workspaceData); } if (!dontChange) { if (beforeChangeCallback) { @@ -2598,9 +2479,9 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { const workspacesIds = []; if (entry.target.closest('#zen-essentials')) { // Get all workspaces that have the same userContextId - const activeWorkspace = await this.getActiveWorkspace(); + const activeWorkspace = this.getActiveWorkspace(); const userContextId = activeWorkspace.containerTabId; - const workspaces = this._workspaceCache.workspaces.filter( + const workspaces = this._workspaceCache.filter( (w) => w.containerTabId === userContextId && w.uuid !== originalWorkspaceId ); workspacesIds.push(...workspaces.map((w) => w.uuid)); @@ -2663,7 +2544,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return; } - let activeWorkspace = await this.getActiveWorkspace(); + let activeWorkspace = this.getActiveWorkspace(); if (!activeWorkspace) { return; } @@ -2702,7 +2583,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } if (!isEssential) { - const activeWorkspace = await this.getActiveWorkspace(); + const activeWorkspace = this.getActiveWorkspace(); if (!activeWorkspace) { return; } @@ -2726,16 +2607,13 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Context menu management async contextChangeContainerTab(event) { this._organizingWorkspaceStrip = true; - let workspaces = await this.getWorkspaces(); - let workspace = workspaces.workspaces.find( + let workspaces = this.getWorkspaces(); + let workspace = workspaces.find( (workspace) => workspace.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace) ); let userContextId = parseInt(event.target.getAttribute('data-usercontextid')); workspace.containerTabId = userContextId + 0; // +0 to convert to number - await this.saveWorkspace(workspace); - await this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache(), true); - await this.updateTabsContainers(); - this.tabContainer._invalidateCachedTabs(); + this.saveWorkspace(workspace); } async closeAllUnpinnedTabs() { @@ -2767,7 +2645,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { }, ]); if (Services.prompt.confirm(null, title, body)) { - await this.removeWorkspace(workspaceId); + this.removeWorkspace(workspaceId); } } @@ -2780,21 +2658,21 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { async changeWorkspaceShortcut(offset = 1, whileScrolling = false) { // Cycle through workspaces - let workspaces = await this.getWorkspaces(); - let activeWorkspace = await this.getActiveWorkspace(); - let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace); + let workspaces = this.getWorkspaces(); + let activeWorkspace = this.getActiveWorkspace(); + let workspaceIndex = workspaces.indexOf(activeWorkspace); // note: offset can be negative let targetIndex = workspaceIndex + offset; if (this.shouldWrapAroundNavigation) { // Add length to handle negative indices and loop - targetIndex = (targetIndex + workspaces.workspaces.length) % workspaces.workspaces.length; + targetIndex = (targetIndex + workspaces.length) % workspaces.length; } else { // Clamp within bounds to disable looping - targetIndex = Math.max(0, Math.min(workspaces.workspaces.length - 1, targetIndex)); + targetIndex = Math.max(0, Math.min(workspaces.length - 1, targetIndex)); } - let nextWorkspace = workspaces.workspaces[targetIndex]; + let nextWorkspace = workspaces[targetIndex]; await this.changeWorkspace(nextWorkspace, { whileScrolling }); } @@ -2844,10 +2722,8 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { this.lastSelectedWorkspaceTabs[workspaceID] = gZenGlanceManager.getTabOrGlanceParent( tabs[tabs.length - 1] ); - const workspaces = await this.getWorkspaces(); - await this.changeWorkspace( - workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID) - ); + const workspaces = this.getWorkspaces(); + await this.changeWorkspace(workspaces.find((workspace) => workspace.uuid === workspaceID)); } // Tab browser utilities @@ -2860,11 +2736,11 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { if ( this.shouldForceContainerTabsToWorkspace && typeof userContextId !== 'undefined' && - this._workspaceCache?.workspaces && + this._workspaceCache && !fromExternal ) { // Find all workspaces that match the given userContextId - const matchingWorkspaces = this._workspaceCache.workspaces.filter( + const matchingWorkspaces = this._workspaceCache.filter( (workspace) => workspace.containerTabId === userContextId ); @@ -2903,12 +2779,12 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } async shortcutSwitchTo(index) { - const workspaces = await this.getWorkspaces(); + const workspaces = this.getWorkspaces(); // The index may be out of bounds, if it doesnt exist, don't do anything - if (index >= workspaces.workspaces.length || index < 0) { + if (index >= workspaces.length || index < 0) { return; } - const workspaceToSwitch = workspaces.workspaces[index]; + const workspaceToSwitch = workspaces[index]; await this.changeWorkspace(workspaceToSwitch); } @@ -2952,7 +2828,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { pinnedContainers = [document.getElementById('pinned-tabs-container')]; normalContainers = [this.activeWorkspaceStrip]; } else { - let workspaces = Array.from(this._workspaceCache?.workspaces || []); + let workspaces = Array.from(this._workspaceCache || []); // Make the active workspace first workspaces = workspaces.sort((a, b) => a.uuid === this.activeWorkspace ? -1 : b.uuid === this.activeWorkspace ? 1 : 0 @@ -2998,7 +2874,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { } const pinnedContainers = []; const normalContainers = []; - for (const workspace of this._workspaceCache.workspaces) { + for (const workspace of this._workspaceCache) { const container = this.workspaceElement(workspace.uuid); if (container) { pinnedContainers.push(container.pinnedTabsContainer); @@ -3096,7 +2972,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { // Find first workspace with the same container const containerTabId = parseInt(tab.parentNode.getAttribute('container')); // +0 to convert to number - workspaceToSwitch = this._workspaceCache.workspaces.find( + workspaceToSwitch = this._workspaceCache.find( (workspace) => workspace.containerTabId + 0 === containerTabId ); } else { @@ -3134,7 +3010,7 @@ class nsZenWorkspaces extends nsZenMultiWindowFeature { return 0; } const activeWorkspace = this.activeWorkspace; - const workspace = workspaces.workspaces.find((workspace) => workspace.uuid === activeWorkspace); + const workspace = workspaces.find((workspace) => workspace.uuid === activeWorkspace); return workspace.containerTabId; } diff --git a/src/zen/workspaces/ZenWorkspacesStorage.mjs b/src/zen/workspaces/ZenWorkspacesStorage.mjs index 43d183d39..63097e240 100644 --- a/src/zen/workspaces/ZenWorkspacesStorage.mjs +++ b/src/zen/workspaces/ZenWorkspacesStorage.mjs @@ -2,437 +2,20 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -window.ZenWorkspacesStorage = { +// Integration of workspace-specific bookmarks into Places +window.ZenWorkspaceBookmarksStorage = { lazy: {}, async init() { ChromeUtils.defineESModuleGetters(this.lazy, { PlacesUtils: 'resource://gre/modules/PlacesUtils.sys.mjs', - Weave: 'resource://services-sync/main.sys.mjs', }); - if (!window.gZenWorkspaces) return; await this._ensureTable(); - await ZenWorkspaceBookmarksStorage.init(); }, async _ensureTable() { await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage._ensureTable', - async (db) => { - // Create the main workspaces table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_workspaces ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - name TEXT NOT NULL, - icon TEXT, - container_id INTEGER, - position INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) - `); - - // Add new columns if they don't exist - // SQLite doesn't have a direct "ADD COLUMN IF NOT EXISTS" syntax, - // so we need to check if the columns exist first - const columns = await db.execute(`PRAGMA table_info(zen_workspaces)`); - const columnNames = columns.map((row) => row.getResultByName('name')); - - // Helper function to add column if it doesn't exist - const addColumnIfNotExists = async (columnName, definition) => { - if (!columnNames.includes(columnName)) { - await db.execute(`ALTER TABLE zen_workspaces ADD COLUMN ${columnName} ${definition}`); - } - }; - - // Add each new column if it doesn't exist - await addColumnIfNotExists('theme_type', 'TEXT'); - await addColumnIfNotExists('theme_colors', 'TEXT'); - await addColumnIfNotExists('theme_opacity', 'REAL'); - await addColumnIfNotExists('theme_rotation', 'INTEGER'); - await addColumnIfNotExists('theme_texture', 'REAL'); - - // Create an index on the uuid column - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_workspaces_uuid ON zen_workspaces(uuid) - `); - - // Create the changes tracking table if it doesn't exist - await db.execute(` - CREATE TABLE IF NOT EXISTS zen_workspaces_changes ( - uuid TEXT PRIMARY KEY, - timestamp INTEGER NOT NULL - ) - `); - - // Create an index on the uuid column for changes tracking table - await db.execute(` - CREATE INDEX IF NOT EXISTS idx_zen_workspaces_changes_uuid ON zen_workspaces_changes(uuid) - `); - - if (!this.lazy.Weave.Service.engineManager.get('workspaces')) { - this.lazy.Weave.Service.engineManager.register(ZenWorkspacesEngine); - await ZenWorkspacesStorage.migrateWorkspacesFromJSON(); - } - - gZenWorkspaces._resolveDBInitialized(); - } - ); - }, - - async migrateWorkspacesFromJSON() { - const oldWorkspacesPath = PathUtils.join( - PathUtils.profileDir, - 'zen-workspaces', - 'Workspaces.json' - ); - if (await IOUtils.exists(oldWorkspacesPath)) { - console.info('ZenWorkspacesStorage: Migrating workspaces from JSON...'); - const oldWorkspaces = await IOUtils.readJSON(oldWorkspacesPath); - if (oldWorkspaces.workspaces) { - for (const workspace of oldWorkspaces.workspaces) { - await this.saveWorkspace(workspace); - } - } - await IOUtils.remove(oldWorkspacesPath); - } - }, - - /** - * Private helper method to notify observers with a list of changed UUIDs. - * @param {string} event - The observer event name. - * @param {Array} uuids - Array of changed workspace UUIDs. - */ - _notifyWorkspacesChanged(event, uuids) { - if (uuids.length === 0) return; // No changes to notify - - // Convert the array of UUIDs to a JSON string - const data = JSON.stringify(uuids); - - Services.obs.notifyObservers(null, event, data); - }, - - async saveWorkspace(workspace, notifyObservers = true) { - const changedUUIDs = new Set(); - - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.saveWorkspace', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - let newPosition; - if ('position' in workspace && Number.isFinite(workspace.position)) { - newPosition = workspace.position; - } else { - // Get the maximum position - const maxPositionResult = await db.execute( - `SELECT MAX("position") as max_position FROM zen_workspaces` - ); - const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; // Add a large increment to avoid frequent reordering - } - - // Insert or replace the workspace - await db.executeCached( - ` - INSERT OR REPLACE INTO zen_workspaces ( - uuid, name, icon, container_id, created_at, updated_at, "position", - theme_type, theme_colors, theme_opacity, theme_rotation, theme_texture - ) VALUES ( - :uuid, :name, :icon, :container_id, - COALESCE((SELECT created_at FROM zen_workspaces WHERE uuid = :uuid), :now), - :now, - :position, - :theme_type, :theme_colors, :theme_opacity, :theme_rotation, :theme_texture - ) - `, - { - uuid: workspace.uuid, - name: workspace.name, - icon: workspace.icon || null, - container_id: workspace.containerTabId || null, - now, - position: newPosition, - theme_type: workspace.theme?.type || null, - theme_colors: workspace.theme ? JSON.stringify(workspace.theme.gradientColors) : null, - theme_opacity: workspace.theme?.opacity || null, - theme_rotation: workspace.theme?.rotation || null, - theme_texture: workspace.theme?.texture || null, - } - ); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: workspace.uuid, - timestamp: Math.floor(now / 1000), - } - ); - - changedUUIDs.add(workspace.uuid); - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - if (notifyObservers) { - this._notifyWorkspacesChanged('zen-workspace-updated', Array.from(changedUUIDs)); - } - }, - - async getWorkspaces() { - const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const rows = await db.executeCached(` - SELECT * FROM zen_workspaces ORDER BY created_at ASC - `); - return rows.map((row) => ({ - uuid: row.getResultByName('uuid'), - name: row.getResultByName('name'), - icon: row.getResultByName('icon'), - containerTabId: row.getResultByName('container_id') ?? 0, - position: row.getResultByName('position'), - theme: row.getResultByName('theme_type') - ? { - type: row.getResultByName('theme_type'), - gradientColors: JSON.parse(row.getResultByName('theme_colors')), - opacity: row.getResultByName('theme_opacity'), - rotation: row.getResultByName('theme_rotation'), - texture: row.getResultByName('theme_texture'), - } - : null, - })); - }, - - async removeWorkspace(uuid, notifyObservers = true) { - const changedUUIDs = [uuid]; - - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.removeWorkspace', - async (db) => { - await db.execute( - ` - DELETE FROM zen_workspaces WHERE uuid = :uuid - `, - { uuid } - ); - - // Record the removal as a change - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - - await this.updateLastChangeTimestamp(db); - } - ); - - if (notifyObservers) { - this._notifyWorkspacesChanged('zen-workspace-removed', changedUUIDs); - } - }, - - async wipeAllWorkspaces() { - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.wipeAllWorkspaces', - async (db) => { - await db.execute(`DELETE FROM zen_workspaces`); - await db.execute(`DELETE FROM zen_workspaces_changes`); - await this.updateLastChangeTimestamp(db); - } - ); - }, - - async markChanged(uuid) { - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.markChanged', - async (db) => { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - ); - }, - - async saveWorkspaceTheme(uuid, theme, notifyObservers = true) { - const changedUUIDs = [uuid]; - await this.lazy.PlacesUtils.withConnectionWrapper('saveWorkspaceTheme', async (db) => { - await db.execute( - ` - UPDATE zen_workspaces - SET - theme_type = :type, - theme_colors = :colors, - theme_opacity = :opacity, - theme_rotation = :rotation, - theme_texture = :texture, - updated_at = :now - WHERE uuid = :uuid - `, - { - type: theme.type, - colors: JSON.stringify(theme.gradientColors), - opacity: theme.opacity, - rotation: theme.rotation, - texture: theme.texture, - now: Date.now(), - uuid, - } - ); - - await this.markChanged(uuid); - await this.updateLastChangeTimestamp(db); - }); - - if (notifyObservers) { - this._notifyWorkspacesChanged('zen-workspace-updated', changedUUIDs); - } - }, - - async getChangedIDs() { - const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const rows = await db.execute(` - SELECT uuid, timestamp FROM zen_workspaces_changes - `); - const changes = {}; - for (const row of rows) { - changes[row.getResultByName('uuid')] = row.getResultByName('timestamp'); - } - return changes; - }, - - async clearChangedIDs() { - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.clearChangedIDs', - async (db) => { - await db.execute(`DELETE FROM zen_workspaces_changes`); - } - ); - }, - - shouldReorderWorkspaces(before, current, after) { - const minGap = 1; // Minimum allowed gap between positions - return ( - (before !== null && current - before < minGap) || (after !== null && after - current < minGap) - ); - }, - - async reorderAllWorkspaces(db, changedUUIDs) { - const workspaces = await db.execute(` - SELECT uuid - FROM zen_workspaces - ORDER BY "position" ASC - `); - - for (let i = 0; i < workspaces.length; i++) { - const newPosition = (i + 1) * 1000; // Use large increments - await db.execute( - ` - UPDATE zen_workspaces - SET "position" = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: workspaces[i].getResultByName('uuid') } - ); - changedUUIDs.add(workspaces[i].getResultByName('uuid')); - } - }, - - async updateLastChangeTimestamp(db) { - const now = Date.now(); - await db.execute( - ` - INSERT OR REPLACE INTO moz_meta (key, value) - VALUES ('zen_workspaces_last_change', :now) - `, - { now } - ); - }, - - async getLastChangeTimestamp() { - const db = await this.lazy.PlacesUtils.promiseDBConnection(); - const result = await db.executeCached(` - SELECT value FROM moz_meta WHERE key = 'zen_workspaces_last_change' - `); - return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0; - }, - - async updateWorkspacePositions(workspaces) { - const changedUUIDs = new Set(); - - await this.lazy.PlacesUtils.withConnectionWrapper( - 'ZenWorkspacesStorage.updateWorkspacePositions', - async (db) => { - await db.executeTransaction(async () => { - const now = Date.now(); - - for (let i = 0; i < workspaces.length; i++) { - const workspace = workspaces[i]; - const newPosition = (i + 1) * 1000; - - await db.execute( - ` - UPDATE zen_workspaces - SET "position" = :newPosition - WHERE uuid = :uuid - `, - { newPosition, uuid: workspace.uuid } - ); - - changedUUIDs.add(workspace.uuid); - - // Record the change - await db.execute( - ` - INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, - { - uuid: workspace.uuid, - timestamp: Math.floor(now / 1000), - } - ); - } - - await this.updateLastChangeTimestamp(db); - }); - } - ); - - this._notifyWorkspacesChanged('zen-workspace-updated', Array.from(changedUUIDs)); - }, -}; - -// Integration of workspace-specific bookmarks into Places -window.ZenWorkspaceBookmarksStorage = { - async init() { - await this._ensureTable(); - }, - - async _ensureTable() { - await ZenWorkspacesStorage.lazy.PlacesUtils.withConnectionWrapper( 'ZenWorkspaceBookmarksStorage.init', async (db) => { // Create table using GUIDs instead of IDs @@ -498,7 +81,7 @@ window.ZenWorkspaceBookmarksStorage = { * @returns {Promise} The timestamp of the last change. */ async getLastChangeTimestamp() { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const result = await db.executeCached(` SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change' `); @@ -506,7 +89,7 @@ window.ZenWorkspaceBookmarksStorage = { }, async getBookmarkWorkspaces(bookmarkGuid) { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute( ` @@ -531,7 +114,7 @@ window.ZenWorkspaceBookmarksStorage = { * } */ async getBookmarkGuidsByWorkspace() { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute(` SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids @@ -554,7 +137,7 @@ window.ZenWorkspaceBookmarksStorage = { * @returns {Promise} An object mapping bookmark+workspace pairs to their change data. */ async getChangedIDs() { - const db = await ZenWorkspacesStorage.lazy.PlacesUtils.promiseDBConnection(); + const db = await this.lazy.PlacesUtils.promiseDBConnection(); const rows = await db.execute(` SELECT bookmark_guid, workspace_uuid, change_type, timestamp FROM zen_bookmarks_workspaces_changes @@ -575,7 +158,7 @@ window.ZenWorkspaceBookmarksStorage = { * Clear all recorded changes. */ async clearChangedIDs() { - await ZenWorkspacesStorage.lazy.PlacesUtils.withConnectionWrapper( + await this.lazy.PlacesUtils.withConnectionWrapper( 'ZenWorkspaceBookmarksStorage.clearChangedIDs', async (db) => { await db.execute(`DELETE FROM zen_bookmarks_workspaces_changes`); @@ -584,4 +167,4 @@ window.ZenWorkspaceBookmarksStorage = { }, }; -ZenWorkspacesStorage.init(); +ZenWorkspaceBookmarksStorage.init(); diff --git a/src/zen/workspaces/ZenWorkspacesSync.mjs b/src/zen/workspaces/ZenWorkspacesSync.mjs deleted file mode 100644 index 6f2633eec..000000000 --- a/src/zen/workspaces/ZenWorkspacesSync.mjs +++ /dev/null @@ -1,459 +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 { Tracker, Store, SyncEngine } = ChromeUtils.importESModule( - 'resource://services-sync/engines.sys.mjs' -); -var { CryptoWrapper } = ChromeUtils.importESModule('resource://services-sync/record.sys.mjs'); -var { Utils } = ChromeUtils.importESModule('resource://services-sync/util.sys.mjs'); -var { SCORE_INCREMENT_XLARGE } = ChromeUtils.importESModule( - 'resource://services-sync/constants.sys.mjs' -); - -// Define ZenWorkspaceRecord -function ZenWorkspaceRecord(collection, id) { - CryptoWrapper.call(this, collection, id); -} - -ZenWorkspaceRecord.prototype = Object.create(CryptoWrapper.prototype); -ZenWorkspaceRecord.prototype.constructor = ZenWorkspaceRecord; - -ZenWorkspaceRecord.prototype._logName = 'Sync.Record.ZenWorkspace'; - -Utils.deferGetSet(ZenWorkspaceRecord, 'cleartext', [ - 'name', - 'icon', - 'default', - 'containerTabId', - 'position', - 'theme_type', - 'theme_colors', - 'theme_opacity', - 'theme_rotation', - 'theme_texture', -]); - -// Define ZenWorkspacesStore -function ZenWorkspacesStore(name, engine) { - Store.call(this, name, engine); -} - -ZenWorkspacesStore.prototype = Object.create(Store.prototype); -ZenWorkspacesStore.prototype.constructor = ZenWorkspacesStore; - -/** - * Initializes the store by loading the current changeset. - */ -ZenWorkspacesStore.prototype.initialize = async function () { - await Store.prototype.initialize.call(this); - // Additional initialization if needed -}; - -/** - * Retrieves all workspace IDs from the storage. - * @returns {Object} An object mapping workspace UUIDs to true. - */ -ZenWorkspacesStore.prototype.getAllIDs = async function () { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - const ids = {}; - for (const workspace of workspaces) { - ids[workspace.uuid] = true; - } - return ids; - } catch (error) { - this._log.error('Error fetching all workspace IDs', error); - throw error; - } -}; - -/** - * Handles changing the ID of a workspace. - * @param {String} oldID - The old UUID. - * @param {String} newID - The new UUID. - */ -ZenWorkspacesStore.prototype.changeItemID = async function (oldID, newID) { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - const workspace = workspaces.find((ws) => ws.uuid === oldID); - if (workspace) { - workspace.uuid = newID; - await ZenWorkspacesStorage.saveWorkspace(workspace, false); - // Mark the new ID as changed for sync - await ZenWorkspacesStorage.markChanged(newID); - } - } catch (error) { - this._log.error(`Error changing workspace ID from ${oldID} to ${newID}`, error); - throw error; - } -}; - -/** - * Checks if a workspace exists. - * @param {String} id - The UUID of the workspace. - * @returns {Boolean} True if the workspace exists, false otherwise. - */ -ZenWorkspacesStore.prototype.itemExists = async function (id) { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - return workspaces.some((ws) => ws.uuid === id); - } catch (error) { - this._log.error(`Error checking if workspace exists with ID ${id}`, error); - throw error; - } -}; - -/** - * Creates a record for a workspace. - * @param {String} id - The UUID of the workspace. - * @param {String} collection - The collection name. - * @returns {ZenWorkspaceRecord} The workspace record. - */ -ZenWorkspacesStore.prototype.createRecord = async function (id, collection) { - try { - const workspaces = await ZenWorkspacesStorage.getWorkspaces(); - const workspace = workspaces.find((ws) => ws.uuid === id); - const record = new ZenWorkspaceRecord(collection, id); - - if (workspace) { - record.name = workspace.name; - record.icon = workspace.icon; - record.default = workspace.default; - record.containerTabId = workspace.containerTabId; - record.position = workspace.position; - if (workspace.theme) { - record.theme_type = workspace.theme.type; - record.theme_colors = JSON.stringify(workspace.theme.gradientColors); - record.theme_opacity = workspace.theme.opacity; - record.theme_rotation = workspace.theme.rotation; - record.theme_texture = workspace.theme.texture; - } - record.deleted = false; - } else { - record.deleted = true; - } - - return record; - } catch (error) { - this._log.error(`Error creating record for workspace ID ${id}`, error); - throw error; - } -}; - -/** - * Creates a new workspace. - * @param {ZenWorkspaceRecord} record - The workspace record to create. - */ -ZenWorkspacesStore.prototype.create = async function (record) { - try { - this._validateRecord(record); - const workspace = { - uuid: record.id, - name: record.name, - icon: record.icon, - default: record.default, - containerTabId: record.containerTabId, - position: record.position, - theme: record.theme_type - ? { - type: record.theme_type, - gradientColors: JSON.parse(record.theme_colors), - opacity: record.theme_opacity, - rotation: record.theme_rotation, - texture: record.theme_texture, - } - : null, - }; - await ZenWorkspacesStorage.saveWorkspace(workspace, false); - } catch (error) { - this._log.error(`Error creating workspace with ID ${record.id}`, error); - throw error; - } -}; - -/** - * Updates an existing workspace. - * @param {ZenWorkspaceRecord} record - The workspace record to update. - */ -ZenWorkspacesStore.prototype.update = async function (record) { - try { - this._validateRecord(record); - await this.create(record); // Reuse create for update - } catch (error) { - this._log.error(`Error updating workspace with ID ${record.id}`, error); - throw error; - } -}; - -/** - * Removes a workspace. - * @param {ZenWorkspaceRecord} record - The workspace record to remove. - */ -ZenWorkspacesStore.prototype.remove = async function (record) { - try { - await ZenWorkspacesStorage.removeWorkspace(record.id, false); - } catch (error) { - this._log.error(`Error removing workspace with ID ${record.id}`, error); - throw error; - } -}; - -/** - * Wipes all workspaces from the storage. - */ -ZenWorkspacesStore.prototype.wipe = async function () { - try { - await ZenWorkspacesStorage.wipeAllWorkspaces(); - } catch (error) { - this._log.error('Error wiping all workspaces', error); - throw error; - } -}; - -/** - * Validates a workspace record. - * @param {ZenWorkspaceRecord} record - The workspace record to validate. - */ -ZenWorkspacesStore.prototype._validateRecord = function (record) { - if (!record.id || typeof record.id !== 'string') { - throw new Error('Invalid workspace ID'); - } - if (!record.name || typeof record.name !== 'string') { - throw new Error(`Invalid workspace name for ID ${record.id}`); - } - if (typeof record.default !== 'boolean') { - record.default = false; - } - if (record.icon != null && typeof record.icon !== 'string') { - throw new Error(`Invalid icon for workspace ID ${record.id}`); - } - if (record.containerTabId != null && typeof record.containerTabId !== 'number') { - throw new Error(`Invalid containerTabId for workspace ID ${record.id}`); - } - if (record.position != null && typeof record.position !== 'number') { - throw new Error(`Invalid position for workspace ID ${record.id}`); - } - - // Validate theme properties if they exist - if (record.theme_type) { - if (typeof record.theme_type !== 'string') { - throw new Error(`Invalid theme_type for workspace ID ${record.id}`); - } - if (!record.theme_colors || typeof record.theme_colors !== 'string') { - throw new Error(`Invalid theme_colors for workspace ID ${record.id}`); - } - try { - JSON.parse(record.theme_colors); - } catch (e) { - throw new Error( - `Invalid theme_colors JSON for workspace ID ${record.id}. Error: ${e.message}` - ); - } - if (record.theme_opacity != null && typeof record.theme_opacity !== 'number') { - throw new Error(`Invalid theme_opacity for workspace ID ${record.id}`); - } - if (record.theme_rotation != null && typeof record.theme_rotation !== 'number') { - throw new Error(`Invalid theme_rotation for workspace ID ${record.id}`); - } - if (record.theme_texture != null && typeof record.theme_texture !== 'number') { - throw new Error(`Invalid theme_texture for workspace ID ${record.id}`); - } - } -}; - -/** - * Retrieves changed workspace IDs since the last sync. - * @returns {Object} An object mapping workspace UUIDs to their change timestamps. - */ -ZenWorkspacesStore.prototype.getChangedIDs = async function () { - try { - return await ZenWorkspacesStorage.getChangedIDs(); - } catch (error) { - this._log.error('Error retrieving changed IDs from storage', error); - throw error; - } -}; - -/** - * Clears all recorded changes after a successful sync. - */ -ZenWorkspacesStore.prototype.clearChangedIDs = async function () { - try { - await ZenWorkspacesStorage.clearChangedIDs(); - } catch (error) { - this._log.error('Error clearing changed IDs in storage', error); - throw error; - } -}; - -/** - * Marks a workspace as changed. - * @param {String} uuid - The UUID of the workspace that changed. - */ -ZenWorkspacesStore.prototype.markChanged = async function (uuid) { - try { - await ZenWorkspacesStorage.markChanged(uuid); - } catch (error) { - this._log.error(`Error marking workspace ${uuid} as changed`, error); - throw error; - } -}; - -/** - * Finalizes the store by ensuring all pending operations are completed. - */ -ZenWorkspacesStore.prototype.finalize = async function () { - await Store.prototype.finalize.call(this); -}; - -// Define ZenWorkspacesTracker -function ZenWorkspacesTracker(name, engine) { - Tracker.call(this, name, engine); - this._ignoreAll = false; - - // Observe profile-before-change to stop the tracker gracefully - Services.obs.addObserver(this.asyncObserver, 'profile-before-change'); -} - -ZenWorkspacesTracker.prototype = Object.create(Tracker.prototype); -ZenWorkspacesTracker.prototype.constructor = ZenWorkspacesTracker; - -/** - * Retrieves changed workspace IDs by delegating to the store. - * @returns {Object} An object mapping workspace UUIDs to their change timestamps. - */ -ZenWorkspacesTracker.prototype.getChangedIDs = async function () { - try { - return await this.engine._store.getChangedIDs(); - } catch (error) { - this._log.error('Error retrieving changed IDs from store', error); - throw error; - } -}; - -/** - * Clears all recorded changes after a successful sync. - */ -ZenWorkspacesTracker.prototype.clearChangedIDs = async function () { - try { - await this.engine._store.clearChangedIDs(); - } catch (error) { - this._log.error('Error clearing changed IDs in store', error); - throw error; - } -}; - -/** - * Called when the tracker starts. Registers observers to listen for workspace changes. - */ -ZenWorkspacesTracker.prototype.onStart = function () { - if (this._started) { - return; - } - this._log.trace('Starting tracker'); - // Register observers for workspace changes - Services.obs.addObserver(this.asyncObserver, 'zen-workspace-added'); - Services.obs.addObserver(this.asyncObserver, 'zen-workspace-removed'); - Services.obs.addObserver(this.asyncObserver, 'zen-workspace-updated'); - this._started = true; -}; - -/** - * Called when the tracker stops. Unregisters observers. - */ -ZenWorkspacesTracker.prototype.onStop = function () { - if (!this._started) { - return; - } - this._log.trace('Stopping tracker'); - // Unregister observers for workspace changes - Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-added'); - Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-removed'); - Services.obs.removeObserver(this.asyncObserver, 'zen-workspace-updated'); - this._started = false; -}; - -/** - * Handles observed events and marks workspaces as changed accordingly. - * @param {nsISupports} subject - The subject of the notification. - * @param {String} topic - The topic of the notification. - * @param {String} data - Additional data (JSON stringified array of UUIDs). - */ -ZenWorkspacesTracker.prototype.observe = async function (subject, topic, data) { - if (this.ignoreAll) { - return; - } - - try { - switch (topic) { - case 'profile-before-change': - await this.stop(); - break; - case 'zen-workspace-removed': - case 'zen-workspace-updated': - case 'zen-workspace-added': { - let workspaceIDs; - if (data) { - try { - workspaceIDs = JSON.parse(data); - if (!Array.isArray(workspaceIDs)) { - throw new Error('Parsed data is not an array'); - } - } catch (parseError) { - this._log.error(`Failed to parse workspace UUIDs from data: ${data}`, parseError); - return; - } - } else { - this._log.error(`No data received for event ${topic}`); - return; - } - - this._log.trace(`Observed ${topic} for UUIDs: ${workspaceIDs.join(', ')}`); - - // Process each UUID - for (const workspaceID of workspaceIDs) { - if (typeof workspaceID === 'string') { - // Inform the store about the change - await this.engine._store.markChanged(workspaceID); - } else { - this._log.warn(`Invalid workspace ID encountered: ${workspaceID}`); - } - } - - // Bump the score once after processing all changes - if (workspaceIDs.length > 0) { - this.score += SCORE_INCREMENT_XLARGE; - } - break; - } - } - } catch (error) { - this._log.error(`Error handling ${topic} in observe method`, error); - } -}; - -/** - * Finalizes the tracker by ensuring all pending operations are completed. - */ -ZenWorkspacesTracker.prototype.finalize = async function () { - await Tracker.prototype.finalize.call(this); -}; - -// Define ZenWorkspacesEngine -function ZenWorkspacesEngine(service) { - SyncEngine.call(this, 'Workspaces', service); -} - -ZenWorkspacesEngine.prototype = Object.create(SyncEngine.prototype); -ZenWorkspacesEngine.prototype.constructor = ZenWorkspacesEngine; - -ZenWorkspacesEngine.prototype._storeObj = ZenWorkspacesStore; -ZenWorkspacesEngine.prototype._trackerObj = ZenWorkspacesTracker; -ZenWorkspacesEngine.prototype._recordObj = ZenWorkspaceRecord; -ZenWorkspacesEngine.prototype.version = 2; - -ZenWorkspacesEngine.prototype.syncPriority = 10; -ZenWorkspacesEngine.prototype.allowSkippedRecord = false; - -Object.setPrototypeOf(ZenWorkspacesEngine.prototype, SyncEngine.prototype); diff --git a/src/zen/workspaces/jar.inc.mn b/src/zen/workspaces/jar.inc.mn index 1e4fd273f..4ab11e7ab 100644 --- a/src/zen/workspaces/jar.inc.mn +++ b/src/zen/workspaces/jar.inc.mn @@ -7,7 +7,6 @@ content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.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) content/browser/zen-components/ZenGradientGenerator.mjs (../../zen/workspaces/ZenGradientGenerator.mjs) * content/browser/zen-styles/zen-workspaces.css (../../zen/workspaces/zen-workspaces.css) content/browser/zen-styles/zen-gradient-generator.css (../../zen/workspaces/zen-gradient-generator.css) \ No newline at end of file diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js index 06e07eaa4..30374b9ff 100644 --- a/src/zen/zen.globals.js +++ b/src/zen/zen.globals.js @@ -20,7 +20,6 @@ export default [ 'gZenWorkspaces', 'gZenKeyboardShortcutsManager', 'ZenWorkspacesEngine', - 'ZenWorkspacesStorage', 'ZenWorkspaceBookmarksStorage', 'ZEN_KEYSET_ID',