Compare commits

...

10 Commits

31 changed files with 947 additions and 269 deletions

View File

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

View File

@@ -1 +1 @@
3985a970489e32fc751e1b80abf4d5534c905e45
6bd10168f81fb90f878132d87cf681200afbcd91

View File

@@ -24,3 +24,5 @@ files:
translation: browser/browser/zen-boosts.ftl
- source: en-US/browser/browser/zen-space-routing.ftl
translation: browser/browser/zen-space-routing.ftl
- source: en-US/browser/browser/zen-command-palette.ftl
translation: browser/browser/zen-command-palette.ftl

View File

@@ -0,0 +1,36 @@
# 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/.
zen-action-toggle-compact-mode = Toggle Compact Mode
zen-action-open-theme-picker = Open Theme Picker
zen-action-new-split-view = New Split View
zen-action-new-folder = New Folder
zen-action-copy-current-url = Copy Current URL
zen-action-settings = Settings
zen-action-open-private-window = Open Private Window
zen-action-open-new-window = Open New Window
zen-action-new-blank-window = New Blank Window
zen-action-pin-tab = Pin Tab
zen-action-unpin-tab = Unpin Tab
zen-action-open-space-routing = Open Space Routing
zen-action-new-boost = New Boost
zen-action-next-space = Next Space
zen-action-previous-space = Previous Space
zen-action-close-tab = Close Tab
zen-action-reload-tab = Reload Tab
zen-action-reload-tab-without-cache = Reload Tab Without Cache
zen-action-next-tab = Next Tab
zen-action-previous-tab = Previous Tab
zen-action-capture-screenshot = Capture Screenshot
zen-action-toggle-tabs-on-right = Toggle Tabs on right
zen-action-add-to-essentials = Add to Essentials
zen-action-remove-from-essentials = Remove from Essentials
zen-action-find-in-page = Find in Page
zen-action-manage-extensions = Manage Extensions
zen-action-switch-to-automatic-appearance = Switch to Automatic Appearance
zen-action-switch-to-light-mode = Switch to Light Mode
zen-action-switch-to-dark-mode = Switch to Dark Mode
zen-action-print = Print
zen-action-focus-on = Focus on
zen-action-extension = Extension

View File

@@ -87,6 +87,8 @@ zen-icons-picker-emoji =
.label = Emojis
zen-icons-picker-svg =
.label = Icons
zen-emojis-picker-search =
.placeholder = Search emojis
urlbar-search-mode-zen_actions = Actions
zen-site-data-settings = Settings

12
prefs/zen/sync.yaml Normal file
View File

@@ -0,0 +1,12 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: services.sync.engine.spaces
value: true
condition: "@IS_TWILIGHT@"
- name: services.sync.engine.spaces
value: false
locked: true
condition: "!@IS_TWILIGHT@"

View File

@@ -32,9 +32,6 @@
- name: zen.workspaces.scroll-modifier-key
value: ctrl
- name: services.sync.engine.workspaces
value: false
- name: zen.workspaces.separate-essentials
value: true

View File

@@ -14,7 +14,7 @@
<hbox id="PanelUI-zen-emojis-picker-pages">
<vbox emojis="true">
<hbox id="PanelUI-zen-emojis-picker-header">
<html:input type="search" id="PanelUI-zen-emojis-picker-search" placeholder="Search emojis" />
<html:input type="search" id="PanelUI-zen-emojis-picker-search" data-l10n-id="zen-emojis-picker-search" />
</hbox>
<hbox id="PanelUI-zen-emojis-picker-list" />
</vbox>

View File

@@ -0,0 +1,20 @@
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
index b503987a08e2ce64bfc01c4e3a21e5e19ef834f0..b1c03a0b219fd3eddd93c1deaeb18ab79c85f168 100644
--- a/browser/components/preferences/config/account-sync.mjs
+++ b/browser/components/preferences/config/account-sync.mjs
@@ -42,6 +42,7 @@ Preferences.addAll([
{ id: "services.sync.engine.creditcards", type: "bool" },
{ id: "services.sync.engine.addons", type: "bool" },
{ id: "services.sync.engine.prefs", type: "bool" },
+ { id: "services.sync.engine.spaces", type: "bool" },
]);
/**
@@ -546,6 +547,7 @@ const SYNC_ENGINE_SETTINGS = [
type: "payments",
},
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
+ { id: "syncSpaces", pref: "services.sync.engine.spaces", type: "workspaces" },
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
];

View File

@@ -6,7 +6,7 @@ index 64aa0d98a0622c01f3dcfff1a04bfcda368354d2..2013e04b0881ad2295d6897b91e1573c
{ id: "services.sync.engine.passwords", type: "bool" },
{ id: "services.sync.engine.addresses", type: "bool" },
{ id: "services.sync.engine.creditcards", type: "bool" },
+ { id: "services.sync.engine.workspaces", type: "bool" },
+ { id: "services.sync.engine.spaces", type: "bool" },
]);
let gSyncChooseWhatToSync = {

View File

@@ -20,7 +20,7 @@ index a893c5ec3d007820d98f5d92dd039640faa2c181..9cbd00102e44ccf98b37845474d92d57
+ <html:div class="sync-engine-workspaces">
+ <checkbox
+ data-l10n-id="sync-engine-workspaces"
+ preference="services.sync.engine.workspaces"
+ preference="services.sync.engine.spaces"
+ />
+ </html:div>
</html:div>

View File

@@ -6,7 +6,7 @@ index c379e1a5f82692406a92d9fcd3bca2769dfac5b2..af037dd3d995813d966524ac44a3795d
<image class="sync-engine-image sync-engine-prefs" alt=""/>
<label data-l10n-id="sync-currently-syncing-settings"/>
</html:div>
+ <html:div engine_preference="services.sync.engine.workspaces">
+ <html:div engine_preference="services.sync.engine.spaces">
+ <image class="sync-engine-image sync-engine-workspaces" alt=""/>
+ <label data-l10n-id="sync-currently-syncing-workspaces"/>
+ </html:div>

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index 08b5b56e069d038d72c87355920c4ce8a55ed805..209c0ddc0297adb8340180c58de07ae107790751 100644
index 08b5b56e069d038d72c87355920c4ce8a55ed805..e1a3a18081c7a8153f8d12c187bf4dd522944fcb 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -511,6 +511,7 @@
@@ -308,7 +308,7 @@ index 08b5b56e069d038d72c87355920c4ce8a55ed805..209c0ddc0297adb8340180c58de07ae1
+
+ let hasZenDefaultUserContextId = false;
+ let zenForcedWorkspaceId = undefined;
+ if (beforeRouteResult.isRouteFound && typeof userContextId !== "undefined") {
+ if (beforeRouteResult.isRouteFound && typeof userContextId === "undefined") {
+ userContextId = beforeRouteResult.userContextId;
+ hasZenDefaultUserContextId = true;
+ } else if (typeof gZenWorkspaces !== "undefined" && !_forZenEmptyTab) {

View File

@@ -484,11 +484,10 @@ groupbox h2 {
#helpButton,
#support-firefox,
#tabGroupSuggestions,
#web-appearance-manage-themes-link,
#setting-control-sidebarChatbotFieldset,
#setting-control-supportFirefox,
.mission-message,
html|setting-group:is([data-subcategory="layout"], [groupid="support"]) {
html|setting-group:is([data-subcategory="layout"], [groupid="support"], [groupid="browserTheme"]) {
display: none !important;
}

View File

@@ -0,0 +1,15 @@
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f06a4f8dbf 100644
--- a/services/sync/modules/service.sys.mjs
+++ b/services/sync/modules/service.sys.mjs
@@ -99,6 +99,10 @@ function getEngineModules() {
whenTrue: "ExtensionStorageEngineKinto",
whenFalse: "ExtensionStorageEngineBridge",
};
+ result.Spaces = {
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
+ symbol: "ZenWorkspacesEngine",
+ };
return result;
}

View File

@@ -0,0 +1,37 @@
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
index 5702ff28cc22206f5ce16584dac8a78d816562ce..3d08ecc97ce5995b30d8a4af0c33df329b428008 100644
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
@@ -270,11 +270,29 @@ _ContextualIdentityService.prototype = {
});
},
- create(name, icon, color) {
+ create(name, icon, color, id = null) {
this.ensureDataReady();
- // Retrieve the next userContextId available.
- let userContextId = ++this._lastUserContextId;
+ if (id !== null) {
+ id = typeof id == "string" ? Number(id) : id;
+ if (!Number.isSafeInteger(id) || id <= 0) {
+ throw new Error(`Invalid userContextId '${id}'`);
+ }
+ }
+
+ let userContextId;
+ if (
+ id !== null &&
+ !this._identities.some(identity => identity.userContextId === id)
+ ) {
+ userContextId = id;
+ this._lastUserContextId = Math.max(
+ this._lastUserContextId,
+ userContextId
+ );
+ } else {
+ userContextId = ++this._lastUserContextId;
+ }
// Throw an error if the next userContextId available is invalid (the one associated to
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which

View File

@@ -289,6 +289,9 @@ export class nsZenFolder extends MozTabbrowserTabGroup {
addTabs(tabs) {
let tabsFromOutside = [];
for (let tab of tabs) {
if (tab.hasAttribute("zen-essential")) {
gZenPinnedTabManager.removeEssentials(tab, false);
}
if (tab.group !== this) {
tabsFromOutside.push(tab);
}

View File

@@ -622,15 +622,13 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
}
createFolder(tabs = [], options = {}) {
const filteredTabs = tabs
.filter(tab => !tab.hasAttribute("zen-essential"))
.map(tab => {
gBrowser.pinTab(tab);
if (tab?.group?.hasAttribute("split-view-group")) {
tab = tab.group;
}
return tab;
});
const filteredTabs = tabs.map(tab => {
gBrowser.pinTab(tab);
if (tab?.group?.hasAttribute("split-view-group")) {
tab = tab.group;
}
return tab;
});
const workspacePinned = gZenWorkspaces.workspaceElement(
options.workspaceId

View File

@@ -1551,6 +1551,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
return (
owner &&
owner.pinned &&
!owner.hasAttribute("glance-id") &&
this._lazyPref.SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE &&
owner.linkedBrowser?.browsingContext?.isAppTab &&
this.tabDomainsDiffer(owner, uri) &&

View File

@@ -20,4 +20,5 @@ DIRS += [
"share",
"spaces",
"space-routing",
"sync",
]

View File

@@ -10,6 +10,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenLiveFoldersManager:
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
@@ -609,6 +610,7 @@ export class nsZenSessionManager {
}
);
this.#collectWindowData(windows);
lazy.ZenSyncStore.notifyAboutChanges();
// This would save the data to disk asynchronously or when quitting the app.
let sidebar = this.#sidebarWithoutCloning;
this.#file.data = sidebar;
@@ -622,6 +624,15 @@ export class nsZenSessionManager {
this.log(`Saving Zen session data with ${sidebar.tabs?.length || 0} tabs`);
}
/**
* Returns the sidebar data object.
*
* @returns {object}
*/
getSidebarData() {
return this.#sidebarWithoutCloning;
}
/**
* Called when the last known backup should be deleted and a new one
* created. This uses the #deferredBackupTask to debounce clusters of

View File

@@ -192,7 +192,7 @@ class nsZenWorkspaceCreation extends MozXULElement {
this.currentProfile = {
id: 0,
name: "Default",
name: ContextualIdentityService.formatContextLabel("user-context-none"),
};
} else {
this.inputProfile.parentNode.hidden = true;

View File

@@ -11,6 +11,7 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
@@ -148,7 +149,7 @@ class nsZenWorkspaces {
if (!this.privateWindowOrDisabled) {
const observerFunction = async () => {
delete this._workspaceBookmarksCache;
await this.workspaceBookmarks();
await this.#initializeWorkspaceBookmarks();
this._invalidateBookmarkContainers();
};
Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
@@ -168,6 +169,63 @@ class nsZenWorkspaces {
}
}
/**
* Applies live sync changes: updates workspace cache, removes deleted items,
* then creates/updates pulled items.
*
* @param {{ spaces: Array}} pulled Reconcile-pulled items.
* @param {{ spaces: Array}} removals Items to remove.
*/
async _applySyncChanges(pulled, removals = {}) {
if (!this.shouldHaveWorkspaces || this.privateWindowOrDisabled) {
return;
}
await this.promiseInitialized;
// 1. Update workspace cache (remove deleted, merge pulled)
const removedSpaceIds = new Set((removals.spaces || []).map(s => s.uuid));
if (removedSpaceIds.size || pulled.spaces?.length) {
const localMap = new Map(
this.getWorkspaces()
.filter(w => !removedSpaceIds.has(w.uuid))
.map(w => [w.uuid, w])
);
for (const space of pulled.spaces || []) {
if (!space?.uuid) {
continue;
}
const existing = localMap.get(space.uuid);
localMap.set(space.uuid, existing ? { ...existing, ...space } : space);
}
await this.propagateWorkspaces(
this.#getOrderedWorkspacesByPosition(Array.from(localMap.values()))
);
this.#propagateWorkspaceData();
}
}
#getOrderedWorkspacesByPosition(workspaces) {
return [...workspaces]
.map((workspace, index) => ({ workspace, index }))
.sort((a, b) => {
const aPosition =
typeof a.workspace.position === "number"
? a.workspace.position
: a.index;
const bPosition =
typeof b.workspace.position === "number"
? b.workspace.position
: b.index;
return aPosition - bPosition || a.index - b.index;
})
.map(({ workspace }) => {
// strip the position property that comes from pulled workspaces
const rest = { ...workspace };
delete rest.position;
return rest;
});
}
#afterLoadInit() {
const onResize = (...args) => {
requestAnimationFrame(() => {
@@ -391,7 +449,7 @@ class nsZenWorkspaces {
container = 0;
}
let essentialsContainer = document.querySelector(
`.zen-essentials-container[container="${container}"]:not([cloned])`
`.zen-essentials-container[container="${container}"]`
);
if (!essentialsContainer) {
essentialsContainer = document.createXULElement("hbox");
@@ -691,17 +749,17 @@ class nsZenWorkspaces {
return spacesForSS;
}
async workspaceBookmarks() {
async #initializeWorkspaceBookmarks() {
if (this.privateWindowOrDisabled) {
this._workspaceBookmarksCache = {
bookmarks: [],
lastChangeTimestamp: 0,
};
return this._workspaceBookmarksCache;
return;
}
if (this._workspaceBookmarksCache) {
return this._workspaceBookmarksCache;
return;
}
const [bookmarks, lastChangeTimestamp] = await Promise.all([
@@ -710,8 +768,6 @@ class nsZenWorkspaces {
]);
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
return this._workspaceCache;
}
restoreWorkspacesFromSessionStore(aWinData = {}) {
@@ -771,7 +827,7 @@ class nsZenWorkspaces {
return (async () => {
await this.#waitForPromises();
this.#afterLoadInit();
await this.workspaceBookmarks();
await this.#initializeWorkspaceBookmarks();
await this.changeWorkspace(activeWorkspace, { onInit: true });
this.#fixTabPositions();
this.onWindowResize();
@@ -797,6 +853,13 @@ class nsZenWorkspaces {
})();
}
#markWorkspaceChanged(workspaceId) {
lazy.ZenSyncStore.markItemChanged({
type: "space",
id: workspaceId,
});
}
async selectStartPage() {
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
return;
@@ -1214,12 +1277,19 @@ class nsZenWorkspaces {
} else {
workspacesData.push(workspaceData);
}
// mark item as changed for sync
this.#markWorkspaceChanged(workspaceData.uuid);
this.#propagateWorkspaceData();
}
removeWorkspace(windowID) {
let { promise, resolve } = Promise.withResolvers();
this.#deleteWorkspaceOwnedTabs(windowID);
// mark item as changed for sync
this.#markWorkspaceChanged(windowID);
let workspacesData = this.getWorkspaces();
// Remove the workspace from the cache
workspacesData = workspacesData.filter(
@@ -1351,30 +1421,50 @@ class nsZenWorkspaces {
if (this.privateWindowOrDisabled) {
return;
}
const workspaces = this._workspaceCache;
const workspaces = this.getWorkspaces();
// Track previous positions so we only notify observers for workspaces whose
// position changed during the reorder.
const previousPositions = new Map(
workspaces.map((workspace, index) => [workspace.uuid, index])
);
const workspace = workspaces.find(w => w.uuid === id);
if (!workspace) {
console.warn(`Workspace with ID ${id} not found for reordering.`);
return;
}
// Remove the workspace from its current position
const currentIndex = workspaces.indexOf(workspace);
if (currentIndex === -1) {
console.warn(`Workspace with ID ${id} not found in the list.`);
return;
}
workspaces.splice(currentIndex, 1);
// Insert the workspace at the new position
if (newPosition < 0 || newPosition > workspaces.length) {
if (newPosition < 0 || newPosition >= workspaces.length) {
console.warn(
`Invalid position ${newPosition} for reordering workspace with ID ${id}.`
);
return;
}
workspaces.splice(currentIndex, 1);
workspaces.splice(newPosition, 0, workspace);
// Propagate the changes if the order has changed
if (currentIndex !== newPosition) {
this.#propagateWorkspaceData();
this._workspaceCache = workspaces;
for (const [i, ws] of workspaces.entries()) {
if (previousPositions.get(ws.uuid) === i) {
continue;
}
// mark item as changed for sync
this.#markWorkspaceChanged(ws.uuid);
}
this.#propagateWorkspaceData(workspaces);
}
}
@@ -1774,6 +1864,11 @@ class nsZenWorkspaces {
}
const newTransform = diff * 100;
element.style.transform = `translateX(${newTransform + offsetPixels / 2}%)`;
// A special case for two spaces
if (spaceLen === 2 && diff !== 0) {
const side = offsetPixels > 0 ? -100 : 100;
element.style.transform = `translateX(${side + offsetPixels / 2}%)`;
}
}
// Hide other essentials with different containerTabId
for (const container of otherContainersEssentials) {
@@ -1927,28 +2022,20 @@ class nsZenWorkspaces {
diff += spaceLen;
}
const isGoingLeft = diff < 0;
const clonedEssentials = [];
const essentialsAnimData = [];
if (shouldAnimate && this.shouldAnimateEssentials && previousWorkspace) {
const containerIds = new Map();
for (const workspace of workspaces) {
const essentialsContainer = this.getEssentialsSection(
workspace.containerTabId
);
let lastCloned = clonedEssentials[clonedEssentials.length - 1];
if (lastCloned && lastCloned.contextId == workspace.containerTabId) {
lastCloned.repeat++;
lastCloned.workspaces.push(workspace);
continue;
const containerId = workspace.containerTabId;
if (!containerIds.has(containerId)) {
containerIds.set(containerId, []);
}
essentialsContainer.setAttribute("hidden", "true");
const essentialsClone = essentialsContainer.cloneNode(true);
essentialsClone.removeAttribute("hidden");
essentialsClone.setAttribute("cloned", "true");
clonedEssentials.push({
container: essentialsClone,
workspaces: [workspace],
contextId: workspace.containerTabId,
originalContainer: essentialsContainer,
repeat: 0,
containerIds.get(containerId).push(workspace);
}
for (const [containerId, spaces] of containerIds) {
essentialsAnimData.push({
element: this.getEssentialsSection(containerId),
workspaces: spaces,
});
}
}
@@ -2012,6 +2099,12 @@ class nsZenWorkspaces {
offset += spaceLen;
}
offset = offset * 100;
// A special case for two spaces
if (spaceLen === 2 && offset !== 0) {
const currentTransform =
parseFloat(element.style.transform.split("(")[1]) || 0;
offset = currentTransform >= 0 ? 100 : -100;
}
const newTransform = `translateX(${offset}%)`;
// Only animate the workspace that is coming in, to avoid having multiple workspaces
// animating off-screen at the same time which can cause performance issues. With an off
@@ -2055,190 +2148,59 @@ class nsZenWorkspaces {
}
}
if (this.shouldAnimateEssentials && previousWorkspace) {
// Animate essentials
const newWorkspaceEssentialsContainer = clonedEssentials.find(cloned =>
cloned.workspaces.some(w => w.uuid === newWorkspace.uuid)
);
// 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.findIndex(
w => w.uuid === essentialsWorkspaces[0].uuid
for (const data of essentialsAnimData) {
const container = data.element;
const essentialsWorkspaces = data.workspaces;
const containsPrev = essentialsWorkspaces.some(
w => w.uuid === previousWorkspace.uuid
);
const lastIndex = workspaces.findIndex(
w =>
w.uuid ===
essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
const containsNew = essentialsWorkspaces.some(
w => w.uuid === newWorkspace.uuid
);
const [start, end] = [
Math.min(previousWorkspaceIndex, newWorkspaceIndex),
Math.max(previousWorkspaceIndex, newWorkspaceIndex),
];
// Check if any part of the container overlaps with the movement range
return firstIndex <= end && lastIndex >= start;
});
for (const cloned of clonedEssentials) {
const container = cloned.container;
const essentialsWorkspaces = cloned.workspaces;
const repeats = cloned.repeat;
// Animate like the workspaces above expect essentials are a bit more
// complicated because they are not based on workspaces but on containers
// So, if we have the following arangement:
// | [workspace1] [workspace2] [workspace3] [workspace4]
// | [container1] [container1] [container2] [container1]
// And if we are changing from workspace 1 to workspace 4,
// we should be doing the following:
// First container (repeat 2 times) will stay in place until
// we reach container 3, then animate to the left and container 2
// also move to the left after that while container 1 in workspace 4
// will slide in from the right
// Get the index from first and last workspace
const firstWorkspaceIndex = workspaces.findIndex(
w => w.uuid === essentialsWorkspaces[0].uuid
);
const lastWorkspaceIndex = workspaces.findIndex(
w =>
w.uuid ===
essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
);
cloned.originalContainer.style.removeProperty("transform");
// Check if the container is even going to appear on the screen, to save on animation
if (
// We also need to check if the container is even going to appear on the screen.
// In order to do this, we need to check if the container is between the first and last workspace.
// Note that essential containers can have multiple workspaces,
// so we need to check if any of the workspaces in the container are between the
// first and last workspace.
!essentialsContainersInBetween.find(
ce =>
ce.workspaces.some(
w => w.uuid === essentialsWorkspaces[0].uuid
) &&
ce.workspaces.some(
w =>
w.uuid ===
essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
)
)
) {
if (!containsPrev && !containsNew) {
container.setAttribute("hidden", "true");
continue;
}
cloned.originalContainer.parentNode.appendChild(container);
let stepsInBetween =
Math.abs(
newWorkspaceIndex -
(isGoingLeft ? firstWorkspaceIndex : lastWorkspaceIndex)
) + 1;
const usingSameContainer =
newWorkspaceEssentialsContainer?.workspaces.some(
w => w.uuid === newWorkspace.uuid
) &&
newWorkspaceEssentialsContainer?.workspaces.some(
w => w.uuid === previousWorkspace.uuid
);
let newOffset =
-(
newWorkspaceIndex -
(isGoingLeft ? firstWorkspaceIndex : lastWorkspaceIndex) +
(!isGoingLeft ? repeats - 1 : -repeats + 1)
) * 100;
let existingOffset =
-(
newWorkspaceIndex -
(isGoingLeft ? lastWorkspaceIndex : firstWorkspaceIndex) +
(isGoingLeft ? repeats - 1 : -repeats + 1)
) * 100;
container.removeAttribute("hidden");
// If we are on the same container and both new and old workspace are in the same "essentialsWorkspaces"
// we can simply not animate the essentials
if (
usingSameContainer &&
essentialsWorkspaces.some(w => w.uuid === newWorkspace.uuid) &&
essentialsWorkspaces.some(w => w.uuid === previousWorkspace.uuid)
) {
newOffset = 0;
existingOffset = 0;
if (containsPrev && containsNew) {
container.style.transform = "translateX(0%)";
continue;
}
const needsOffsetAdjustment =
stepsInBetween > essentialsWorkspaces.length || usingSameContainer;
let existingOffset, newOffset;
const currentTransform =
parseFloat(container.style.transform.split("(")[1]) || 0;
if (containsPrev && !containsNew) {
existingOffset = currentTransform;
newOffset = isGoingLeft ? 100 : -100;
} else {
existingOffset = currentTransform || (isGoingLeft ? -100 : 100);
newOffset = 0;
}
if (repeats > 0 && needsOffsetAdjustment) {
if (!isGoingLeft) {
if (existingOffset !== 0) {
existingOffset += 100;
}
if (newOffset !== 0) {
newOffset += 100;
}
if (spaceLen === 2) {
if (containsPrev && !containsNew) {
existingOffset = currentTransform;
newOffset = currentTransform >= 0 ? 100 : -100;
} else {
if (existingOffset !== 0) {
existingOffset -= 100;
}
if (newOffset !== 0) {
newOffset -= 100;
}
existingOffset = currentTransform >= 0 ? 100 : -100;
newOffset = 0;
}
}
// Special case: going forward from single reused container to a new one
if (
!usingSameContainer &&
!isGoingLeft &&
lastWorkspaceIndex === newWorkspaceIndex - 1
) {
existingOffset = 0;
newOffset = -100;
stepsInBetween = 1;
}
if (
!usingSameContainer &&
isGoingLeft &&
firstWorkspaceIndex === newWorkspaceIndex + 1
) {
existingOffset = 0;
newOffset = 100;
stepsInBetween = 1;
}
if (
!usingSameContainer &&
isGoingLeft &&
(firstWorkspaceIndex === newWorkspaceIndex - 1 ||
firstWorkspaceIndex === newWorkspaceIndex)
) {
existingOffset = -100;
newOffset = 0;
stepsInBetween = 1;
}
if (
!usingSameContainer &&
!isGoingLeft &&
firstWorkspaceIndex === newWorkspaceIndex
) {
existingOffset = 100;
newOffset = 0;
stepsInBetween = 1;
}
const newTransform = `translateX(${newOffset}%)`;
let existingTransform = `translateX(${existingOffset}%)`;
if (container.style.transform && container.style.transform !== "none") {
existingTransform = container.style.transform;
}
if (shouldAnimate) {
container.style.transform = existingTransform;
animations.push(
gZenUIManager.motion.animate(
container,
{
transform: [
existingTransform,
new Array(stepsInBetween).fill(newTransform).join(","),
],
transform: [existingTransform, newTransform],
},
{
type: "spring",
@@ -2267,17 +2229,12 @@ class nsZenWorkspaces {
this.#currentSpaceSwitchContext.animations = [];
document.documentElement.removeAttribute("animating-background");
if (shouldAnimate) {
for (const cloned of clonedEssentials) {
cloned.container.remove();
for (const data of essentialsAnimData) {
data.element.style.removeProperty("transform");
}
this._alwaysAnimatePaddingTop = true;
this.updateTabsContainers();
}
const essentialsContainer = this.getEssentialsSection(
newWorkspace.containerTabId
);
essentialsContainer.removeAttribute("hidden");
essentialsContainer.style.transform = "none";
gBrowser.tabContainer._invalidateCachedTabs();
gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
this._animatingChange = false;
@@ -2493,7 +2450,7 @@ class nsZenWorkspaces {
for (const tab of gBrowser.tabs) {
if (
!tab.hasAttribute("zen-workspace-id") &&
!tab.hasAttribute("zen-workspace-id")
!tab.hasAttribute("zen-essential")
) {
tab.setAttribute("zen-workspace-id", workspace.uuid);
}
@@ -3152,9 +3109,6 @@ class nsZenWorkspaces {
...normalContainers,
];
for (const container of containers) {
if (container.hasAttribute("cloned")) {
continue;
}
for (const tab of container.children) {
if (gBrowser.isTab(tab)) {
tabs.push(tab);

View File

@@ -0,0 +1,160 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
});
function normalizeUserContextId(value) {
const normalized = typeof value === "string" ? Number(value) : value;
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
class ZenSyncManager {
getSidebarData() {
return lazy.ZenSessionStore.getSidebarData();
}
/**
* Whether to ignore changes to items. This is used to prevent
* infinite loops when applying incoming sync changes.
*
* @type {boolean}
*/
#ignoreChanges = false;
#changedItems = new Map();
markItemChanged(item) {
if (item.type && item.id && !this.#ignoreChanges) {
const key = `${item.type}~${item.id}`;
this.#changedItems.set(key, { type: item.type, id: item.id });
}
}
#getChangedItems() {
return Array.from(this.#changedItems.values());
}
#clearChangedItems() {
this.#changedItems.clear();
}
notifyAboutChanges() {
const changedItems = this.#getChangedItems();
for (const item of changedItems) {
Services.obs.notifyObservers(
{ wrappedJSObject: item },
"zen-workspace-item-changed"
);
}
this.#clearChangedItems();
}
async applyIncomingBatch(pulled, removals) {
try {
this.#ignoreChanges = true;
this.#applyIncomingContainers(
pulled.containers || [],
removals.containers || []
);
const win = lazy.ZenWindowSync.firstSyncedWindow;
if (win?.gZenWorkspaces) {
await win.gZenWorkspaces._applySyncChanges(pulled, removals);
}
} catch (e) {
console.error("ZenSyncManager: Failed to apply incoming sync data:", e);
throw e;
} finally {
this.#ignoreChanges = false;
}
}
#applyIncomingContainers(pulledContainers, removedContainers) {
const localContainersById = new Map(
lazy.ContextualIdentityService.getPublicIdentities().map(container => [
container.userContextId,
container,
])
);
for (const container of pulledContainers) {
if (!container.name) {
continue;
}
const userContextId = normalizeUserContextId(container.userContextId);
if (userContextId === null) {
console.warn(
"ZenSyncManager: Ignoring incoming container with invalid userContextId",
{ container }
);
continue;
}
const existsLocally = localContainersById.has(userContextId);
if (existsLocally) {
lazy.ContextualIdentityService.update(
userContextId,
container.name,
container.icon,
container.color
);
continue;
}
const createdIdentity = lazy.ContextualIdentityService.create(
container.name,
container.icon,
container.color,
userContextId
);
if (createdIdentity) {
localContainersById.set(createdIdentity.userContextId, createdIdentity);
}
if (createdIdentity && createdIdentity.userContextId !== userContextId) {
console.warn("ZenSyncManager: Container sync created unexpected ID", {
requestedId: userContextId,
createdId: createdIdentity.userContextId,
name: container.name,
});
}
}
for (const container of removedContainers) {
const userContextId = normalizeUserContextId(container.userContextId);
if (userContextId === null) {
console.warn(
"ZenSyncManager: Ignoring container removal with invalid userContextId",
{ container }
);
continue;
}
if (!localContainersById.has(userContextId)) {
continue;
}
try {
lazy.ContextualIdentityService.remove(userContextId);
localContainersById.delete(userContextId);
} catch {
// Container may already be gone locally.
}
}
}
}
export const ZenSyncStore = new ZenSyncManager();

View File

@@ -0,0 +1,404 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
Store,
SyncEngine,
Tracker,
} from "resource://services-sync/engines.sys.mjs";
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
});
const RECORD_ID_PREFIX_BY_TYPE = Object.freeze({
space: "s",
container: "c",
});
const RECORD_TYPE_BY_PREFIX = Object.freeze({
s: "space",
c: "container",
});
/**
* Sync record wrapper for workspace and container items stored in the
* Workspaces engine collection.
*/
export class ZenWorkspacesRecord extends CryptoWrapper {
_logName = "Sync.Record.ZenWorkspaces";
}
ZenWorkspacesRecord.prototype.type = "workspaces";
function parseRecordId(id) {
const sep = id.indexOf("~");
if (sep === -1) {
return null;
}
const prefix = id.slice(0, sep);
const key = id.slice(sep + 1);
return { type: RECORD_TYPE_BY_PREFIX[prefix] || prefix, key };
}
function createRecordId(type, id) {
const prefix = RECORD_ID_PREFIX_BY_TYPE[type];
if (!prefix) {
throw new Error(`Unknown Workspaces Sync record type: ${type}`);
}
return `${prefix}~${id}`;
}
function normalizeUserContextId(value) {
const normalized = typeof value === "string" ? Number(value) : value;
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
/**
* Strips the sync-envelope fields (`id` and `type`) from incoming record data
* and restores the item's real identity key where needed
*
* @param {object} data
*/
function stripSyncFields(data) {
const rest = { ...data };
delete rest.id;
delete rest.type;
return rest;
}
/**
* Sync store implementation that serializes local workspace and container
* state into records and applies incoming remote changes.
*/
class ZenWorkspacesStore extends Store {
constructor(name, engine) {
super(name, engine);
}
async getAllIDs() {
const ids = {};
const sidebar = lazy.ZenSyncStore.getSidebarData();
for (const space of sidebar.spaces || []) {
if (space.uuid) {
ids[createRecordId("space", space.uuid)] = true;
}
}
for (const c of lazy.ContextualIdentityService.getPublicIdentities()) {
ids[createRecordId("container", c.userContextId)] = true;
}
return ids;
}
async itemExists(id) {
const parsed = parseRecordId(id);
if (!parsed) {
return false;
}
const sidebar = lazy.ZenSyncStore.getSidebarData();
switch (parsed.type) {
case "space":
return (sidebar.spaces || []).some(s => s.uuid === parsed.key);
case "container":
return lazy.ContextualIdentityService.getPublicIdentities().some(
c => String(c.userContextId) === parsed.key
);
default:
return false;
}
}
async createRecord(id, collection) {
const record = new ZenWorkspacesRecord(collection, id);
const parsed = parseRecordId(id);
if (!parsed) {
record.deleted = true;
return record;
}
const sidebar = lazy.ZenSyncStore.getSidebarData();
switch (parsed.type) {
case "space": {
const spaces = sidebar.spaces || [];
const idx = spaces.findIndex(s => s.uuid === parsed.key);
if (idx === -1) {
record.deleted = true;
return record;
}
const rest = { ...spaces[idx] };
delete rest.syncStatus;
record.cleartext = { id, type: "space", ...rest, position: idx };
break;
}
case "container": {
const container =
lazy.ContextualIdentityService.getPublicIdentities().find(
c => String(c.userContextId) === parsed.key
);
if (!container) {
record.deleted = true;
return record;
}
record.cleartext = {
id,
type: "container",
userContextId: container.userContextId,
name: container.name,
icon: container.icon,
color: container.color,
};
break;
}
default:
record.deleted = true;
}
return record;
}
async applyIncomingBatch(records, _countTelemetry) {
const pulled = { spaces: [], containers: [] };
const removals = { spaces: [], containers: [] };
for (const record of records) {
if (record.deleted) {
this._collectRemoval(record.id, removals);
continue;
}
const data = record.cleartext;
if (!data?.type) {
continue;
}
const clean = stripSyncFields(data);
switch (data.type) {
case "space":
pulled.spaces.push(clean);
break;
case "container":
pulled.containers.push(clean);
break;
}
}
// Suppress change tracking while applying incoming data to prevent
// feedback loops where applied items get re-uploaded immediately.
this.engine._tracker.ignoreAll = true;
try {
await lazy.ZenSyncStore.applyIncomingBatch(pulled, removals);
} finally {
this.engine._tracker.ignoreAll = false;
}
return [];
}
_collectRemoval(id, removals) {
const parsed = parseRecordId(id);
if (!parsed) {
return;
}
switch (parsed.type) {
case "space":
removals.spaces.push({ uuid: parsed.key });
break;
case "container": {
const userContextId = normalizeUserContextId(parsed.key);
if (userContextId === null) {
console.warn(
"ZenWorkspacesStore: Ignoring container removal with invalid userContextId",
{ id }
);
break;
}
removals.containers.push({ userContextId });
break;
}
}
}
async create(record) {
await this._applySingle(record);
}
async update(record) {
await this._applySingle(record);
}
async _applySingle(record) {
this.engine._tracker.ignoreAll = true;
try {
if (record.deleted) {
const removals = { spaces: [], containers: [] };
this._collectRemoval(record.id, removals);
await lazy.ZenSyncStore.applyIncomingBatch(
{ spaces: [], containers: [] },
removals
);
return;
}
const data = record.cleartext;
if (!data?.type) {
return;
}
const clean = stripSyncFields(data);
const pulled = { spaces: [], containers: [] };
switch (data.type) {
case "space":
pulled.spaces.push(clean);
break;
case "container":
pulled.containers.push(clean);
break;
}
await lazy.ZenSyncStore.applyIncomingBatch(pulled, {
spaces: [],
containers: [],
});
} finally {
this.engine._tracker.ignoreAll = false;
}
}
async remove() {
// No-op: never delete user data on wipe
}
async wipe() {
// No-op: never delete user data on wipe
}
changeItemID() {
// No-op
}
}
/**
* Sync tracker that watches workspace and contextual identity observers and
* marks the corresponding record IDs as changed.
*/
class ZenWorkspacesTracker extends Tracker {
#changedIDs = {};
#ignoreAll = false;
get ignoreAll() {
return this.#ignoreAll;
}
set ignoreAll(value) {
this.#ignoreAll = value;
}
onStart() {
Services.obs.addObserver(this, "zen-workspace-item-changed");
Services.obs.addObserver(this, "contextual-identity-created");
Services.obs.addObserver(this, "contextual-identity-updated");
Services.obs.addObserver(this, "contextual-identity-deleted");
}
onStop() {
Services.obs.removeObserver(this, "zen-workspace-item-changed");
Services.obs.removeObserver(this, "contextual-identity-created");
Services.obs.removeObserver(this, "contextual-identity-updated");
Services.obs.removeObserver(this, "contextual-identity-deleted");
}
observe(subject, topic, _data) {
if (this.#ignoreAll) {
return;
}
if (topic === "zen-workspace-item-changed") {
const type = subject?.wrappedJSObject?.type;
const id = subject?.wrappedJSObject?.id;
if (type && id) {
this._trackChange({ type, id });
}
} else if (topic.startsWith("contextual-identity-")) {
const id = subject?.wrappedJSObject?.userContextId;
if (id && normalizeUserContextId(id) !== null) {
this._trackChange({ type: "container", id });
}
}
}
_trackChange(data) {
if (data.type && data.id) {
const id = createRecordId(data.type, data.id);
this.#changedIDs[id] = Date.now() / 1000;
this.score += SCORE_INCREMENT_XLARGE;
}
}
async getChangedIDs() {
return { ...this.#changedIDs };
}
async addChangedID(id, when) {
this.#changedIDs[id] = when;
return true;
}
async removeChangedID(...ids) {
for (const id of ids) {
delete this.#changedIDs[id];
}
return true;
}
clearChangedIDs() {
this.#changedIDs = {};
}
}
/**
* Sync engine entrypoint that wires the Workspaces record, store, and tracker
* implementations into Firefox Sync.
*/
export class ZenWorkspacesEngine extends SyncEngine {
static get name() {
return "Workspaces";
}
constructor(service) {
super("Workspaces", service);
}
get _storeObj() {
return ZenWorkspacesStore;
}
get _trackerObj() {
return ZenWorkspacesTracker;
}
get _recordObj() {
return ZenWorkspacesRecord;
}
get version() {
return 2;
}
get syncPriority() {
return 8;
}
get allowSkippedRecord() {
return false;
}
}

8
src/zen/sync/moz.build Normal file
View File

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

View File

@@ -1156,10 +1156,6 @@
pointer-events: none;
position: fixed; /* Fix position to prevent scrolling */
}
&[cloned] {
pointer-events: none;
}
}
.tabbrowser-tab[zen-essential="true"],

View File

@@ -27,6 +27,10 @@ ChromeUtils.defineESModuleGetters(lazy, {
UrlUtils: "resource://gre/modules/UrlUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
return new Localization(["browser/zen-command-palette.ftl"], true);
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"enabledPref",
@@ -182,7 +186,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
.workspaceElement(workspace.uuid)
?.style.getPropertyValue("--zen-primary-color");
actions.push({
label: "Focus on",
label: lazy.l10n.formatValueSync("zen-action-focus-on"),
extraPayload: {
workspaceId: workspace.uuid,
prettyName: workspace.name,
@@ -215,7 +219,7 @@ export class ZenUrlbarProviderGlobalActions extends UrlbarProvider {
.map(addon => {
return {
icon: "chrome://browser/skin/zen-icons/extension.svg",
label: "Extension",
label: lazy.l10n.formatValueSync("zen-action-extension"),
commandId: `zen:extension-${addon.id}`,
extraPayload: {
extensionId: addon.id,

View File

@@ -13,58 +13,62 @@ XPCOMUtils.defineLazyPreferenceGetter(
2
);
ChromeUtils.defineLazyGetter(lazy, "l10n", () => {
return new Localization(["browser/zen-command-palette.ftl"], true);
});
function isNotEmptyTab(window) {
return !window.gBrowser.selectedTab.hasAttribute("zen-empty-tab");
}
const globalActionsTemplate = [
{
label: "Toggle Compact Mode",
l10nId: "zen-action-toggle-compact-mode",
command: "cmd_zenCompactModeToggle",
icon: "chrome://browser/skin/zen-icons/sidebar.svg",
},
{
label: "Open Theme Picker",
l10nId: "zen-action-open-theme-picker",
command: "cmd_zenOpenZenThemePicker",
icon: "chrome://browser/skin/zen-icons/edit-theme.svg",
},
{
label: "New Split View",
l10nId: "zen-action-new-split-view",
command: "cmd_zenNewEmptySplit",
icon: "chrome://browser/skin/zen-icons/split.svg",
},
{
label: "New Folder",
l10nId: "zen-action-new-folder",
command: "cmd_zenOpenFolderCreation",
icon: "chrome://browser/skin/zen-icons/folder.svg",
},
{
label: "Copy Current URL",
l10nId: "zen-action-copy-current-url",
command: "cmd_zenCopyCurrentURL",
icon: "chrome://browser/skin/zen-icons/link.svg",
},
{
label: "Settings",
l10nId: "zen-action-settings",
command: window => window.openPreferences(),
icon: "chrome://browser/skin/zen-icons/settings.svg",
},
{
label: "Open Private Window",
l10nId: "zen-action-open-private-window",
command: "Tools:PrivateBrowsing",
icon: "chrome://browser/skin/zen-icons/private-window.svg",
},
{
label: "Open New Window",
l10nId: "zen-action-open-new-window",
command: "cmd_newNavigator",
icon: "chrome://browser/skin/zen-icons/window.svg",
},
{
label: "New Blank Window",
l10nId: "zen-action-new-blank-window",
command: "cmd_zenNewNavigatorUnsynced",
icon: "chrome://browser/skin/zen-icons/window.svg",
},
{
label: "Pin Tab",
l10nId: "zen-action-pin-tab",
command: "cmd_zenTogglePinTab",
icon: "chrome://browser/skin/zen-icons/pin.svg",
isAvailable: window => {
@@ -73,7 +77,7 @@ const globalActionsTemplate = [
},
},
{
label: "Unpin Tab",
l10nId: "zen-action-unpin-tab",
command: "cmd_zenTogglePinTab",
icon: "chrome://browser/skin/zen-icons/unpin.svg",
isAvailable: window => {
@@ -82,12 +86,12 @@ const globalActionsTemplate = [
},
},
{
label: "Open Space Routing",
l10nId: "zen-action-open-space-routing",
command: "cmd_zenOpenSpaceRoutingSettings",
icon: "chrome://browser/skin/zen-icons/selectable/airplane.svg",
},
{
label: "New Boost",
l10nId: "zen-action-new-boost",
icon: "chrome://browser/skin/zen-icons/boost.svg",
isAvailable: window => {
if (!isNotEmptyTab(window)) {
@@ -130,7 +134,7 @@ const globalActionsTemplate = [
},
},
{
label: "Next Space",
l10nId: "zen-action-next-space",
command: "cmd_zenWorkspaceForward",
icon: "chrome://browser/skin/zen-icons/forward.svg",
isAvailable: window => {
@@ -138,7 +142,7 @@ const globalActionsTemplate = [
},
},
{
label: "Previous Space",
l10nId: "zen-action-previous-space",
command: "cmd_zenWorkspaceBackward",
icon: "chrome://browser/skin/zen-icons/back.svg",
isAvailable: window => {
@@ -147,7 +151,7 @@ const globalActionsTemplate = [
},
},
{
label: "Close Tab",
l10nId: "zen-action-close-tab",
command: "cmd_close",
icon: "chrome://browser/skin/zen-icons/close.svg",
isAvailable: window => {
@@ -155,27 +159,27 @@ const globalActionsTemplate = [
},
},
{
label: "Reload Tab",
l10nId: "zen-action-reload-tab",
command: "Browser:Reload",
icon: "chrome://browser/skin/zen-icons/reload.svg",
},
{
label: "Reload Tab Without Cache",
l10nId: "zen-action-reload-tab-without-cache",
command: "Browser:ReloadSkipCache",
icon: "chrome://browser/skin/zen-icons/reload.svg",
},
{
label: "Next Tab",
l10nId: "zen-action-next-tab",
command: "Browser:NextTab",
icon: "chrome://browser/skin/zen-icons/forward.svg",
},
{
label: "Previous Tab",
l10nId: "zen-action-previous-tab",
command: "Browser:PrevTab",
icon: "chrome://browser/skin/zen-icons/back.svg",
},
{
label: "Capture Screenshot",
l10nId: "zen-action-capture-screenshot",
command: "Browser:Screenshot",
icon: "chrome://browser/skin/zen-icons/screenshot.svg",
isAvailable: window => {
@@ -183,12 +187,12 @@ const globalActionsTemplate = [
},
},
{
label: "Toggle Tabs on right",
l10nId: "zen-action-toggle-tabs-on-right",
command: "cmd_zenToggleTabsOnRight",
icon: "chrome://browser/skin/zen-icons/sidebars-right.svg",
},
{
label: "Add to Essentials",
l10nId: "zen-action-add-to-essentials",
command: window =>
window.gZenPinnedTabManager.addToEssentials(window.gBrowser.selectedTab),
isAvailable: window => {
@@ -201,7 +205,7 @@ const globalActionsTemplate = [
icon: "chrome://browser/skin/zen-icons/essential-add.svg",
},
{
label: "Remove from Essentials",
l10nId: "zen-action-remove-from-essentials",
command: window =>
window.gZenPinnedTabManager.removeEssentials(window.gBrowser.selectedTab),
isAvailable: window =>
@@ -209,7 +213,7 @@ const globalActionsTemplate = [
icon: "chrome://browser/skin/zen-icons/essential-remove.svg",
},
{
label: "Find in Page",
l10nId: "zen-action-find-in-page",
command: "cmd_find",
icon: "chrome://browser/skin/zen-icons/search-page.svg",
isAvailable: window => {
@@ -217,12 +221,12 @@ const globalActionsTemplate = [
},
},
{
label: "Manage Extensions",
l10nId: "zen-action-manage-extensions",
command: "Tools:Addons",
icon: "chrome://browser/skin/zen-icons/extension.svg",
},
{
label: "Switch to Automatic Appearance",
l10nId: "zen-action-switch-to-automatic-appearance",
command: () => Services.prefs.setIntPref("zen.view.window.scheme", 2),
icon: "chrome://browser/skin/zen-icons/sparkles.svg",
isAvailable: () => {
@@ -230,7 +234,7 @@ const globalActionsTemplate = [
},
},
{
label: "Switch to Light Mode",
l10nId: "zen-action-switch-to-light-mode",
command: () => Services.prefs.setIntPref("zen.view.window.scheme", 1),
icon: "chrome://browser/skin/zen-icons/face-sun.svg",
isAvailable: () => {
@@ -238,7 +242,7 @@ const globalActionsTemplate = [
},
},
{
label: "Switch to Dark Mode",
l10nId: "zen-action-switch-to-dark-mode",
command: () => Services.prefs.setIntPref("zen.view.window.scheme", 0),
icon: "chrome://browser/skin/zen-icons/moon-stars.svg",
isAvailable: () => {
@@ -246,7 +250,7 @@ const globalActionsTemplate = [
},
},
{
label: "Print",
l10nId: "zen-action-print",
command: "cmd_print",
icon: "chrome://browser/skin/zen-icons/print.svg",
isAvailable: window => {
@@ -266,7 +270,10 @@ export const globalActions = globalActionsTemplate.map(action => ({
commandId:
typeof action.command === "string"
? action.command
: `zen:global-action-${action.label.toLowerCase().replace(/\s+/g, "-")}`,
: `zen:global-action-${action.l10nId.replace("zen-action-", "")}`,
extraPayload: {},
...action,
get label() {
return lazy.l10n.formatValueSync(action.l10nId);
},
}));

View File

@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
"version": "152.0.4",
"candidate": "152.0.4",
"version": "152.0.5",
"candidate": "152.0.5",
"candidateBuild": 1
},
"buildOptions": {
@@ -20,7 +20,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
"displayVersion": "1.21.5b",
"displayVersion": "1.21.6b",
"github": {
"repo": "zen-browser/desktop"
},
@@ -54,4 +54,4 @@
"licenseType": "MPL-2.0"
},
"updateHostname": "updates.zen-browser.app"
}
}

View File

@@ -346,6 +346,17 @@ fn expand_pref_values(prefs: &mut [Preference]) {
}
pref.value = new_value.clone();
}
// Also change the condition if it contains placeholders
if let Some(condition) = &pref.condition {
let mut new_condition = condition.clone();
for (key, value) in &env_values {
let placeholder = format!("@{}@", key);
if new_condition.contains(&placeholder) {
new_condition = new_condition.replace(&placeholder, if *value { "1" } else { "0" });
}
pref.condition = Some(new_condition.clone());
}
}
}
}