1
0
Fork 1
mirror of https://github.com/zen-browser/desktop.git synced 2025-07-08 00:10:00 +02:00

Translated files to a single monorepo

This commit is contained in:
mr. M 2024-11-04 17:37:02 +01:00
parent f498a64413
commit ec65a69496
No known key found for this signature in database
GPG key ID: CBD57A2AEDBDA1FB
25 changed files with 8696 additions and 26 deletions

3
.gitmodules vendored
View file

@ -1,6 +1,3 @@
[submodule "src/browser/base/content/zen-components"]
path = src/browser/base/content/zen-components
url = https://github.com/zen-browser/components
[submodule "l10n"] [submodule "l10n"]
path = l10n path = l10n
url = https://github.com/zen-browser/l10n-packs url = https://github.com/zen-browser/l10n-packs

View file

@ -4,24 +4,24 @@
content/browser/ZenStartup.mjs (content/ZenStartup.mjs) content/browser/ZenStartup.mjs (content/ZenStartup.mjs)
content/browser/ZenUIManager.mjs (content/ZenUIManager.mjs) content/browser/ZenUIManager.mjs (content/ZenUIManager.mjs)
content/browser/ZenCustomizableUI.sys.mjs (content/ZenCustomizableUI.sys.mjs) content/browser/ZenCustomizableUI.sys.mjs (content/ZenCustomizableUI.sys.mjs)
content/browser/zen-components/ZenCompactMode.mjs (content/zen-components/src/ZenCompactMode.mjs) content/browser/zen-components/ZenCompactMode.mjs (zen-components/ZenCompactMode.mjs)
content/browser/zen-components/ZenViewSplitter.mjs (content/zen-components/src/ZenViewSplitter.mjs) content/browser/zen-components/ZenViewSplitter.mjs (zen-components/ZenViewSplitter.mjs)
content/browser/zen-components/ZenThemesCommon.mjs (content/zen-components/src/ZenThemesCommon.mjs) content/browser/zen-components/ZenThemesCommon.mjs (zen-components/ZenThemesCommon.mjs)
content/browser/zen-components/ZenWorkspaces.mjs (content/zen-components/src/ZenWorkspaces.mjs) content/browser/zen-components/ZenWorkspaces.mjs (zen-components/ZenWorkspaces.mjs)
content/browser/zen-components/ZenWorkspacesStorage.mjs (content/zen-components/src/ZenWorkspacesStorage.mjs) content/browser/zen-components/ZenWorkspacesStorage.mjs (zen-components/ZenWorkspacesStorage.mjs)
content/browser/zen-components/ZenWorkspacesSync.mjs (content/zen-components/src/ZenWorkspacesSync.mjs) content/browser/zen-components/ZenWorkspacesSync.mjs (zen-components/ZenWorkspacesSync.mjs)
content/browser/zen-components/ZenSidebarManager.mjs (content/zen-components/src/ZenSidebarManager.mjs) content/browser/zen-components/ZenSidebarManager.mjs (zen-components/ZenSidebarManager.mjs)
content/browser/zen-components/ZenProfileDialogUI.mjs (content/zen-components/src/ZenProfileDialogUI.mjs) content/browser/zen-components/ZenProfileDialogUI.mjs (zen-components/ZenProfileDialogUI.mjs)
content/browser/zen-components/ZenKeyboardShortcuts.mjs (content/zen-components/src/ZenKeyboardShortcuts.mjs) content/browser/zen-components/ZenKeyboardShortcuts.mjs (zen-components/ZenKeyboardShortcuts.mjs)
content/browser/zen-components/ZenThemeBuilder.mjs (content/zen-components/src/ZenThemeBuilder.mjs) content/browser/zen-components/ZenThemeBuilder.mjs (zen-components/ZenThemeBuilder.mjs)
content/browser/zen-components/ZenThemesImporter.mjs (content/zen-components/src/ZenThemesImporter.mjs) content/browser/zen-components/ZenThemesImporter.mjs (zen-components/ZenThemesImporter.mjs)
content/browser/zen-components/ZenTabUnloader.mjs (content/zen-components/src/ZenTabUnloader.mjs) content/browser/zen-components/ZenTabUnloader.mjs (zen-components/ZenTabUnloader.mjs)
content/browser/zen-components/ZenPinnedTabsStorage.mjs (content/zen-components/src/ZenPinnedTabsStorage.mjs) content/browser/zen-components/ZenPinnedTabsStorage.mjs (zen-components/ZenPinnedTabsStorage.mjs)
content/browser/zen-components/ZenPinnedTabManager.mjs (content/zen-components/src/ZenPinnedTabManager.mjs) content/browser/zen-components/ZenPinnedTabManager.mjs (zen-components/ZenPinnedTabManager.mjs)
content/browser/zen-components/ZenCommonUtils.mjs (content/zen-components/src/ZenCommonUtils.mjs) content/browser/zen-components/ZenCommonUtils.mjs (zen-components/ZenCommonUtils.mjs)
content/browser/zen-components/ZenGradientGenerator.mjs (content/zen-components/src/ZenGradientGenerator.mjs) content/browser/zen-components/ZenGradientGenerator.mjs (zen-components/ZenGradientGenerator.mjs)
content/browser/zen-components/ZenGlanceManager.mjs (content/zen-components/src/ZenGlanceManager.mjs) content/browser/zen-components/ZenGlanceManager.mjs (zen-components/ZenGlanceManager.mjs)
content/browser/zen-components/ZenActorsManager.mjs (content/zen-components/src/ZenActorsManager.mjs) content/browser/zen-components/ZenActorsManager.mjs (zen-components/ZenActorsManager.mjs)
content/browser/zen-styles/zen-theme.css (content/zen-styles/zen-theme.css) content/browser/zen-styles/zen-theme.css (content/zen-styles/zen-theme.css)
content/browser/zen-styles/zen-buttons.css (content/zen-styles/zen-buttons.css) content/browser/zen-styles/zen-buttons.css (content/zen-styles/zen-buttons.css)
@ -56,7 +56,7 @@
content/browser/zen-images/gradient-display.png (content/zen-images/gradient-display.png) content/browser/zen-images/gradient-display.png (content/zen-images/gradient-display.png)
# Actors # Actors
content/browser/zen-components/actors/ZenThemeMarketplaceParent.sys.mjs (content/zen-components/src/actors/ZenThemeMarketplaceParent.sys.mjs) content/browser/zen-components/actors/ZenThemeMarketplaceParent.sys.mjs (zen-components/actors/ZenThemeMarketplaceParent.sys.mjs)
content/browser/zen-components/actors/ZenThemeMarketplaceChild.sys.mjs (content/zen-components/src/actors/ZenThemeMarketplaceChild.sys.mjs) content/browser/zen-components/actors/ZenThemeMarketplaceChild.sys.mjs (zen-components/actors/ZenThemeMarketplaceChild.sys.mjs)
content/browser/zen-components/actors/ZenGlanceChild.sys.mjs (content/zen-components/src/actors/ZenGlanceChild.sys.mjs) content/browser/zen-components/actors/ZenGlanceChild.sys.mjs (zen-components/actors/ZenGlanceChild.sys.mjs)
content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (content/zen-components/src/actors/ZenGlanceParent.sys.mjs) content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (zen-components/actors/ZenGlanceParent.sys.mjs)

@ -1 +0,0 @@
Subproject commit 037547460d0e25a276ad340e1351d3acd9d29f60

View file

@ -0,0 +1,19 @@
// Utility to register JSWindowActors
var gZenActorsManager = {
_actors: new Set(),
addJSWindowActor(...args) {
if (this._actors.has(args[0])) {
// Actor already registered, nothing to do
return;
}
try {
ChromeUtils.registerWindowActor(...args);
this._actors.add(args[0]);
} catch (e) {
console.warn(`Failed to register JSWindowActor: ${e}`);
}
},
}

View file

@ -0,0 +1,55 @@
var gZenOperatingSystemCommonUtils = {
kZenOSToSmallName: {
WINNT: 'windows',
Darwin: 'macos',
Linux: 'linux',
},
get currentOperatingSystem() {
let os = Services.appinfo.OS;
return this.kZenOSToSmallName[os];
},
};
class ZenMultiWindowFeature {
constructor() {}
static get browsers() {
return Services.wm.getEnumerator('navigator:browser');
}
static get currentBrowser() {
return Services.wm.getMostRecentWindow('navigator:browser');
}
isActiveWindow() {
return ZenMultiWindowFeature.currentBrowser === window;
}
async foreachWindowAsActive(callback) {
if (!this.isActiveWindow()) {
return;
}
for (const browser of ZenMultiWindowFeature.browsers) {
try {
await callback(browser);
} catch (e) {
console.error(e);
}
}
}
}
class ZenDOMOperatedFeature {
constructor() {
var initBound = this.init.bind(this);
document.addEventListener('DOMContentLoaded', initBound, { once: true });
}
}
class ZenPreloadedFeature {
constructor() {
var initBound = this.init.bind(this);
document.addEventListener('MozBeforeInitialXULLayout', initBound, { once: true });
}
}

View file

@ -0,0 +1,253 @@
const lazyCompactMode = {};
XPCOMUtils.defineLazyPreferenceGetter(
lazyCompactMode,
'COMPACT_MODE_FLASH_DURATION',
'zen.view.compact.toolbar-flash-popup.duration',
800
);
var gZenCompactModeManager = {
_flashTimeouts: {},
_evenListeners: [],
_removeHoverFrames: {},
init() {
Services.prefs.addObserver('zen.view.compact', this._updateEvent.bind(this));
Services.prefs.addObserver('zen.view.sidebar-expanded.on-hover', this._disableTabsOnHoverIfConflict.bind(this));
Services.prefs.addObserver('zen.tabs.vertical.right-side', this._updateSidebarIsOnRight.bind(this));
gZenUIManager.addPopupTrackingAttribute(this.sidebar);
gZenUIManager.addPopupTrackingAttribute(document.getElementById('zen-appcontent-navbar-container'));
this.addMouseActions();
this.addContextMenu();
},
get prefefence() {
return Services.prefs.getBoolPref('zen.view.compact');
},
set preference(value) {
Services.prefs.setBoolPref('zen.view.compact', value);
return value;
},
get sidebarIsOnRight() {
if (this._sidebarIsOnRight) {
return this._sidebarIsOnRight;
}
return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
},
get sidebar() {
if (!this._sidebar) {
this._sidebar = document.getElementById('navigator-toolbox');
}
return this._sidebar;
},
addContextMenu() {
const fragment = window.MozXULElement.parseXULToFragment(`
<menu id="zen-context-menu-compact-mode" data-l10n-id="zen-toolbar-context-compact-mode">
<menupopup>
<menuitem id="zen-context-menu-compact-mode-toggle" data-l10n-id="zen-toolbar-context-compact-mode-enable" type="checkbox" oncommand="gZenCompactModeManager.toggle();"/>
<menuseparator/>
<menuitem id="zen-context-menu-compact-mode-hide-sidebar" data-l10n-id="zen-toolbar-context-compact-mode-just-tabs" type="radio" oncommand="gZenCompactModeManager.hideSidebar();"/>
<menuitem id="zen-context-menu-compact-mode-hide-toolbar" data-l10n-id="zen-toolbar-context-compact-mode-just-toolbar" type="radio" oncommand="gZenCompactModeManager.hideToolbar();"/>
<menuitem id="zen-context-menu-compact-mode-hide-both" data-l10n-id="zen-toolbar-context-compact-mode-hide-both" type="radio" oncommand="gZenCompactModeManager.hideBoth();"/>
</menupopup>
</menu>
`);
document.getElementById('viewToolbarsMenuSeparator').before(fragment);
this.updateContextMenu();
},
hideSidebar() {
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', false);
},
hideToolbar() {
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', false);
},
hideBoth() {
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
},
addEventListener(callback) {
this._evenListeners.push(callback);
},
_updateEvent() {
this._evenListeners.forEach((callback) => callback());
this._disableTabsOnHoverIfConflict();
this.updateContextMenu();
},
updateContextMenu() {
document
.getElementById('zen-context-menu-compact-mode-toggle')
.setAttribute('checked', Services.prefs.getBoolPref('zen.view.compact'));
const hideTabBar = Services.prefs.getBoolPref('zen.view.compact.hide-tabbar');
const hideToolbar = Services.prefs.getBoolPref('zen.view.compact.hide-toolbar');
const hideBoth = hideTabBar && hideToolbar;
const idName = 'zen-context-menu-compact-mode-hide-';
document.getElementById(idName + 'sidebar').setAttribute('checked', !hideBoth && hideTabBar);
document.getElementById(idName + 'toolbar').setAttribute('checked', !hideBoth && hideToolbar);
document.getElementById(idName + 'both').setAttribute('checked', hideBoth);
},
_removeOpenStateOnUnifiedExtensions() {
// Fix for bug https://github.com/zen-browser/desktop/issues/1925
const buttons = document.querySelectorAll('toolbarbutton:is(#unified-extensions-button, .webextension-browser-action)');
for (let button of buttons) {
button.removeAttribute('open');
}
},
_disableTabsOnHoverIfConflict() {
if (Services.prefs.getBoolPref('zen.view.compact') && Services.prefs.getBoolPref('zen.view.compact.hide-tabbar')) {
Services.prefs.setBoolPref('zen.view.sidebar-expanded.on-hover', false);
}
},
toggle() {
return (this.preference = !this.prefefence);
},
_updateSidebarIsOnRight() {
this._sidebarIsOnRight = Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
},
toggleSidebar() {
this.sidebar.toggleAttribute('zen-user-show');
},
get hideAfterHoverDuration() {
if (this._hideAfterHoverDuration) {
return this._hideAfterHoverDuration;
}
return Services.prefs.getIntPref('zen.view.compact.toolbar-hide-after-hover.duration');
},
get hoverableElements() {
return [
{
element: this.sidebar,
screenEdge: this.sidebarIsOnRight ? 'right' : 'left',
},
{
element: document.getElementById('zen-appcontent-navbar-container'),
screenEdge: 'top',
},
];
},
flashSidebar(duration = lazyCompactMode.COMPACT_MODE_FLASH_DURATION) {
let tabPanels = document.getElementById('tabbrowser-tabpanels');
if (!tabPanels.matches("[zen-split-view='true']")) {
this.flashElement(this.sidebar, duration, this.sidebar.id);
}
},
flashElement(element, duration, id, attrName = 'flash-popup') {
if (element.matches(':hover')) {
return;
}
if (this._flashTimeouts[id]) {
clearTimeout(this._flashTimeouts[id]);
} else {
requestAnimationFrame(() => element.setAttribute(attrName, 'true'));
}
this._flashTimeouts[id] = setTimeout(() => {
window.requestAnimationFrame(() => {
element.removeAttribute(attrName);
this._flashTimeouts[id] = null;
});
}, duration);
},
clearFlashTimeout(id) {
clearTimeout(this._flashTimeouts[id]);
this._flashTimeouts[id] = null;
},
addMouseActions() {
for (let i = 0; i < this.hoverableElements.length; i++) {
let target = this.hoverableElements[i].element;
target.addEventListener('mouseenter', (event) => {
this.clearFlashTimeout('has-hover' + target.id);
window.requestAnimationFrame(() => target.setAttribute('zen-has-hover', 'true'));
});
target.addEventListener('mouseleave', (event) => {
// If on Mac, ignore mouseleave in the area of window buttons
if (AppConstants.platform == 'macosx') {
const MAC_WINDOW_BUTTONS_X_BORDER = 75;
const MAC_WINDOW_BUTTONS_Y_BORDER = 40;
if (event.clientX < MAC_WINDOW_BUTTONS_X_BORDER && event.clientY < MAC_WINDOW_BUTTONS_Y_BORDER) {
return;
}
}
if (this.hoverableElements[i].keepHoverDuration) {
this.flashElement(target, keepHoverDuration, 'has-hover' + target.id, 'zen-has-hover');
} else {
this._removeHoverFrames[target.id] = window.requestAnimationFrame(() => target.removeAttribute('zen-has-hover'));
}
});
}
document.documentElement.addEventListener('mouseleave', (event) => {
const screenEdgeCrossed = this._getCrossedEdge(event.pageX, event.pageY);
if (!screenEdgeCrossed) return;
for (let entry of this.hoverableElements) {
if (screenEdgeCrossed !== entry.screenEdge) continue;
const target = entry.element;
const boundAxis = entry.screenEdge === 'right' || entry.screenEdge === 'left' ? 'y' : 'x';
if (!this._positionInBounds(boundAxis, target, event.pageX, event.pageY, 7)) {
continue;
}
window.cancelAnimationFrame(this._removeHoverFrames[target.id]);
this.flashElement(target, this.hideAfterHoverDuration, 'has-hover' + target.id, 'zen-has-hover');
document.addEventListener(
'mousemove',
() => {
if (target.matches(':hover')) return;
target.removeAttribute('zen-has-hover');
this.clearFlashTimeout('has-hover' + target.id);
},
{ once: true }
);
}
});
},
_getCrossedEdge(posX, posY, element = document.documentElement, maxDistance = 10) {
const targetBox = element.getBoundingClientRect();
posX = Math.max(targetBox.left, Math.min(posX, targetBox.right));
posY = Math.max(targetBox.top, Math.min(posY, targetBox.bottom));
return ['top', 'bottom', 'left', 'right'].find((edge, i) => {
const distance = Math.abs((i < 2 ? posY : posX) - targetBox[edge]);
return distance <= maxDistance;
});
},
_positionInBounds(axis = 'x', element, x, y, error = 0) {
const bBox = element.getBoundingClientRect();
if (axis === 'y') return bBox.top - error < y && y < bBox.bottom + error;
else return bBox.left - error < x && x < bBox.right + error;
},
toggleToolbar() {
let toolbar = document.getElementById('zen-appcontent-navbar-container');
toolbar.toggleAttribute('zen-user-show');
},
};

View file

@ -0,0 +1,352 @@
{
class ZenGlanceManager extends ZenDOMOperatedFeature {
#currentBrowser = null;
#currentTab = null;
#animating = false;
init() {
document.documentElement.setAttribute("zen-glance-uuid", gZenUIManager.generateUuidv4());
window.addEventListener("keydown", this.onKeyDown.bind(this));
window.addEventListener("TabClose", this.onTabClose.bind(this));
ChromeUtils.defineLazyGetter(
this,
'sidebarButtons',
() => document.getElementById('zen-glance-sidebar-container')
);
document.getElementById('tabbrowser-tabpanels').addEventListener("click", this.onOverlayClick.bind(this));
Services.obs.addObserver(this, "quit-application-requested");
}
onKeyDown(event) {
if (event.key === "Escape" && this.#currentBrowser) {
event.preventDefault();
event.stopPropagation();
this.closeGlance();
}
}
onOverlayClick(event) {
if (event.target === this.overlay && event.originalTarget !== this.contentWrapper) {
this.closeGlance();
}
}
observe(subject, topic) {
switch (topic) {
case "quit-application-requested":
this.onUnload();
break;
}
}
onUnload() {
// clear everything
if (this.#currentBrowser) {
gBrowser.removeTab(this.#currentTab);
}
}
createBrowserElement(url, currentTab) {
const newTabOptions = {
userContextId: currentTab.getAttribute("usercontextid") || "",
skipBackgroundNotify: true,
insertTab: true,
skipLoad: false,
index: currentTab._tPos + 1,
};
this.currentParentTab = currentTab;
const newTab = gBrowser.addTrustedTab(Services.io.newURI(url).spec, newTabOptions);
gBrowser.selectedTab = newTab;
currentTab.querySelector(".tab-content").appendChild(newTab);
newTab.setAttribute("zen-glance-tab", true);
this.#currentBrowser = newTab.linkedBrowser;
this.#currentTab = newTab;
return this.#currentBrowser;
}
openGlance(data) {
if (this.#currentBrowser) {
return;
}
const initialX = data.x;
const initialY = data.y;
const initialWidth = data.width;
const initialHeight = data.height;
this.browserWrapper?.removeAttribute("animate");
this.browserWrapper?.removeAttribute("animate-end");
this.browserWrapper?.removeAttribute("animate-full");
this.browserWrapper?.removeAttribute("animate-full-end");
this.browserWrapper?.removeAttribute("has-finished-animation");
const url = data.url;
const currentTab = gBrowser.selectedTab;
this.animatingOpen = true;
const browserElement = this.createBrowserElement(url, currentTab);
this.overlay = browserElement.closest(".browserSidebarContainer");
this.browserWrapper = browserElement.closest(".browserContainer");
this.contentWrapper = browserElement.closest(".browserStack");
this.browserWrapper.prepend(this.sidebarButtons);
this.overlay.classList.add("zen-glance-overlay");
this.browserWrapper.removeAttribute("animate-end");
window.requestAnimationFrame(() => {
this.quickOpenGlance();
this.browserWrapper.style.setProperty("--initial-x", `${initialX}px`);
this.browserWrapper.style.setProperty("--initial-y", `${initialY}px`);
this.browserWrapper.style.setProperty("--initial-width", initialWidth + "px");
this.browserWrapper.style.setProperty("--initial-height", initialHeight + "px");
this.overlay.removeAttribute("fade-out");
this.browserWrapper.setAttribute("animate", true);
this.#animating = true;
setTimeout(() => {
this.browserWrapper.setAttribute("animate-end", true);
this.browserWrapper.setAttribute("has-finished-animation", true);
this.#animating = false;
this.animatingOpen = false;
}, 400);
});
}
closeGlance({ noAnimation = false, onTabClose = false } = {}) {
if (this.#animating || !this.#currentBrowser || this.animatingOpen || this._duringOpening) {
return;
}
this.browserWrapper.removeAttribute("has-finished-animation");
if (noAnimation) {
this.quickCloseGlance({ closeCurrentTab: false });
this.#currentBrowser = null;
this.#currentTab = null;
return;
}
gBrowser._insertTabAtIndex(this.#currentTab, {
index: this.currentParentTab._tPos + 1,
});
let quikcCloseZen = false;
if (onTabClose) {
// Create new tab if no more ex
if (gBrowser.tabs.length === 1) {
gBrowser.selectedTab = gZenUIManager.openAndChangeToTab(Services.prefs.getStringPref('browser.startup.homepage'));
return;
} else if (gBrowser.selectedTab === this.#currentTab) {
this._duringOpening = true;
gBrowser.tabContainer.advanceSelectedTab(1, true); // to skip the current tab
this._duringOpening = false;
quikcCloseZen = true;
}
}
// do NOT touch here, I don't know what it does, but it works...
window.requestAnimationFrame(() => {
this.#currentTab.style.display = "none";
this.browserWrapper.removeAttribute("animate");
this.browserWrapper.removeAttribute("animate-end");
this.overlay.setAttribute("fade-out", true);
window.requestAnimationFrame(() => {
this.browserWrapper.setAttribute("animate", true);
setTimeout(() => {
if (!this.currentParentTab) {
return;
}
if (!onTabClose || quikcCloseZen) {
this.quickCloseGlance();
}
this.overlay.removeAttribute("fade-out");
this.browserWrapper.removeAttribute("animate");
this.lastCurrentTab = this.#currentTab;
this.overlay.classList.remove("zen-glance-overlay");
gBrowser._getSwitcher().setTabStateNoAction(this.lastCurrentTab, gBrowser.AsyncTabSwitcher.STATE_UNLOADED);
if (!onTabClose && gBrowser.selectedTab === this.lastCurrentTab) {
this._duringOpening = true;
gBrowser.selectedTab = this.currentParentTab;
}
// reset everything
this.currentParentTab = null;
this.browserWrapper = null;
this.overlay = null;
this.contentWrapper = null;
this.lastCurrentTab.removeAttribute("zen-glance-tab");
gBrowser.tabContainer._invalidateCachedTabs();
this.lastCurrentTab.closing = true;
gBrowser.removeTab(this.lastCurrentTab, { animate: false });
this.#currentTab = null;
this.#currentBrowser = null;
this.lastCurrentTab = null;
this._duringOpening = false;
}, 500);
});
});
}
quickOpenGlance() {
if (!this.#currentBrowser || this._duringOpening) {
return;
}
this._duringOpening = true;
try {
gBrowser.selectedTab = this.#currentTab;
} catch (e) {}
this.currentParentTab.linkedBrowser.closest(".browserSidebarContainer").classList.add("deck-selected", "zen-glance-background");
this.currentParentTab.linkedBrowser.closest(".browserSidebarContainer").classList.remove("zen-glance-overlay");
this.currentParentTab.linkedBrowser.zenModeActive = true;
this.#currentBrowser.zenModeActive = true;
this.currentParentTab.linkedBrowser.docShellIsActive = true;
this.#currentBrowser.docShellIsActive = true;
this.#currentBrowser.setAttribute("zen-glance-selected", true);
this.currentParentTab._visuallySelected = true;
this.overlay.classList.add("deck-selected");
this._duringOpening = false;
}
quickCloseGlance({ closeCurrentTab = true, closeParentTab = true } = {}) {
const parentHasBrowser = !!(this.currentParentTab.linkedBrowser);
if (parentHasBrowser) {
if (closeParentTab) {
this.currentParentTab.linkedBrowser.closest(".browserSidebarContainer").classList.remove("deck-selected");
}
this.currentParentTab.linkedBrowser.zenModeActive = false;
}
this.#currentBrowser.zenModeActive = false;
if (closeParentTab && parentHasBrowser) {
this.currentParentTab.linkedBrowser.docShellIsActive = false;
}
if (closeCurrentTab) {
this.#currentBrowser.docShellIsActive = false;
this.overlay.classList.remove("deck-selected");
}
if (!this.currentParentTab._visuallySelected && closeParentTab) {
this.currentParentTab._visuallySelected = false;
}
this.#currentBrowser.removeAttribute("zen-glance-selected");
if (parentHasBrowser) {
this.currentParentTab.linkedBrowser.closest(".browserSidebarContainer").classList.remove("zen-glance-background");
}
}
onLocationChange(_) {
if (this._duringOpening) {
return;
}
if (gBrowser.selectedTab === this.#currentTab && !this.animatingOpen && !this._duringOpening && this.#currentBrowser) {
this.quickOpenGlance();
return;
}
if (gBrowser.selectedTab === this.currentParentTab && this.#currentBrowser) {
this.quickOpenGlance();
} else if ((!this.animatingFullOpen || this.animatingOpen) && this.#currentBrowser) {
this.closeGlance();
}
}
onTabClose(event) {
if (event.target === this.currentParentTab) {
this.closeGlance({ onTabClose: true });
}
}
fullyOpenGlance() {
gBrowser._insertTabAtIndex(this.#currentTab, {
index: this.#currentTab._tPos + 1,
});
this.animatingFullOpen = true;
this.currentParentTab._visuallySelected = false;
this.browserWrapper.removeAttribute("has-finished-animation");
this.browserWrapper.setAttribute("animate-full", true);
this.#currentTab.removeAttribute("zen-glance-tab");
gBrowser.selectedTab = this.#currentTab;
this.currentParentTab.linkedBrowser.closest(".browserSidebarContainer").classList.remove("zen-glance-background");
setTimeout(() => {
window.requestAnimationFrame(() => {
this.browserWrapper.setAttribute("animate-full-end", true);
setTimeout(() => {
this.animatingFullOpen = false;
this.closeGlance({ noAnimation: true });
}, 600);
});
}, 300);
}
openGlanceForBookmark(event) {
const activationMethod = Services.prefs.getStringPref('zen.glance.activation-method', 'ctrl');
if (activationMethod === 'ctrl' && !event.ctrlKey) {
return;
} else if (activationMethod === 'alt' && !event.altKey) {
return;
} else if (activationMethod === 'shift' && !event.shiftKey) {
return;
} else if (activationMethod === 'meta' && !event.metaKey) {
return;
}else if (activationMethod === 'mantain' || typeof activationMethod === 'undefined') {
return;
}
event.preventDefault();
event.stopPropagation();
const rect = event.target.getBoundingClientRect();
const data = {
url: event.target._placesNode.uri,
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
};
this.openGlance(data);
return false;
}
}
window.gZenGlanceManager = new ZenGlanceManager();
function registerWindowActors() {
if (Services.prefs.getBoolPref("zen.glance.enabled", true)) {
gZenActorsManager.addJSWindowActor("ZenGlance", {
parent: {
esModuleURI: "chrome://browser/content/zen-components/actors/ZenGlanceParent.sys.mjs",
},
child: {
esModuleURI: "chrome://browser/content/zen-components/actors/ZenGlanceChild.sys.mjs",
events: {
DOMContentLoaded: {},
},
},
});
}
}
registerWindowActors();
}

View file

@ -0,0 +1,722 @@
{
class ZenThemePicker extends ZenMultiWindowFeature {
static GRADIENT_IMAGE_URL = 'chrome://browser/content/zen-images/gradient.png';
static GRADIENT_DISPLAY_URL = 'chrome://browser/content/zen-images/gradient-display.png';
static MAX_DOTS = 5;
currentOpacity = 0.5;
currentRotation = 45;
numberOfDots = 0;
constructor() {
super();
if (!Services.prefs.getBoolPref('zen.theme.gradient', true) || !ZenWorkspaces.shouldHaveWorkspaces) {
return;
}
this.dragStartPosition = null;
ChromeUtils.defineLazyGetter(this, 'panel', () => document.getElementById('PanelUI-zen-gradient-generator'));
ChromeUtils.defineLazyGetter(this, 'toolbox', () => document.getElementById('TabsToolbar'));
ChromeUtils.defineLazyGetter(this, 'customColorInput', () => document.getElementById('PanelUI-zen-gradient-generator-custom-input'));
ChromeUtils.defineLazyGetter(this, 'customColorList', () => document.getElementById('PanelUI-zen-gradient-generator-custom-list'));
XPCOMUtils.defineLazyPreferenceGetter(
this,
'allowWorkspaceColors',
'zen.theme.color-prefs.use-workspace-colors',
true,
this.onDarkModeChange.bind(this)
)
this.initRotation();
this.initCanvas();
ZenWorkspaces.addChangeListeners(this.onWorkspaceChange.bind(this));
window.matchMedia('(prefers-color-scheme: dark)').addListener(this.onDarkModeChange.bind(this));
}
get isDarkMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
async onDarkModeChange(event, skipUpdate = false) {
const currentWorkspace = await ZenWorkspaces.getActiveWorkspace();
this.onWorkspaceChange(currentWorkspace, skipUpdate);
}
initContextMenu() {
const menu = window.MozXULElement.parseXULToFragment(`
<menuitem id="zenToolbarThemePicker"
data-lazy-l10n-id="zen-workspaces-change-gradient"
oncommand="gZenThemePicker.openThemePicker(event);"/>
`);
document.getElementById('toolbar-context-customize').before(menu);
}
openThemePicker(event) {
PanelMultiView.openPopup(this.panel, this.toolbox, {
position: 'topright topleft',
triggerEvent: event,
});
}
initCanvas() {
this.image = new Image();
this.image.src = ZenThemePicker.GRADIENT_IMAGE_URL;
this.canvas = document.createElement('canvas');
this.panel.appendChild(this.canvas);
this.canvasCtx = this.canvas.getContext('2d');
// wait for the image to load
this.image.onload = this.onImageLoad.bind(this);
}
onImageLoad() {
// resize the image to fit the panel
const imageSize = 300 - 20; // 20 is the padding (10px)
const scale = imageSize / Math.max(this.image.width, this.image.height);
this.image.width *= scale;
this.image.height *= scale;
this.canvas.width = this.image.width;
this.canvas.height = this.image.height;
this.canvasCtx.drawImage(this.image, 0, 0);
this.canvas.setAttribute('hidden', 'true');
// Call the rest of the initialization
this.initContextMenu();
this.initThemePicker();
this._hasInitialized = true;
this.onDarkModeChange(null);
}
initRotation() {
this.rotationInput = document.getElementById('PanelUI-zen-gradient-degrees');
this.rotationInputDot = this.rotationInput.querySelector('.dot');
this.rotationInputText = this.rotationInput.querySelector('.text');
this.rotationInputDot.addEventListener('mousedown', this.onRotationMouseDown.bind(this));
this.rotationInput.addEventListener('wheel', this.onRotationWheel.bind(this));
}
onRotationWheel(event) {
event.preventDefault();
const delta = event.deltaY;
const degrees = this.currentRotation + (delta > 0 ? 10 : -10);
this.setRotationInput(degrees);
this.updateCurrentWorkspace();
}
onRotationMouseDown(event) {
event.preventDefault();
this.rotationDragging = true;
this.rotationInputDot.style.zIndex = 2;
this.rotationInputDot.classList.add('dragging');
document.addEventListener('mousemove', this.onRotationMouseMove.bind(this));
document.addEventListener('mouseup', this.onRotationMouseUp.bind(this));
}
onRotationMouseUp(event) {
this.rotationDragging = false;
this.rotationInputDot.style.zIndex = 1;
this.rotationInputDot.classList.remove('dragging');
document.removeEventListener('mousemove', this.onRotationMouseMove.bind(this));
document.removeEventListener('mouseup', this.onRotationMouseUp.bind(this));
}
onRotationMouseMove(event) {
if (this.rotationDragging) {
event.preventDefault();
const rect = this.rotationInput.getBoundingClientRect();
// Make the dot follow the mouse in a circle, it can't go outside or inside the circle
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const angle = Math.atan2(event.clientY - centerY, event.clientX - centerX);
const distance = Math.sqrt((event.clientX - centerX) ** 2 + (event.clientY - centerY) ** 2);
const radius = rect.width / 2;
let x = centerX + Math.cos(angle) * radius;
let y = centerY + Math.sin(angle) * radius;
if (distance > radius) {
x = event.clientX;
y = event.clientY;
}
const degrees = Math.round(Math.atan2(y - centerY, x - centerX) * 180 / Math.PI);
this.setRotationInput(degrees);
this.updateCurrentWorkspace();
}
}
setRotationInput(degrees) {
let fixedRotation = degrees;
while (fixedRotation < 0) {
fixedRotation += 360;
}
while (fixedRotation >= 360) {
fixedRotation -= 360;
}
this.currentRotation = degrees;
this.rotationInputDot.style.transform = `rotate(${degrees - 20}deg)`;
this.rotationInputText.textContent = `${fixedRotation}°`;
}
initThemePicker() {
const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
themePicker.style.setProperty('--zen-theme-picker-gradient-image', `url(${ZenThemePicker.GRADIENT_DISPLAY_URL})`);
themePicker.addEventListener('mousemove', this.onDotMouseMove.bind(this));
themePicker.addEventListener('mouseup', this.onDotMouseUp.bind(this));
themePicker.addEventListener('click', this.onThemePickerClick.bind(this));
}
calculateInitialPosition(color) {
const [r, g, b] = color.c;
const imageData = this.canvasCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
// Find all pixels that are at least 90% similar to the color
const similarPixels = [];
for (let i = 0; i < imageData.data.length; i += 4) {
const pixelR = imageData.data[i];
const pixelG = imageData.data[i + 1];
const pixelB = imageData.data[i + 2];
if (Math.abs(r - pixelR) < 25 && Math.abs(g - pixelG) < 25 && Math.abs(b - pixelB) < 25) {
similarPixels.push(i);
}
}
// Check if there's an exact match
for (const pixel of similarPixels) {
const x = (pixel / 4) % this.canvas.width;
const y = Math.floor((pixel / 4) / this.canvas.width);
const pixelColor = this.getColorFromPosition(x, y);
if (pixelColor[0] === r && pixelColor[1] === g && pixelColor[2] === b) {
return {x: x / this.canvas.width, y: y / this.canvas.height};
}
}
// If there's no exact match, return the first similar pixel
const pixel = similarPixels[0];
const x = (pixel / 4) % this.canvas.width;
const y = Math.floor((pixel / 4) / this.canvas.width);
return {x: x / this.canvas.width, y: y / this.canvas.height};
}
getColorFromPosition(x, y) {
// get the color from the x and y from the image
const imageData = this.canvasCtx.getImageData(x, y, 1, 1);
return imageData.data;
}
createDot(color, fromWorkspace = false) {
if (color.isCustom) {
this.addColorToCustomList(color.c);
}
const [r, g, b] = color.c;
const dot = document.createElement('div');
dot.classList.add('zen-theme-picker-dot');
if (color.isCustom) {
if (!color.c) {
return;
}
dot.classList.add('custom');
dot.style.opacity = 0;
dot.style.setProperty('--zen-theme-picker-dot-color', color.c);
} else {
dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${r}, ${g}, ${b})`);
const { x, y } = this.calculateInitialPosition(color);
dot.style.left = `${x * 100}%`;
dot.style.top = `${y * 100}%`;
dot.addEventListener('mousedown', this.onDotMouseDown.bind(this));
}
this.panel.querySelector('.zen-theme-picker-gradient').appendChild(dot);
if (!fromWorkspace) {
this.updateCurrentWorkspace(true);
}
}
onThemePickerClick(event) {
event.preventDefault();
if (event.button !== 0 || this.dragging ) return;
const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
const rect = gradient.getBoundingClientRect();
const padding = 90; // each side
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const radius = (rect.width - padding) / 2;
let pixelX = event.clientX;
let pixelY = event.clientY;
// Check if the click is within the circle
const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
if (distance > radius) {
return; // Don't create a dot if clicking outside the circle
}
// Check if we clicked on an existing dot
const clickedElement = event.target;
const isExistingDot = clickedElement.classList.contains('zen-theme-picker-dot');
// Only proceed if not clicking on an existing dot
if (!isExistingDot) {
const relativeX = event.clientX - rect.left;
const relativeY = event.clientY - rect.top;
const color = this.getColorFromPosition(relativeX, relativeY);
// Create new dot
const dot = document.createElement('div');
dot.classList.add('zen-theme-picker-dot');
dot.addEventListener('mousedown', this.onDotMouseDown.bind(this));
dot.style.left = `${relativeX}px`;
dot.style.top = `${relativeY}px`;
dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${color[0]}, ${color[1]}, ${color[2]})`);
gradient.appendChild(dot);
this.updateCurrentWorkspace(true);
}
}
onDotMouseDown(event) {
event.preventDefault();
if (event.button === 2) {
return;
}
this.dragging = true;
this.draggedDot = event.target;
this.draggedDot.style.zIndex = 1;
this.draggedDot.classList.add('dragging');
// Store the starting position of the drag
this.dragStartPosition = {
x: event.clientX,
y: event.clientY
};
}
onDotMouseMove(event) {
if (this.dragging) {
event.preventDefault();
const rect = this.panel.querySelector('.zen-theme-picker-gradient').getBoundingClientRect();
const padding = 90; // each side
// do NOT let the ball be draged outside of an imaginary circle. You can drag it anywhere inside the circle
// if the distance between the center of the circle and the dragged ball is bigger than the radius, then the ball
// should be placed on the edge of the circle. If it's inside the circle, then the ball just follows the mouse
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const radius = (rect.width - padding) / 2;
let pixelX = event.clientX;
let pixelY = event.clientY;
const distance = Math.sqrt((pixelX - centerX) **2 + (pixelY - centerY) **2);
if (distance > radius) {
const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
pixelX = centerX + Math.cos(angle) * radius;
pixelY = centerY + Math.sin(angle) * radius;
}
// set the location of the dot in pixels
const relativeX = pixelX - rect.left;
const relativeY = pixelY - rect.top;
this.draggedDot.style.left = `${relativeX}px`;
this.draggedDot.style.top = `${relativeY}px`;
const color = this.getColorFromPosition(relativeX, relativeY);
this.draggedDot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${color[0]}, ${color[1]}, ${color[2]})`);
this.updateCurrentWorkspace();
}
}
addColorToCustomList(color) {
const listItems = window.MozXULElement.parseXULToFragment(`
<hbox class="zen-theme-picker-custom-list-item">
<html:div class="zen-theme-picker-dot-custom"></html:div>
<label class="zen-theme-picker-custom-list-item-label"></label>
<toolbarbutton class="zen-theme-picker-custom-list-item-remove toolbarbutton-1" oncommand="gZenThemePicker.removeCustomColor(event);"></toolbarbutton>
</hbox>
`);
listItems.querySelector('.zen-theme-picker-custom-list-item').setAttribute('data-color', color);
listItems.querySelector('.zen-theme-picker-dot-custom').style.setProperty('--zen-theme-picker-dot-color', color);
listItems.querySelector('.zen-theme-picker-custom-list-item-label').textContent = color;
this.customColorList.appendChild(listItems);
}
async addCustomColor() {
const color = this.customColorInput.value;
if (!color) {
return;
}
// can be any color format, we just add it to the list as a dot, but hidden
const dot = document.createElement('div');
dot.classList.add('zen-theme-picker-dot', 'hidden', 'custom');
dot.style.opacity = 0;
dot.style.setProperty('--zen-theme-picker-dot-color', color);
this.panel.querySelector('.zen-theme-picker-gradient').appendChild(dot);
this.customColorInput.value = '';
await this.updateCurrentWorkspace();
}
onThemePickerClick(event) {
event.preventDefault();
if (event.button !== 0 || this.dragging) return;
const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
const rect = gradient.getBoundingClientRect();
const padding = 90; // each side
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const radius = (rect.width - padding) / 2;
let pixelX = event.clientX;
let pixelY = event.clientY;
// Check if the click is within the circle
const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
if (distance > radius) {
return;
}
const clickedElement = event.target;
const isExistingDot = clickedElement.classList.contains('zen-theme-picker-dot');
if (!isExistingDot && this.numberOfDots < ZenThemePicker.MAX_DOTS) {
const relativeX = event.clientX - rect.left;
const relativeY = event.clientY - rect.top;
const color = this.getColorFromPosition(relativeX, relativeY);
const dot = document.createElement('div');
dot.classList.add('zen-theme-picker-dot');
dot.addEventListener('mousedown', this.onDotMouseDown.bind(this));
dot.style.left = `${relativeX}px`;
dot.style.top = `${relativeY}px`;
dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${color[0]}, ${color[1]}, ${color[2]})`);
gradient.appendChild(dot);
this.updateCurrentWorkspace(true);
}
}
onDotMouseDown(event) {
event.preventDefault();
if (event.button === 2) {
return;
}
this.dragging = true;
this.draggedDot = event.target;
this.draggedDot.style.zIndex = 1;
this.draggedDot.classList.add('dragging');
// Store the starting position of the drag
this.dragStartPosition = {
x: event.clientX,
y: event.clientY
};
}
onDotMouseUp(event) {
if (event.button === 2) {
if (!event.target.classList.contains('zen-theme-picker-dot')) {
return;
}
event.target.remove();
this.updateCurrentWorkspace();
this.numberOfDots--;
return;
}
if (this.dragging) {
event.preventDefault();
event.stopPropagation();
this.dragging = false;
this.draggedDot.style.zIndex = 1;
this.draggedDot.classList.remove('dragging');
this.draggedDot = null;
this.dragStartPosition = null; // Reset the drag start position
return;
}
this.numberOfDots = this.panel.querySelectorAll('.zen-theme-picker-dot').length;
}
themedColors(colors) {
const isDarkMode = this.isDarkMode;
const factor = isDarkMode ? 0.5 : 1.1;
return colors.map(color => {
return {
c: color.isCustom ? color.c : [
Math.min(255, color.c[0] * factor),
Math.min(255, color.c[1] * factor),
Math.min(255, color.c[2] * factor),
],
isCustom: color.isCustom,
}
});
}
onOpacityChange(event) {
this.currentOpacity = event.target.value;
this.updateCurrentWorkspace();
}
onTextureChange(event) {
this.currentTexture = event.target.value;
this.updateCurrentWorkspace();
}
getSingleRGBColor(color) {
if (color.isCustom) {
return color.c;
}
return `color-mix(in srgb, rgb(${color.c[0]}, ${color.c[1]}, ${color.c[2]}) ${this.currentOpacity * 100}%, var(--zen-themed-toolbar-bg) ${(1 - this.currentOpacity) * 100}%)`;
}
getGradient(colors) {
const themedColors = this.themedColors(colors);
if (themedColors.length === 0) {
return "var(--zen-themed-toolbar-bg)";
} else if (themedColors.length === 1) {
return this.getSingleRGBColor(themedColors[0]);
}
return `linear-gradient(${this.currentRotation}deg, ${themedColors.map(color => this.getSingleRGBColor(color)).join(', ')})`;
}
getTheme(colors, opacity = 0.5, rotation = 45, texture = 0) {
return {
type: 'gradient',
gradientColors: colors.filter(color => color), // remove undefined
opacity,
rotation,
texture,
};
}
//TODO: add a better noise system that adds noise not just changes transparency
updateNoise(texture) {
const wrapper = document.getElementById('zen-main-app-wrapper');
wrapper.style.setProperty('--zen-grainy-background-opacity', texture);
}
hexToRgb(hex) {
if (hex.startsWith('#')) {
hex = hex.substring(1);
}
if (hex.length === 3) {
hex = hex.split('').map(char => char + char).join('');
}
return [
parseInt(hex.substring(0, 2), 16),
parseInt(hex.substring(2, 4), 16),
parseInt(hex.substring(4, 6), 16),
];
}
pSBC=(p,c0,c1,l)=>{
let r,g,b,P,f,t,h,i=parseInt,m=Math.round,a=typeof(c1)=="string";
if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null;
if(!this.pSBCr)this.pSBCr=(d)=>{
let n=d.length,x={};
if(n>9){
[r,g,b,a]=d=d.split(","),n=d.length;
if(n<3||n>4)return null;
x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1
}else{
if(n==8||n==6||n<4)return null;
if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:"");
d=i(d.slice(1),16);
if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=m((d&255)/0.255)/1000;
else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1
}return x};
h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=this.pSBCr(c0),P=p<0,t=c1&&c1!="c"?this.pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p;
if(!f||!t)return null;
if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b);
else r=m((P*f.r**2+p*t.r**2)**0.5),g=m((P*f.g**2+p*t.g**2)**0.5),b=m((P*f.b**2+p*t.b**2)**0.5);
a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0;
if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")";
else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2)
}
getMostDominantColor(allColors) {
const colors = this.themedColors(allColors);
const themedColors = colors.filter(color => !color.isCustom);
if (themedColors.length === 0 || !this.allowWorkspaceColors) {
return null;
}
// get the most dominant color in the gradient
let dominantColor = themedColors[0].c;
let dominantColorCount = 0;
for (const color of themedColors) {
const count = themedColors.filter(c => c.c[0] === color.c[0] && c.c[1] === color.c[1] && c.c[2] === color.c[2]).length;
if (count > dominantColorCount) {
dominantColorCount = count;
dominantColor = color.c;
}
}
const result = this.pSBC(
this.isDarkMode ? 0.5 : -0.5,
`rgb(${dominantColor[0]}, ${dominantColor[1]}, ${dominantColor[2]})`);
return result?.match(/\d+/g).map(Number);
}
async onWorkspaceChange(workspace, skipUpdate = false, theme = null) {
const uuid = workspace.uuid;
// Use theme from workspace object or passed theme
let workspaceTheme = theme || workspace.theme;
await this.foreachWindowAsActive(async (browser) => {
if (!browser.gZenThemePicker._hasInitialized) {
return;
}
// Do not rebuild if the workspace is not the same as the current one
const windowWorkspace = await browser.ZenWorkspaces.getActiveWorkspace();
if (windowWorkspace.uuid !== uuid && theme !== null) {
return;
}
// get the theme from the window
workspaceTheme = theme || windowWorkspace.theme;
const appWrapper = browser.document.getElementById('zen-main-app-wrapper');
if (!skipUpdate) {
appWrapper.removeAttribute('animating');
appWrapper.setAttribute('animating', 'true');
browser.document.body.style.setProperty('--zen-main-browser-background-old',
browser.document.body.style.getPropertyValue('--zen-main-browser-background')
);
browser.window.requestAnimationFrame(() => {
setTimeout(() => {
appWrapper.removeAttribute('animating');
}, 500);
});
}
browser.gZenThemePicker.resetCustomColorList();
if (!workspaceTheme || workspaceTheme.type !== 'gradient') {
browser.document.documentElement.style.removeProperty('--zen-main-browser-background');
browser.gZenThemePicker.updateNoise(0);
if (!skipUpdate) {
for (const dot of browser.gZenThemePicker.panel.querySelectorAll('.zen-theme-picker-dot')) {
dot.remove();
}
}
browser.document.documentElement.style.setProperty('--zen-primary-color', this.getNativeAccentColor());
return;
}
browser.gZenThemePicker.currentOpacity = workspaceTheme.opacity ?? 0.5;
browser.gZenThemePicker.currentRotation = workspaceTheme.rotation ?? 45;
browser.gZenThemePicker.currentTexture = workspaceTheme.texture ?? 0;
browser.gZenThemePicker.numberOfDots = workspaceTheme.gradientColors.length;
browser.document.getElementById('PanelUI-zen-gradient-generator-opacity').value = browser.gZenThemePicker.currentOpacity;
browser.document.getElementById('PanelUI-zen-gradient-generator-texture').value = browser.gZenThemePicker.currentTexture;
browser.gZenThemePicker.setRotationInput(browser.gZenThemePicker.currentRotation);
const gradient = browser.gZenThemePicker.getGradient(workspaceTheme.gradientColors);
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
for (const dot of workspaceTheme.gradientColors) {
if (dot.isCustom) {
browser.gZenThemePicker.addColorToCustomList(dot.c);
}
}
browser.document.documentElement.style.setProperty('--zen-main-browser-background', gradient);
const dominantColor = this.getMostDominantColor(workspaceTheme.gradientColors);
if (dominantColor) {
browser.document.documentElement.style.setProperty('--zen-primary-color', `rgb(${dominantColor[0]}, ${dominantColor[1]}, ${dominantColor[2]})`);
}
if (!skipUpdate) {
browser.gZenThemePicker.recalculateDots(workspaceTheme.gradientColors);
}
});
}
getNativeAccentColor() {
return Services.prefs.getStringPref('zen.theme.accent-color');
}
resetCustomColorList() {
this.customColorList.innerHTML = '';
}
removeCustomColor(event) {
const target = event.target.closest('.zen-theme-picker-custom-list-item');
const color = target.getAttribute('data-color');
const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
for (const dot of dots) {
if (dot.style.getPropertyValue('--zen-theme-picker-dot-color') === color) {
dot.remove();
break;
}
}
target.remove();
this.updateCurrentWorkspace();
}
recalculateDots(colors) {
const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
for (let i = 0; i < colors.length; i++) {
dots[i]?.remove();
}
for (const color of colors) {
this.createDot(color, true);
}
}
async updateCurrentWorkspace(skipSave = true) {
this.updated = skipSave;
const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
const colors = Array.from(dots).map(dot => {
const color = dot.style.getPropertyValue('--zen-theme-picker-dot-color');
if (color === 'undefined') {
return;
}
const isCustom = dot.classList.contains('custom');
return {c: isCustom ? color : color.match(/\d+/g).map(Number), isCustom};
});
const gradient = this.getTheme(colors, this.currentOpacity, this.currentRotation, this.currentTexture);
let currentWorkspace = await ZenWorkspaces.getActiveWorkspace();
if(!skipSave) {
await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient);
await ZenWorkspaces._propagateWorkspaceData();
ConfirmationHint.show(document.getElementById("PanelUI-menu-button"), "zen-panel-ui-gradient-generator-saved-message");
currentWorkspace = await ZenWorkspaces.getActiveWorkspace();
}
await this.onWorkspaceChange(currentWorkspace, true, skipSave ? gradient : null);
}
async handlePanelClose() {
if(this.updated) {
await this.updateCurrentWorkspace(false);
}
}
}
window.ZenThemePicker = ZenThemePicker;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,422 @@
{
const lazy = {};
class ZenPinnedTabsObserver {
static ALL_EVENTS = ['TabPinned', 'TabUnpinned', 'TabClose'];
#listeners = [];
constructor() {
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenPinnedTabRestorePinnedTabsToPinnedUrl', 'zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url', false);
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenPinnedTabCloseShortcutBehavior', 'zen.pinned-tab-manager.close-shortcut-behavior', 'switch');
ChromeUtils.defineESModuleGetters(lazy, {E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs"});
this.#listenPinnedTabEvents();
}
#listenPinnedTabEvents() {
const eventListener = this.#eventListener.bind(this);
for (const event of ZenPinnedTabsObserver.ALL_EVENTS) {
window.addEventListener(event, eventListener);
}
window.addEventListener('unload', () => {
for (const event of ZenPinnedTabsObserver.ALL_EVENTS) {
window.removeEventListener(event, eventListener);
}
});
}
#eventListener(event) {
for (const listener of this.#listeners) {
listener(event.type, event);
}
}
addPinnedTabListener(listener) {
this.#listeners.push(listener);
}
}
class ZenPinnedTabManager extends ZenDOMOperatedFeature {
init() {
this.observer = new ZenPinnedTabsObserver();
this._initClosePinnedTabShortcut();
this._insertItemsIntoTabContextMenu();
this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this));
this._zenClickEventListener = this._onTabClick.bind(this);
}
async initTabs() {
await ZenPinnedTabsStorage.init();
await this._refreshPinnedTabs();
}
async _refreshPinnedTabs() {
await this._initializePinsCache();
this._initializePinnedTabs();
}
async _initializePinsCache() {
try {
// Get pin data
const pins = await ZenPinnedTabsStorage.getPins();
// Enhance pins with favicons
const enhancedPins = await Promise.all(pins.map(async pin => {
try {
const faviconData = await PlacesUtils.promiseFaviconData(pin.url);
return {
...pin,
iconUrl: faviconData?.uri?.spec || null
};
} catch(ex) {
// If favicon fetch fails, continue without icon
return {
...pin,
iconUrl: null
};
}
}));
this._pinsCache = enhancedPins.sort((a, b) => {
if (!a.workspaceUuid && b.workspaceUuid) return -1;
if (a.workspaceUuid && !b.workspaceUuid) return 1;
return 0;
});
} catch (ex) {
console.error("Failed to initialize pins cache:", ex);
this._pinsCache = [];
}
return this._pinsCache;
}
_initializePinnedTabs() {
const pins = this._pinsCache;
if (!pins?.length) {
// If there are no pins, we should remove any existing pinned tabs
for (let tab of gBrowser.tabs) {
if (tab.pinned && !tab.getAttribute("zen-pin-id")) {
gBrowser.removeTab(tab);
}
}
return;
}
const activeTab = gBrowser.selectedTab;
const pinnedTabsByUUID = new Map();
const pinsToCreate = new Set(pins.map(p => p.uuid));
// First pass: identify existing tabs and remove those without pins
for (let tab of gBrowser.tabs) {
const pinId = tab.getAttribute("zen-pin-id");
if (!pinId) {
continue;
}
if (pinsToCreate.has(pinId)) {
// This is a valid pinned tab that matches a pin
pinnedTabsByUUID.set(pinId, tab);
pinsToCreate.delete(pinId);
} else {
// This is a pinned tab that no longer has a corresponding pin
gBrowser.removeTab(tab);
}
}
// Second pass: create new tabs for pins that don't have tabs
for (let pin of pins) {
if (!pinsToCreate.has(pin.uuid)) {
continue; // Skip pins that already have tabs
}
let newTab = gBrowser.addTrustedTab(pin.url, {
skipAnimation: true,
userContextId: pin.containerTabId || 0,
allowInheritPrincipal: false,
createLazyBrowser: true,
skipLoad: true,
});
// Set the favicon from cache
if (!!pin.iconUrl) {
// TODO: Figure out if there is a better way -
// calling gBrowser.setIcon messes shit up and should be avoided. I think this works for now.
newTab.setAttribute("image", pin.iconUrl);
}
newTab.setAttribute("zen-pin-id", pin.uuid);
gBrowser.setInitialTabTitle(newTab, pin.title);
if (pin.workspaceUuid) {
newTab.setAttribute("zen-workspace-id", pin.workspaceUuid);
}
gBrowser.pinTab(newTab);
}
// Restore active tab
if (!activeTab.closing) {
gBrowser.selectedTab = activeTab;
}
}
_onPinnedTabEvent(action, event) {
const tab = event.target;
switch (action) {
case "TabPinned":
tab._zenClickEventListener = this._zenClickEventListener;
tab.addEventListener("click", tab._zenClickEventListener);
this._setPinnedAttributes(tab);
break;
case "TabUnpinned":
this._removePinnedAttributes(tab);
if (tab._zenClickEventListener) {
tab.removeEventListener("click", tab._zenClickEventListener);
delete tab._zenClickEventListener;
}
break;
// TODO: Do this in a better way. Closing a second window could trigger remove tab and delete it from db
// case "TabClose":
// this._removePinnedAttributes(tab);
// break;
default:
console.warn('ZenPinnedTabManager: Unhandled tab event', action);
break;
}
}
_onTabClick(e) {
const tab = e.target;
if (e.button === 1) {
this._onCloseTabShortcut(e, tab);
}
}
async resetPinnedTab(tab) {
if (!tab) {
tab = TabContextMenu.contextTab;
}
if (!tab || !tab.pinned) {
return;
}
await this._resetTabToStoredState(tab);
}
async replacePinnedUrlWithCurrent() {
const tab = TabContextMenu.contextTab;
if (!tab || !tab.pinned || !tab.getAttribute("zen-pin-id")) {
return;
}
const browser = tab.linkedBrowser;
const pin = this._pinsCache.find(pin => pin.uuid === tab.getAttribute("zen-pin-id"));
if (!pin) {
return;
}
pin.title = tab.label || browser.contentTitle;
pin.url = browser.currentURI.spec;
pin.workspaceUuid = tab.getAttribute("zen-workspace-id");
pin.userContextId = tab.getAttribute("userContextId");
await ZenPinnedTabsStorage.savePin(pin);
await this._refreshPinnedTabs();
}
async _setPinnedAttributes(tab) {
if (tab.hasAttribute("zen-pin-id")) {
return;
}
const browser = tab.linkedBrowser;
const uuid = gZenUIManager.generateUuidv4();
await ZenPinnedTabsStorage.savePin({
uuid,
title: tab.label || browser.contentTitle,
url: browser.currentURI.spec,
containerTabId: tab.getAttribute("userContextId"),
workspaceUuid: tab.getAttribute("zen-workspace-id")
});
tab.setAttribute("zen-pin-id", uuid);
await this._refreshPinnedTabs();
}
async _removePinnedAttributes(tab) {
if(!tab.getAttribute("zen-pin-id")) {
return;
}
await ZenPinnedTabsStorage.removePin(tab.getAttribute("zen-pin-id"));
tab.removeAttribute("zen-pin-id");
if(!tab.hasAttribute("zen-workspace-id") && ZenWorkspaces.workspaceEnabled) {
const workspace = await ZenWorkspaces.getActiveWorkspace();
tab.setAttribute("zen-workspace-id", workspace.uuid);
}
await this._refreshPinnedTabs();
}
_initClosePinnedTabShortcut() {
let cmdClose = document.getElementById('cmd_close');
if (cmdClose) {
cmdClose.addEventListener('command', this._onCloseTabShortcut.bind(this));
}
}
_onCloseTabShortcut(event, selectedTab = gBrowser.selectedTab) {
if (
!selectedTab?.pinned
) {
return;
}
event.stopPropagation();
event.preventDefault();
const behavior = lazy.zenPinnedTabCloseShortcutBehavior;
switch (behavior) {
case 'close':
gBrowser.removeTab(selectedTab, { animate: true });
break;
case 'reset-unload-switch':
case 'unload-switch':
case 'reset-switch':
case 'switch':
this._handleTabSwitch(selectedTab);
if (behavior.includes('reset')) {
this._resetTabToStoredState(selectedTab);
}
if (behavior.includes('unload')) {
gBrowser.discardBrowser(selectedTab);
}
break;
case 'reset':
this._resetTabToStoredState(selectedTab);
break;
default:
return;
}
}
_handleTabSwitch(selectedTab) {
if(selectedTab !== gBrowser.selectedTab) {
return;
}
const findNextTab = (direction) =>
gBrowser.tabContainer.findNextTab(selectedTab, {
direction,
filter: tab => !tab.hidden && !tab.pinned,
});
let nextTab = findNextTab(1) || findNextTab(-1);
if (!nextTab) {
ZenWorkspaces._createNewTabForWorkspace({ uuid: ZenWorkspaces.activeWorkspace });
nextTab = findNextTab(1) || findNextTab(-1);
}
if (nextTab) {
gBrowser.selectedTab = nextTab;
}
}
async _resetTabToStoredState(tab) {
const id = tab.getAttribute("zen-pin-id");
if (!id) {
return;
}
const pin = this._pinsCache.find(pin => pin.uuid === id);
if (pin) {
const tabState = SessionStore.getTabState(tab);
const state = JSON.parse(tabState);
const icon = await PlacesUtils.promiseFaviconData(pin.url);
state.entries = [{
url: pin.url,
title: pin.title,
triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL
}];
state.image = icon;
state.index = 0;
SessionStore.setTabState(tab, state);
}
}
_addGlobalPin() {
const tab = TabContextMenu.contextTab;
if (!tab || tab.pinned) {
return;
}
tab.removeAttribute("zen-workspace-id");
gBrowser.pinTab(tab);
}
_insertItemsIntoTabContextMenu() {
const elements = window.MozXULElement.parseXULToFragment(`
<menuseparator id="context_zen-pinned-tab-separator" hidden="true"/>
<menuitem id="context_zen-replace-pinned-url-with-current"
data-lazy-l10n-id="tab-context-zen-replace-pinned-url-with-current"
hidden="true"
oncommand="gZenPinnedTabManager.replacePinnedUrlWithCurrent();"/>
<menuitem id="context_zen-reset-pinned-tab"
data-lazy-l10n-id="tab-context-zen-reset-pinned-tab"
hidden="true"
oncommand="gZenPinnedTabManager.resetPinnedTab();"/>
`);
document.getElementById('tabContextMenu').appendChild(elements);
const element = window.MozXULElement.parseXULToFragment(`
<menuitem id="context_zen-pin-tab-global"
data-lazy-l10n-id="pin-tab-global"
hidden="true"
oncommand="gZenPinnedTabManager._addGlobalPin();"/>
`);
document.getElementById('context_pinTab').after(element);
}
resetPinnedTabData(tabData) {
if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && tabData.pinned && tabData.zenPinnedEntry) {
tabData.entries = [JSON.parse(tabData.zenPinnedEntry)];
tabData.image = tabData.zenPinnedIcon;
tabData.index = 0;
}
}
updatePinnedTabContextMenu(contextTab) {
const isVisible = contextTab.pinned && !contextTab.multiselected;
document.getElementById("context_zen-reset-pinned-tab").hidden = !isVisible || !contextTab.getAttribute("zen-pin-id");
document.getElementById("context_zen-replace-pinned-url-with-current").hidden = !isVisible;
document.getElementById("context_zen-pin-tab-global").hidden = contextTab.pinned;
document.getElementById("context_zen-pinned-tab-separator").hidden = !isVisible;
}
}
window.gZenPinnedTabManager = new ZenPinnedTabManager();
}

View file

@ -0,0 +1,332 @@
var ZenPinnedTabsStorage = {
async init() {
console.log('ZenPinnedTabsStorage: Initializing...');
await this._ensureTable();
},
async _ensureTable() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage._ensureTable', async (db) => {
// Create the pins table if it doesn't exist
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_pins (
id INTEGER PRIMARY KEY,
uuid TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
url TEXT,
container_id INTEGER,
workspace_uuid TEXT,
position INTEGER NOT NULL DEFAULT 0,
is_essential BOOLEAN NOT NULL DEFAULT 0,
is_group BOOLEAN NOT NULL DEFAULT 0,
parent_uuid TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (parent_uuid) REFERENCES zen_pins(uuid) ON DELETE SET NULL
)
`);
// Create indices
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid)
`);
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_zen_pins_parent_uuid ON zen_pins(parent_uuid)
`);
// Create the changes tracking table if it doesn't exist
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_pins_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_pins_changes_uuid ON zen_pins_changes(uuid)
`);
});
},
/**
* Private helper method to notify observers with a list of changed UUIDs.
* @param {string} event - The observer event name.
* @param {Array<string>} uuids - Array of changed workspace UUIDs.
*/
_notifyPinsChanged(event, uuids) {
if (uuids.length === 0) return; // No changes to notify
// Convert the array of UUIDs to a JSON string
const data = JSON.stringify(uuids);
Services.obs.notifyObservers(null, event, data);
},
async savePin(pin, notifyObservers = true) {
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.savePin', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
let newPosition;
if ('position' in pin && Number.isFinite(pin.position)) {
newPosition = pin.position;
} else {
// Get the maximum position within the same parent group (or null for root level)
const maxPositionResult = await db.execute(`
SELECT MAX("position") as max_position
FROM zen_pins
WHERE COALESCE(parent_uuid, '') = COALESCE(:parent_uuid, '')
`, { parent_uuid: pin.parentUuid || null });
const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0;
newPosition = maxPosition + 1000;
}
// Insert or replace the pin
await db.executeCached(`
INSERT OR REPLACE INTO zen_pins (
uuid, title, url, container_id, workspace_uuid, position,
is_essential, is_group, parent_uuid, created_at, updated_at
) VALUES (
:uuid, :title, :url, :container_id, :workspace_uuid, :position,
:is_essential, :is_group, :parent_uuid,
COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now),
:now
)
`, {
uuid: pin.uuid,
title: pin.title,
url: pin.isGroup ? null : pin.url,
container_id: pin.containerTabId || null,
workspace_uuid: pin.workspaceUuid || null,
position: newPosition,
is_essential: pin.isEssential || false,
is_group: pin.isGroup || false,
parent_uuid: pin.parentUuid || null,
now
});
await db.execute(`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`, {
uuid: pin.uuid,
timestamp: Math.floor(now / 1000)
});
changedUUIDs.add(pin.uuid);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged("zen-pin-updated", Array.from(changedUUIDs));
}
},
async getPins() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.executeCached(`
SELECT * FROM zen_pins
ORDER BY parent_uuid NULLS FIRST, position ASC
`);
return rows.map((row) => ({
uuid: row.getResultByName('uuid'),
title: row.getResultByName('title'),
url: row.getResultByName('url'),
containerTabId: row.getResultByName('container_id'),
workspaceUuid: row.getResultByName('workspace_uuid'),
position: row.getResultByName('position'),
isEssential: Boolean(row.getResultByName('is_essential')),
isGroup: Boolean(row.getResultByName('is_group')),
parentUuid: row.getResultByName('parent_uuid')
}));
},
async getGroupChildren(groupUuid) {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.executeCached(`
SELECT * FROM zen_pins
WHERE parent_uuid = :groupUuid
ORDER BY position ASC
`, { groupUuid });
return rows.map((row) => ({
uuid: row.getResultByName('uuid'),
title: row.getResultByName('title'),
url: row.getResultByName('url'),
containerTabId: row.getResultByName('container_id'),
workspaceUuid: row.getResultByName('workspace_uuid'),
position: row.getResultByName('position'),
isEssential: Boolean(row.getResultByName('is_essential')),
isGroup: Boolean(row.getResultByName('is_group')),
parentUuid: row.getResultByName('parent_uuid')
}));
},
async removePin(uuid, notifyObservers = true) {
const changedUUIDs = [uuid];
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => {
await db.executeTransaction(async () => {
// Get all child UUIDs first for change tracking
const children = await db.execute(
`SELECT uuid FROM zen_pins WHERE parent_uuid = :uuid`,
{ uuid }
);
// Add child UUIDs to changedUUIDs array
for (const child of children) {
changedUUIDs.push(child.getResultByName('uuid'));
}
// Delete all children in a single statement
await db.execute(
`DELETE FROM zen_pins WHERE parent_uuid = :uuid`,
{ uuid }
);
// Delete the pin/group itself
await db.execute(
`DELETE FROM zen_pins WHERE uuid = :uuid`,
{ uuid }
);
// Record the changes
const now = Math.floor(Date.now() / 1000);
for (const changedUuid of changedUUIDs) {
await db.execute(`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`, {
uuid: changedUuid,
timestamp: now
});
}
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyPinsChanged("zen-pin-removed", changedUUIDs);
}
},
async wipeAllPins() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.wipeAllPins', async (db) => {
await db.execute(`DELETE FROM zen_pins`);
await db.execute(`DELETE FROM zen_pins_changes`);
await this.updateLastChangeTimestamp(db);
});
},
async markChanged(uuid) {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.markChanged', async (db) => {
const now = Date.now();
await db.execute(`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`, {
uuid,
timestamp: Math.floor(now / 1000)
});
});
},
async getChangedIDs() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT uuid, timestamp FROM zen_pins_changes
`);
const changes = {};
for (const row of rows) {
changes[row.getResultByName('uuid')] = row.getResultByName('timestamp');
}
return changes;
},
async clearChangedIDs() {
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.clearChangedIDs', async (db) => {
await db.execute(`DELETE FROM zen_pins_changes`);
});
},
shouldReorderPins(before, current, after) {
const minGap = 1; // Minimum allowed gap between positions
return (before !== null && current - before < minGap) || (after !== null && after - current < minGap);
},
async reorderAllPins(db, changedUUIDs) {
const pins = await db.execute(`
SELECT uuid
FROM zen_pins
ORDER BY position ASC
`);
for (let i = 0; i < pins.length; i++) {
const newPosition = (i + 1) * 1000; // Use large increments
await db.execute(`
UPDATE zen_pins
SET position = :newPosition
WHERE uuid = :uuid
`, { newPosition, uuid: pins[i].getResultByName('uuid') });
changedUUIDs.add(pins[i].getResultByName('uuid'));
}
},
async updateLastChangeTimestamp(db) {
const now = Date.now();
await db.execute(`
INSERT OR REPLACE INTO moz_meta (key, value)
VALUES ('zen_pins_last_change', :now)
`, { now });
},
async getLastChangeTimestamp() {
const db = await PlacesUtils.promiseDBConnection();
const result = await db.executeCached(`
SELECT value FROM moz_meta WHERE key = 'zen_pins_last_change'
`);
return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
},
async updatePinPositions(pins) {
const changedUUIDs = new Set();
await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.updatePinPositions', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
for (let i = 0; i < pins.length; i++) {
const pin = pins[i];
const newPosition = (i + 1) * 1000;
await db.execute(`
UPDATE zen_pins
SET position = :newPosition
WHERE uuid = :uuid
`, { newPosition, uuid: pin.uuid });
changedUUIDs.add(pin.uuid);
// Record the change
await db.execute(`
INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`, {
uuid: pin.uuid,
timestamp: Math.floor(now / 1000)
});
}
await this.updateLastChangeTimestamp(db);
});
});
this._notifyPinsChanged("zen-pin-updated", Array.from(changedUUIDs));
}
};

View file

@ -0,0 +1,138 @@
// 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 ZenProfileDialogUI = {
showSubView(parent, event) {
let element = parent.querySelector('.zen-side-bar-profiles-button-panel-correction') || parent;
PanelUI.showSubView('PanelUI-zen-profiles', element, event);
this._updateProfilesList();
this._updateCurentProfileId();
},
_updateProfilesList() {
let parentList = document.getElementById('PanelUI-zen-profiles-list');
this._emptyUserList(parentList);
if (this._getProfilesSize(ProfileService.profiles) <= 1) {
return;
}
parentList.appendChild(document.createElement('toolbarseparator'));
for (let profile of ProfileService.profiles) {
if (profile == ProfileService.currentProfile) {
continue;
}
let item = document.createElement('div');
item.onclick = () => this._openProfile(profile);
item.className = 'PanelUI-zen-profiles-item';
let avatar = document.createElement('img');
avatar.className = 'PanelUI-zen-profiles-item-avatar';
let name = document.createElement('div');
name.className = 'PanelUI-zen-profiles-item-name';
name.appendChild(document.createTextNode(profile.name));
name.container = true;
avatar.setAttribute('src', ZenThemeModifier._getThemedAvatar(profile.zenAvatarPath));
item.appendChild(avatar);
item.appendChild(name);
parentList.appendChild(item);
}
},
_emptyUserList(element) {
element.innerHTML = '';
},
_updateCurentProfileId() {
let currentProfile = ProfileService.currentProfile;
if (!currentProfile) return;
let nameContainer = document.getElementById('PanelUI-zen-profiles-current-name');
nameContainer.textContent = currentProfile.name;
},
_openProfile(profile) {
Services.startup.createInstanceWithProfile(profile);
},
_getProfilesSize(profiles) {
let size = 0;
for (let _ of profiles) {
size += 1;
}
return size;
},
createProfileWizard() {
// This should be rewritten in HTML eventually.
// TODO: it could be `window.browsingContext.topChromeWindow.gDialogBox.open` but it does not work with the callback?
window.browsingContext.topChromeWindow.openDialog(
'chrome://mozapps/content/profile/createProfileWizard.xhtml',
'',
'centerscreen,chrome,modal,titlebar',
ProfileService,
{
CreateProfile: async (profile) => {
try {
ProfileService.defaultProfile = profile;
this._flush();
this._openProfile(profile);
} catch (e) {
// This can happen on dev-edition.
let [title, msg] = await document.l10n.formatValues([
{ id: 'profiles-cannot-set-as-default-title' },
{ id: 'profiles-cannot-set-as-default-message' },
]);
Services.prompt.alert(window, title, msg);
}
},
}
);
},
async _flush() {
try {
ProfileService.flush();
this._updateProfilesList();
} catch (e) {
let [title, msg, button] = await document.l10n.formatValues([
{ id: 'profiles-flush-fail-title' },
{
id: e.result == Cr.NS_ERROR_DATABASE_CHANGED ? 'profiles-flush-conflict' : 'profiles-flush-failed',
},
{ id: 'profiles-flush-restart-button' },
]);
const PS = Ci.nsIPromptService;
let result = Services.prompt.confirmEx(
window,
title,
msg,
PS.BUTTON_POS_0 * PS.BUTTON_TITLE_CANCEL + PS.BUTTON_POS_1 * PS.BUTTON_TITLE_IS_STRING,
null,
button,
null,
null,
{}
);
if (result == 1) {
this._restart(false);
}
}
},
_restart(safeMode) {
let cancelQuit = Cc['@mozilla.org/supports-PRBool;1'].createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, 'quit-application-requested', 'restart');
if (cancelQuit.data) {
return;
}
let flags = Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart;
if (safeMode) {
Services.startup.restartInSafeMode(flags);
} else {
Services.startup.quit(flags);
}
},
};

View file

@ -0,0 +1,788 @@
class ZenBrowserManagerSidebar extends ZenDOMOperatedFeature {
_sidebarElement = null;
_currentPanel = null;
_lastOpenedPanel = null;
_hasChangedConfig = true;
_splitterElement = null;
_hSplitterElement = null;
_hasRegisteredPinnedClickOutside = false;
_isDragging = false;
contextTab = null;
sidebar = null;
forwardButton = null;
backButton = null;
progressListener = null;
_tabBrowserSet = new WeakMap();
tabBox;
DEFAULT_MOBILE_USER_AGENT = `Mozilla/5.0 (Android 12; Mobile; rv:129.0) Gecko/20100101 Firefox/${AppConstants.ZEN_FIREFOX_VERSION}`;
MAX_SIDEBAR_PANELS = Services.prefs.getIntPref('zen.sidebar.max-webpanels');
init() {
ChromeUtils.defineLazyGetter(this, 'sidebar', () => document.getElementById('zen-sidebar-web-panel'));
ChromeUtils.defineLazyGetter(this, 'forwardButton', () => document.getElementById('zen-sidebar-web-panel-forward'));
ChromeUtils.defineLazyGetter(this, 'backButton', () => document.getElementById('zen-sidebar-web-panel-back'));
ChromeUtils.defineLazyGetter(this, 'tabBox', () => document.getElementById('tabbrowser-tabbox'));
this.onlySafeWidthAndHeight();
this.initProgressListener();
this.update();
this.close(); // avoid caching
this.listenForPrefChanges();
this.insertIntoContextMenu();
this.addPositioningListeners();
}
onlySafeWidthAndHeight() {
const panel = document.getElementById('zen-sidebar-web-panel');
const width = panel.style.width;
const height = panel.style.height;
panel.setAttribute('style', '');
panel.style.width = width;
panel.style.height = height;
}
initProgressListener() {
this.progressListener = {
QueryInterface: ChromeUtils.generateQI(['nsIWebProgressListener', 'nsISupportsWeakReference']),
onLocationChange: function (aWebProgress, aRequest, aLocation, aFlags) {
const browser = this._getCurrentBrowser();
if (!browser) return;
const forwardDisabled = this.forwardButton.hasAttribute('disabled');
const backDisabled = this.backButton.hasAttribute('disabled');
if (browser.canGoForward === forwardDisabled) {
if (browser.canGoForward) {
this.forwardButton.removeAttribute('disabled');
} else {
this.forwardButton.setAttribute('disabled', true);
}
}
if (browser.canGoBack === backDisabled) {
if (browser.canGoBack) {
this.backButton.removeAttribute('disabled');
} else {
this.backButton.setAttribute('disabled', true);
}
}
}.bind(gZenBrowserManagerSidebar),
};
}
get sidebarData() {
let services = Services.prefs.getStringPref('zen.sidebar.data');
if (services === '') {
return {};
}
return JSON.parse(services);
}
get shouldCloseOnBlur() {
return Services.prefs.getBoolPref('zen.sidebar.close-on-blur');
}
listenForPrefChanges() {
Services.prefs.addObserver('zen.sidebar.data', this.handleEvent.bind(this));
Services.prefs.addObserver('zen.sidebar.enabled', this.handleEvent.bind(this));
this.handleEvent();
}
addPositioningListeners() {
this.sidebar
.querySelectorAll('.zen-sidebar-web-panel-splitter')
.forEach((s) => s.addEventListener('mousedown', this.handleSplitterMouseDown.bind(this)));
this.sidebarHeader.addEventListener('mousedown', this.handleDragPanel.bind(this));
window.addEventListener('resize', this.onWindowResize.bind(this));
}
handleSplitterMouseDown(mouseDownEvent) {
if (this._isDragging) return;
this._isDragging = true;
const isHorizontal = mouseDownEvent.target.getAttribute('orient') === 'horizontal';
setCursor(isHorizontal ? 'n-resize' : 'ew-resize');
const reverse = ['left', 'top'].includes(mouseDownEvent.target.getAttribute('side'));
const direction = isHorizontal ? 'height' : 'width';
const axis = isHorizontal ? 'Y' : 'X';
const computedStyle = window.getComputedStyle(this.sidebar);
const maxSize = parseInt(computedStyle.getPropertyValue(`max-${direction}`).match(/(\d+)px/)?.[1]) || Infinity;
const minSize = parseInt(computedStyle.getPropertyValue(`min-${direction}`).match(/(\d+)px/)?.[1]) || 0;
const sidebarSizeStart = this.sidebar.getBoundingClientRect()[direction];
const startPos = mouseDownEvent[`screen${axis}`];
const toAdjust = isHorizontal ? 'top' : 'left';
const sidebarPosStart = parseInt(this.sidebar.style[toAdjust].match(/\d+/));
let mouseMove = function (e) {
let mouseMoved = e[`screen${axis}`] - startPos;
if (reverse) {
mouseMoved *= -1;
}
let newSize = sidebarSizeStart + mouseMoved;
let currentMax = maxSize;
const wrapperBox = this.sidebarWrapper.getBoundingClientRect();
let maxWrapperSize = Infinity;
if (this.isFloating) {
maxWrapperSize = reverse ? sidebarPosStart + sidebarSizeStart : wrapperBox[direction] - sidebarPosStart;
}
newSize = Math.max(minSize, Math.min(currentMax, maxWrapperSize, newSize));
window.requestAnimationFrame(() => {
if (reverse) {
const actualMoved = newSize - sidebarSizeStart;
this.sidebar.style[toAdjust] = sidebarPosStart - actualMoved + 'px';
}
this.sidebar.style[direction] = `${newSize}px`;
});
}.bind(this);
document.addEventListener('mousemove', mouseMove);
document.addEventListener(
'mouseup',
() => {
document.removeEventListener('mousemove', mouseMove);
this._isDragging = false;
setCursor('auto');
},
{ once: true }
);
}
handleDragPanel(mouseDownEvent) {
if (this.sidebarHeaderButtons.find((b) => b.contains(mouseDownEvent.target))) {
return;
}
this._isDragging = true;
const startTop = this.sidebar.style.top?.match(/\d+/)?.[0] || 0;
const startLeft = this.sidebar.style.left?.match(/\d+/)?.[0] || 0;
const sidebarBBox = this.sidebar.getBoundingClientRect();
const sideBarHeight = sidebarBBox.height;
const sideBarWidth = sidebarBBox.width;
const topMouseOffset = startTop - mouseDownEvent.screenY;
const leftMouseOffset = startLeft - mouseDownEvent.screenX;
const moveListener = (mouseMoveEvent) => {
window.requestAnimationFrame(() => {
let top = mouseMoveEvent.screenY + topMouseOffset;
let left = mouseMoveEvent.screenX + leftMouseOffset;
const wrapperBounds = this.sidebarWrapper.getBoundingClientRect();
top = Math.max(0, Math.min(top, wrapperBounds.height - sideBarHeight));
left = Math.max(0, Math.min(left, wrapperBounds.width - sideBarWidth));
this.sidebar.style.top = top + 'px';
this.sidebar.style.left = left + 'px';
});
};
document.addEventListener('mousemove', moveListener);
document.addEventListener(
'mouseup',
() => {
document.removeEventListener('mousemove', moveListener);
this._isDragging = false;
},
{ once: true }
);
}
onWindowResize() {
if (!this.isFloating) return;
const top = parseInt(this.sidebar.style.top?.match(/\d+/)?.[0] || 0);
const left = parseInt(this.sidebar.style.left?.match(/\d+/)?.[0] || 0);
const wrapperRect = this.sidebarWrapper.getBoundingClientRect();
const sidebarRect = this.sidebar.getBoundingClientRect();
if (sidebarRect.height < wrapperRect.height && top + sidebarRect.height > wrapperRect.height) {
this.sidebar.style.top = wrapperRect.height - sidebarRect.height + 'px';
}
if (sidebarRect.width < wrapperRect.width && left + sidebarRect.width > wrapperRect.width) {
this.sidebar.style.left = wrapperRect.width - sidebarRect.width + 'px';
}
}
get isFloating() {
return document.getElementById('zen-sidebar-web-panel').hasAttribute('pinned');
}
handleEvent() {
this._hasChangedConfig = true;
this.update();
this._hasChangedConfig = false;
// https://stackoverflow.com/questions/11565471/removing-event-listener-which-was-added-with-bind
var clickOutsideHandler = this._handleClickOutside.bind(this);
let isFloating = this.isFloating;
if (isFloating && !this._hasRegisteredPinnedClickOutside) {
document.addEventListener('mouseup', clickOutsideHandler);
this._hasRegisteredPinnedClickOutside = true;
} else if (!isFloating && this._hasRegisteredPinnedClickOutside) {
document.removeEventListener('mouseup', clickOutsideHandler);
this._hasRegisteredPinnedClickOutside = false;
}
const button = document.getElementById('zen-sidepanel-button');
if (!button) return;
if (Services.prefs.getBoolPref('zen.sidebar.enabled')) {
button.removeAttribute('hidden');
} else {
button.setAttribute('hidden', 'true');
this._closeSidebarPanel();
return;
}
}
_handleClickOutside(event) {
let sidebar = document.getElementById('zen-sidebar-web-panel');
if (!sidebar.hasAttribute('pinned') || this._isDragging || !this.shouldCloseOnBlur) {
return;
}
let target = event.target;
const closestSelector = [
'#zen-sidebar-web-panel',
'#zen-sidebar-panels-wrapper',
'#zenWebPanelContextMenu',
'#zen-sidebar-web-panel-splitter',
'#contentAreaContextMenu',
].join(', ');
if (target.closest(closestSelector)) {
return;
}
this.close();
}
toggle() {
if (!this._currentPanel) {
this._currentPanel = this._lastOpenedPanel;
}
if (document.getElementById('zen-sidebar-web-panel').hasAttribute('hidden')) {
this.open();
return;
}
this.close();
}
open() {
let sidebar = document.getElementById('zen-sidebar-web-panel');
if (!this.sidebar.hasAttribute('pinned')) {
this.moveToTabBoxWrapper();
}
sidebar.removeAttribute('hidden');
this.update();
}
update() {
this._updateWebPanels();
this._updateSidebarButton();
this._updateWebPanel();
this._updateButtons();
}
_updateSidebarButton() {
let button = document.getElementById('zen-sidepanel-button');
if (!button) return;
if (!document.getElementById('zen-sidebar-web-panel').hasAttribute('hidden')) {
button.setAttribute('open', 'true');
} else {
button.removeAttribute('open');
}
}
_updateWebPanels() {
if (Services.prefs.getBoolPref('zen.sidebar.enabled')) {
this.sidebarElement.removeAttribute('hidden');
} else {
this.sidebarElement.setAttribute('hidden', 'true');
this._closeSidebarPanel();
return;
}
let data = this.sidebarData;
if (!data.data || !data.index) {
return;
}
this.sidebarElement.innerHTML = '';
for (let site of data.index) {
let panel = data.data[site];
if (!panel || !panel.url) {
continue;
}
let button = document.createXULElement('toolbarbutton');
button.classList.add('zen-sidebar-panel-button', 'toolbarbutton-1', 'chromeclass-toolbar-additional');
button.setAttribute('flex', '1');
button.setAttribute('zen-sidebar-id', site);
button.setAttribute('context', 'zenWebPanelContextMenu');
this._getWebPanelIcon(panel.url, button);
button.addEventListener('click', this._handleClick.bind(this));
button.addEventListener('dragstart', this._handleDragStart.bind(this));
button.addEventListener('dragover', this._handleDragOver.bind(this));
button.addEventListener('dragenter', this._handleDragEnter.bind(this));
button.addEventListener('dragend', this._handleDragEnd.bind(this));
this.sidebarElement.appendChild(button);
}
const addButton = document.getElementById('zen-sidebar-add-panel-button');
if (data.index.length < this.MAX_SIDEBAR_PANELS) {
addButton.removeAttribute('hidden');
} else {
addButton.setAttribute('hidden', 'true');
}
}
async _openAddPanelDialog() {
let dialogURL = 'chrome://browser/content/places/zenNewWebPanel.xhtml';
let features = 'centerscreen,chrome,modal,resizable=no';
let aParentWindow = Services.wm.getMostRecentWindow('navigator:browser');
if (aParentWindow?.gDialogBox) {
await aParentWindow.gDialogBox.open(dialogURL, {});
} else {
aParentWindow.openDialog(dialogURL, '', features, {});
}
}
_setPinnedToElements() {
let sidebar = document.getElementById('zen-sidebar-web-panel');
sidebar.setAttribute('pinned', 'true');
document.getElementById('zen-sidebar-web-panel-pinned').setAttribute('pinned', 'true');
}
_removePinnedFromElements() {
let sidebar = document.getElementById('zen-sidebar-web-panel');
sidebar.removeAttribute('pinned');
document.getElementById('zen-sidebar-web-panel-pinned').removeAttribute('pinned');
}
_closeSidebarPanel() {
let sidebar = document.getElementById('zen-sidebar-web-panel');
sidebar.setAttribute('hidden', 'true');
this._lastOpenedPanel = this._currentPanel;
this._currentPanel = null;
}
_handleClick(event) {
let target = event.target;
let panelId = target.getAttribute('zen-sidebar-id');
if (this._currentPanel === panelId) {
return;
}
this._currentPanel = panelId;
this._updateWebPanel();
}
_handleDragStart(event) {
this.__dragingElement = event.target;
this.__dragingIndex = Array.prototype.indexOf.call(event.target.parentNode.children, event.target);
event.target.style.opacity = '0.7';
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/html', event.target.innerHTML);
event.dataTransfer.setData('text/plain', event.target.id);
}
_handleDragOver(event) {}
_handleDragEnter(event) {
if (typeof this.__dragingElement === 'undefined') {
return;
}
const target = event.target;
const elIndex = Array.prototype.indexOf.call(target.parentNode.children, target);
if (elIndex < this.__dragingIndex) {
target.before(this.__dragingElement);
this.__dragingIndex = elIndex - 1;
}
target.after(this.__dragingElement);
this.__dragingIndex = elIndex + 1;
}
_handleDragEnd(event) {
event.target.style.opacity = '1';
let data = this.sidebarData;
let newPos = [];
for (let element of this.__dragingElement.parentNode.children) {
let panelId = element.getAttribute('zen-sidebar-id');
newPos.push(panelId);
}
data.index = newPos;
Services.prefs.setStringPref('zen.sidebar.data', JSON.stringify(data));
this._currentPanel = this.__dragingElement.getAttribute('zen-sidebar-id');
this.open();
this.__dragingElement = undefined;
}
_createNewPanel(url) {
let data = this.sidebarData;
let newName = 'p' + new Date().getTime();
data.index.push(newName);
data.data[newName] = {
url: url,
ua: false,
};
Services.prefs.setStringPref('zen.sidebar.data', JSON.stringify(data));
this._currentPanel = newName;
this.open();
}
_updateButtons() {
for (let button of this.sidebarElement.querySelectorAll('.zen-sidebar-panel-button')) {
if (button.getAttribute('zen-sidebar-id') === this._currentPanel) {
button.setAttribute('selected', 'true');
} else {
button.removeAttribute('selected');
}
}
}
_hideAllWebPanels() {
let sidebar = document.getElementById('zen-sidebar-web-panel');
for (let browser of sidebar.querySelectorAll('browser[zen-sidebar-id]')) {
browser.setAttribute('hidden', 'true');
browser.docShellIsActive = false;
browser.zenModeActive = false;
}
}
get introductionPanel() {
return document.getElementById('zen-sidebar-introduction-panel');
}
_updateWebPanel() {
this._updateButtons();
// let sidebar = document.getElementById("zen-sidebar-web-panel");
this._hideAllWebPanels();
if (!this._currentPanel) {
this.introductionPanel.removeAttribute('hidden');
this.forwardButton.setAttribute('disabled', true);
this.backButton.setAttribute('disabled', true);
return;
}
this.introductionPanel.setAttribute('hidden', 'true');
let existantWebview = this._getCurrentBrowser();
if (existantWebview) {
existantWebview.docShellIsActive = true;
existantWebview.zenModeActive = true;
existantWebview.removeAttribute('hidden');
document.getElementById('zen-sidebar-web-panel-title').textContent = existantWebview.contentTitle;
return;
}
let data = this._getWebPanelData(this._currentPanel);
let browser = this._createWebPanelBrowser(data);
let browserContainers = document.getElementById('zen-sidebar-web-panel-browser-containers');
browserContainers.appendChild(browser);
browser.addProgressListener(this.progressListener, Ci.nsIWebProgress.NOTIFY_LOCATION);
if (data.ua) {
browser.browsingContext.customUserAgent = this.DEFAULT_MOBILE_USER_AGENT;
browser.reload();
}
browser.docShellIsActive = true;
browser.zenModeActive = true;
}
_getWebPanelData(id) {
let data = this.sidebarData;
let panel = data.data[id];
if (!panel || !panel.url) {
return {};
}
return {
id: id,
...panel,
};
}
getTabForBrowser(browser) {
return this._tabBrowserSet.get(browser);
}
setTabForBrowser(browser, tab) {
this._tabBrowserSet.set(browser, tab);
}
removeTabForBrowser(browser) {
this._tabBrowserSet.delete(browser);
}
_createWebPanelBrowser(data) {
const titleContainer = document.getElementById('zen-sidebar-web-panel-title');
titleContainer.textContent = 'Loading...';
let browser = gBrowser.createBrowser({
userContextId: data.userContextId,
});
const tab = this.sidebar.querySelector(`[zen-sidebar-id='${data.id}']`);
this.setTabForBrowser(browser, tab);
tab.linkedBrowser = browser;
tab.permanentKey = browser.permanentKey;
browser.setAttribute('disablefullscreen', 'true');
browser.setAttribute('src', data.url);
browser.setAttribute('zen-sidebar-id', data.id);
browser.addEventListener(
'pagetitlechanged',
function (event) {
let browser = event.target;
let title = browser.contentTitle;
if (!title) {
return;
}
let id = browser.getAttribute('zen-sidebar-id');
if (id === this._currentPanel) {
titleContainer.textContent = title;
}
}.bind(this)
);
return browser;
}
_getWebPanelIcon(url, element) {
let { preferredURI } = Services.uriFixup.getFixupURIInfo(url);
element.setAttribute('image', `page-icon:${preferredURI.spec}`);
fetch(`https://s2.googleusercontent.com/s2/favicons?domain_url=${preferredURI.spec}`).then(async (response) => {
if (response.ok) {
let blob = await response.blob();
let reader = new FileReader();
reader.onload = function () {
element.setAttribute('image', reader.result);
};
reader.readAsDataURL(blob);
}
});
}
_getBrowserById(id) {
let sidebar = document.getElementById('zen-sidebar-web-panel');
return sidebar.querySelector(`browser[zen-sidebar-id="${id}"]`);
}
_getCurrentBrowser() {
return this._getBrowserById(this._currentPanel);
}
reload() {
let browser = this._getCurrentBrowser();
if (browser) {
browser.reload();
}
}
forward() {
let browser = this._getCurrentBrowser();
if (browser) {
browser.goForward();
}
}
back() {
let browser = this._getCurrentBrowser();
if (browser) {
browser.goBack();
}
}
home() {
let browser = this._getCurrentBrowser();
if (browser) {
browser.gotoIndex();
}
}
close() {
this._hideAllWebPanels();
this._closeSidebarPanel();
this._updateSidebarButton();
}
moveToTabBoxWrapper() {
this.tabBox.before(this.sidebarWrapper);
this.sidebarWrapper.style.order = '';
}
moveToTabBox() {
this.tabBox.prepend(this.sidebarWrapper);
}
togglePinned(elem) {
if (this.sidebar.hasAttribute('pinned')) {
this._removePinnedFromElements();
//this.moveToTabBoxWrapper();
} else {
this._setPinnedToElements();
//this.moveToTabBox();
}
this.update();
}
get sidebarElement() {
if (!this._sidebarElement) {
this._sidebarElement = document.getElementById('zen-sidebar-panels-sites');
}
return this._sidebarElement;
}
get splitterElement() {
if (!this._splitterElement) {
this._splitterElement = document.getElementById('zen-sidebar-web-panel-splitter');
}
return this._splitterElement;
}
get hSplitterElement() {
if (!this._hSplitterElement) {
this._hSplitterElement = document.getElementById('zen-sidebar-web-panel-hsplitter');
}
return this._hSplitterElement;
}
get sidebarHeader() {
if (!this._sidebarHeader) {
this._sidebarHeader = document.getElementById('zen-sidebar-web-header');
}
return this._sidebarHeader;
}
get sidebarWrapper() {
if (!this._sideBarWrapper) {
this._sideBarWrapper = document.getElementById('zen-sidebar-web-panel-wrapper');
}
return this._sideBarWrapper;
}
get sidebarHeaderButtons() {
if (!this._sidebarHeaderButtons) {
this._sidebarHeaderButtons = [...this.sidebarHeader.querySelectorAll('.toolbarbutton-1')];
}
return this._sidebarHeaderButtons;
}
// Context menu
updateContextMenu(aPopupMenu) {
let panel =
aPopupMenu.triggerNode && (aPopupMenu.triggerNode || aPopupMenu.triggerNode.closest('toolbarbutton[zen-sidebar-id]'));
if (!panel) {
return;
}
let id = panel.getAttribute('zen-sidebar-id');
this.contextTab = id;
let data = this._getWebPanelData(id);
let browser = this._getBrowserById(id);
let isMuted = browser && browser.audioMuted;
let mutedContextItem = document.getElementById('context_zenToggleMuteWebPanel');
document.l10n.setAttributes(
mutedContextItem,
!isMuted ? 'zen-web-side-panel-context-mute-panel' : 'zen-web-side-panel-context-unmute-panel'
);
if (!isMuted) {
mutedContextItem.setAttribute('muted', 'true');
} else {
mutedContextItem.removeAttribute('muted');
}
document.l10n.setAttributes(
document.getElementById('context_zenToogleUAWebPanel'),
data.ua ? 'zen-web-side-panel-context-disable-ua' : 'zen-web-side-panel-context-enable-ua'
);
if (!browser) {
document.getElementById('context_zenUnloadWebPanel').setAttribute('disabled', 'true');
} else {
document.getElementById('context_zenUnloadWebPanel').removeAttribute('disabled');
}
}
createContainerTabMenu(event) {
let window = event.target.ownerGlobal;
let data = this.sidebarData;
let panelData = data.data[this.contextTab];
return window.createUserContextMenu(event, {
isContextMenu: true,
excludeUserContextId: panelData.userContextId,
showDefaultTab: true,
});
}
contextChangeContainerTab(event) {
let data = this.sidebarData;
let userContextId = parseInt(event.target.getAttribute('data-usercontextid'));
data.data[this.contextTab].userContextId = userContextId;
Services.prefs.setStringPref('zen.sidebar.data', JSON.stringify(data));
let browser = this._getBrowserById(this.contextTab);
if (browser) {
browser.remove();
// We need to re-apply a new browser so it takes the new userContextId
this._updateWebPanel();
}
}
contextOpenNewTab() {
let browser = this._getBrowserById(this.contextTab);
let data = this.sidebarData;
let panel = data.data[this.contextTab];
let url = browser == null ? panel.url : browser.currentURI.spec;
gZenUIManager.openAndChangeToTab(url);
this.close();
}
contextToggleMuteAudio() {
let browser = this._getBrowserById(this.contextTab);
if (browser.audioMuted) {
browser.unmute();
} else {
browser.mute();
}
}
contextToggleUserAgent() {
let browser = this._getBrowserById(this.contextTab);
browser.browsingContext.customUserAgent = browser.browsingContext.customUserAgent ? null : this.DEFAULT_MOBILE_USER_AGENT;
let data = this.sidebarData;
data.data[this.contextTab].ua = !data.data[this.contextTab].ua;
Services.prefs.setStringPref('zen.sidebar.data', JSON.stringify(data));
browser.reload();
}
contextDelete() {
let data = this.sidebarData;
delete data.data[this.contextTab];
data.index = data.index.filter((id) => id !== this.contextTab);
let browser = this._getBrowserById(this.contextTab);
if (browser) {
browser.remove();
document.getElementById('zen-sidebar-web-panel-title').textContent = '';
}
this._currentPanel = null;
this._lastOpenedPanel = null;
this.update();
Services.prefs.setStringPref('zen.sidebar.data', JSON.stringify(data));
}
contextUnload() {
let browser = this._getBrowserById(this.contextTab);
this.removeTabForBrowser(browser);
browser.remove();
document.getElementById('zen-sidebar-web-panel-title').textContent = '';
this._closeSidebarPanel();
this.close();
this._lastOpenedPanel = null;
}
insertIntoContextMenu() {
const sibling = document.getElementById('context-stripOnShareLink');
const menuitem = document.createXULElement('menuitem');
menuitem.setAttribute('id', 'context-zenAddToWebPanel');
menuitem.setAttribute('hidden', 'true');
menuitem.setAttribute('oncommand', 'gZenBrowserManagerSidebar.addPanelFromContextMenu();');
menuitem.setAttribute('data-l10n-id', 'zen-web-side-panel-context-add-to-panel');
sibling.insertAdjacentElement('afterend', menuitem);
}
addPanelFromContextMenu() {
const url = gContextMenu.linkURL || gContextMenu.target.ownerDocument.location.href;
this._createNewPanel(url);
}
}
window.gZenBrowserManagerSidebar = new ZenBrowserManagerSidebar();

View file

@ -0,0 +1,283 @@
{
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenTabUnloaderEnabled', 'zen.tab-unloader.enabled', false);
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenTabUnloaderTimeout', 'zen.tab-unloader.timeout-minutes', 20);
XPCOMUtils.defineLazyPreferenceGetter(lazy, 'zenTabUnloaderExcludedUrls', 'zen.tab-unloader.excluded-urls', '');
const ZEN_TAB_UNLOADER_DEFAULT_EXCLUDED_URLS = [
'^about:',
'^chrome:',
'^devtools:',
'^file:',
'^resource:',
'^view-source:',
'^view-image:',
];
class ZenTabsObserver {
static ALL_EVENTS = [
'TabAttrModified',
'TabPinned',
'TabUnpinned',
'TabBrowserInserted',
'TabBrowserDiscarded',
'TabShow',
'TabHide',
'TabOpen',
'TabClose',
'TabSelect',
'TabMultiSelect',
];
#listeners = [];
constructor() {
this.#listenAllEvents();
}
#listenAllEvents() {
const eventListener = this.#eventListener.bind(this);
for (const event of ZenTabsObserver.ALL_EVENTS) {
window.addEventListener(event, eventListener);
}
window.addEventListener('unload', () => {
for (const event of ZenTabsObserver.ALL_EVENTS) {
window.removeEventListener(event, eventListener);
}
});
}
#eventListener(event) {
for (const listener of this.#listeners) {
listener(event.type, event);
}
}
addTabsListener(listener) {
this.#listeners.push(listener);
}
}
class ZenTabsIntervalUnloader {
static INTERVAL = 1000 * 60; // 1 minute
interval = null;
unloader = null;
#excludedUrls = [];
#compiledExcludedUrls = [];
constructor(unloader) {
this.unloader = unloader;
this.interval = setInterval(this.intervalListener.bind(this), ZenTabsIntervalUnloader.INTERVAL);
this.#excludedUrls = this.lazyExcludeUrls;
}
get lazyExcludeUrls() {
return [
...ZEN_TAB_UNLOADER_DEFAULT_EXCLUDED_URLS,
...lazy.zenTabUnloaderExcludedUrls.split(',').map((url) => url.trim()),
];
}
arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
// If you don't care about the order of the elements inside
// the array, you should sort both arrays here.
// Please note that calling sort on an array will modify that array.
// you might want to clone your array first.
for (var i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) return false;
}
return true;
}
get excludedUrls() {
// Check if excludedrls is the same as the pref value
const excludedUrls = this.lazyExcludeUrls;
if (!this.arraysEqual(this.#excludedUrls, excludedUrls) || !this.#compiledExcludedUrls.length) {
this.#excludedUrls = excludedUrls;
this.#compiledExcludedUrls = excludedUrls.map((url) => new RegExp(url));
}
return this.#compiledExcludedUrls;
}
intervalListener() {
if (!lazy.zenTabUnloaderEnabled) {
return;
}
const currentTimestamp = Date.now();
const excludedUrls = this.excludedUrls;
const tabs = gBrowser.tabs;
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
if (this.unloader.canUnloadTab(tab, currentTimestamp, excludedUrls)) {
this.unloader.unload(tab);
}
}
}
}
class ZenTabUnloader extends ZenDOMOperatedFeature {
static ACTIVITY_MODIFIERS = ['muted', 'soundplaying', 'label', 'attention'];
init() {
if (!lazy.zenTabUnloaderEnabled) {
return;
}
this.insertIntoContextMenu();
this.observer = new ZenTabsObserver();
this.intervalUnloader = new ZenTabsIntervalUnloader(this);
this.observer.addTabsListener(this.onTabEvent.bind(this));
}
onTabEvent(action, event) {
const tab = event.target;
switch (action) {
case 'TabPinned':
case 'TabUnpinned':
case 'TabBrowserInserted':
case 'TabBrowserDiscarded':
case 'TabShow':
case 'TabHide':
break;
case 'TabAttrModified':
this.handleTabAttrModified(tab, event);
break;
case 'TabOpen':
this.handleTabOpen(tab);
break;
case 'TabClose':
this.handleTabClose(tab);
break;
case 'TabSelect':
case 'TabMultiSelect':
this.updateTabActivity(tab);
break;
default:
console.warn('ZenTabUnloader: Unhandled tab event', action);
break;
}
}
onLocationChange(browser) {
const tab = browser.ownerGlobal.gBrowser.getTabForBrowser(browser);
this.updateTabActivity(tab);
}
handleTabClose(tab) {
// Nothing yet
}
handleTabOpen(tab) {
this.updateTabActivity(tab);
}
handleTabAttrModified(tab, event) {
for (const modifier of ZenTabUnloader.ACTIVITY_MODIFIERS) {
if (event.detail.changed.includes(modifier)) {
this.updateTabActivity(tab);
break;
}
}
}
updateTabActivity(tab) {
const currentTimestamp = Date.now();
tab.lastActivity = currentTimestamp;
}
get tabs() {
return gBrowser.tabs;
}
insertIntoContextMenu() {
const element = window.MozXULElement.parseXULToFragment(`
<menuseparator/>
<menuitem id="context_zenUnloadTab"
data-lazy-l10n-id="tab-zen-unload"
oncommand="gZenTabUnloader.unloadTab();"/>
<menu data-lazy-l10n-id="zen-tabs-unloader-tab-actions" id="context_zenTabActions">
<menupopup>
<menuitem id="context_zenPreventUnloadTab"
data-lazy-l10n-id="tab-zen-prevent-unload"
oncommand="gZenTabUnloader.preventUnloadTab();"/>
<menuitem id="context_zenIgnoreUnloadTab"
data-lazy-l10n-id="tab-zen-ignore-unload"
oncommand="gZenTabUnloader.ignoreUnloadTab();"/>
</menupopup>
</menu>
`);
document.getElementById('context_closeDuplicateTabs').parentNode.appendChild(element);
}
unload(tab) {
gBrowser.discardBrowser(tab);
tab.removeAttribute('linkedpanel');
}
unloadTab() {
const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
for (let i = 0; i < tabs.length; i++) {
if (this.canUnloadTab(tabs[i], Date.now(), [], true)) {
this.unload(tabs[i]);
}
}
}
preventUnloadTab() {
const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
tab.zenIgnoreUnload = true;
}
}
ignoreUnloadTab() {
const tabs = TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
tab.zenIgnoreUnload = false;
}
}
canUnloadTab(tab, currentTimestamp, excludedUrls, ignoreTimestamp = false) {
if (
tab.pinned ||
tab.selected ||
tab.multiselected ||
tab.hasAttribute('busy') ||
tab.hasAttribute('pending') ||
!tab.linkedPanel ||
tab.splitView ||
tab.attention ||
tab.linkedBrowser?.zenModeActive ||
tab.pictureinpicture ||
tab.soundPlaying ||
tab.zenIgnoreUnload ||
excludedUrls.some((url) => url.test(tab.linkedBrowser.currentURI.spec))
) {
return false;
}
if (ignoreTimestamp) {
return true;
}
const lastActivity = tab.lastActivity;
if (!lastActivity) {
return false;
}
const diff = currentTimestamp - lastActivity;
// Check if the tab has been inactive for more than the timeout
return diff > lazy.zenTabUnloaderTimeout * 60 * 1000;
}
}
window.gZenTabUnloader = new ZenTabUnloader();
}

View file

@ -0,0 +1,176 @@
const kZenAccentColorConfigKey = 'zen.theme.accent-color';
var gZenThemeBuilder = {
init() {
return; // TODO:
this._mouseMoveListener = this._handleThumbMouseMove.bind(this);
setTimeout(() => {
this._initBuilderUI();
}, 500);
},
get _builderWrapper() {
if (this.__builderWrapper) {
return this.__builderWrapper;
}
this.__builderWrapper = document.getElementById('zen-theme-builder-wrapper');
return this.__builderWrapper;
},
_initBuilderUI() {
let wrapper = this._builderWrapper;
if (!wrapper) {
return;
}
console.info('gZenThemeBuilder: init builder UI');
const kTemplate = `
<html:div id="zen-theme-builder">
<html:div id="zen-theme-builder-color-picker">
<html:canvas id="zen-theme-builder-color-picker-canvas"></html:canvas>
<html:div id="zen-theme-builder-color-picker-deck">
<html:div id="zen-theme-builder-color-picker-thumb"></html:div>
</html:div>
</html:div>
</html:div>
`;
wrapper.innerHTML = kTemplate;
this._initColorPicker();
},
_getPositionFromColor(ctx, color) {
var w = ctx.canvas.width,
h = ctx.canvas.height,
data = ctx.getImageData(0, 0, w, h), /// get image data
buffer = data.data, /// and its pixel buffer
len = buffer.length, /// cache length
x,
y = 0,
p,
px; /// for iterating
/// iterating x/y instead of forward to get position the easy way
for (; y < h; y++) {
/// common value for all x
p = y * 4 * w;
for (x = 0; x < w; x++) {
/// next pixel (skipping 4 bytes as each pixel is RGBA bytes)
px = p + x * 4;
/// if red component match check the others
if (buffer[px] === color[0]) {
if (buffer[px + 1] === color[1] && buffer[px + 2] === color[2]) {
return [x, y];
}
}
}
}
return null;
},
_hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)];
},
_componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? '0' + hex : hex;
},
_rgbToHex(r, g, b) {
return '#' + this._componentToHex(r) + this._componentToHex(g) + this._componentToHex(b);
},
_initColorPicker() {
const canvas = document.getElementById('zen-theme-builder-color-picker-canvas');
const thumb = document.getElementById('zen-theme-builder-color-picker-thumb');
// A all the main colors are all blended together towards the center.
// But we also add some random gradients to make it look more interesting.
// Instead of using a simple gradient, we use a radial gradient.
const ctx = canvas.getContext('2d');
const size = 180;
canvas.width = size;
canvas.height = size;
const center = size / 2;
const radius = size / 2;
const gradient = ctx.createConicGradient(0, center, center);
gradient.addColorStop(0, '#fff490');
gradient.addColorStop(1 / 12, '#f9e380');
gradient.addColorStop(2 / 12, '#fecc87');
gradient.addColorStop(3 / 12, '#ffa894');
gradient.addColorStop(4 / 12, '#f98089');
gradient.addColorStop(5 / 12, '#f9b7c5');
gradient.addColorStop(6 / 12, '#c193b8');
gradient.addColorStop(7 / 12, '#a8b7e0');
gradient.addColorStop(8 / 12, '#88d2f9');
gradient.addColorStop(9 / 12, '#81e8e5');
gradient.addColorStop(10 / 12, '#b7e5a5');
gradient.addColorStop(11 / 12, '#eaefac');
gradient.addColorStop(1, '#fff490');
const radialGradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2);
radialGradient.addColorStop(0, 'rgba(255,255,255,1)');
radialGradient.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size, size);
//ctx.fillStyle = radialGradient;
//ctx.fillRect(0, 0, size, size);
// Add the thumb.
const accentColor = Services.prefs.getStringPref(kZenAccentColorConfigKey, '#aac7ff');
const pos = this._getPositionFromColor(ctx, this._hexToRgb(accentColor));
let x = pos ? pos[0] : center;
let y = pos ? pos[1] : center;
thumb.style.left = `${x}px`;
thumb.style.top = `${y}px`;
thumb.addEventListener('mousedown', this._handleThumbMouseDown.bind(this));
document.addEventListener('mouseup', this._handleThumbMouseUp.bind(this));
},
_handleThumbMouseDown(e) {
document.addEventListener('mousemove', this._mouseMoveListener);
},
_handleThumbMouseUp(e) {
document.removeEventListener('mousemove', this._mouseMoveListener);
},
_handleThumbMouseMove(e) {
const kThumbOffset = 15;
const deck = document.getElementById('zen-theme-builder-color-picker-deck');
const thumb = document.getElementById('zen-theme-builder-color-picker-thumb');
const rect = deck.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
if (x > rect.width - kThumbOffset) {
x = rect.width - kThumbOffset;
}
if (y > rect.height - kThumbOffset) {
y = rect.height - kThumbOffset;
}
if (x < kThumbOffset) {
x = kThumbOffset;
}
if (y < kThumbOffset) {
y = kThumbOffset;
}
thumb.style.left = `${x}px`;
thumb.style.top = `${y}px`;
const canvas = document.getElementById('zen-theme-builder-color-picker-canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(x, y, 1, 1);
// Update the accent color.
Services.prefs.setStringPref(kZenAccentColorConfigKey, this._rgbToHex(...imageData.data));
},
};

View file

@ -0,0 +1,120 @@
var ZenThemesCommon = {
kZenColors: ['#aac7ff', '#74d7cb', '#a0d490', '#dec663', '#ffb787', '#dec1b1', '#ffb1c0', '#ddbfc3', '#f6b0ea', '#d4bbff'],
get browsers() {
return Services.wm.getEnumerator('navigator:browser');
},
get currentBrowser() {
return Services.wm.getMostRecentWindow('navigator:browser');
},
get themesRootPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
},
get themesDataFile() {
return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
},
getThemeFolder(themeId) {
return PathUtils.join(this.themesRootPath, themeId);
},
resetThemesCache() {
this.themes = null;
},
async getThemes() {
if (!this.themes) {
if (!(await IOUtils.exists(this.themesDataFile))) {
await IOUtils.writeJSON(this.themesDataFile, {});
}
try {
this.themes = await IOUtils.readJSON(this.themesDataFile);
} catch (e) {
// If we have a corrupted file, reset it
await IOUtils.writeJSON(this.themesDataFile, {});
this.themes = {};
gNotificationBox.appendNotification(
"zen-themes-corrupted",
{
label: { "l10n-id": "zen-themes-corrupted" },
image: "chrome://browser/skin/notification-icons/persistent-storage-blocked.svg",
priority: gNotificationBox.PRIORITY_INFO_MEDIUM,
},
[]
);
}
}
return this.themes;
},
async getThemePreferences(theme) {
const themePath = PathUtils.join(this.themesRootPath, theme.id, 'preferences.json');
if (!(await IOUtils.exists(themePath)) || !theme.preferences) {
return [];
}
const preferences = await IOUtils.readJSON(themePath);
// compat mode for old preferences, all of them can only be checkboxes
if (typeof preferences === 'object' && !Array.isArray(preferences)) {
console.warn(
`[ZenThemes]: Warning, ${theme.name} uses legacy preferences, please migrate them to the new preferences style, as legacy preferences might be removed at a future release. More information at: https://docs.zen-browser.app/themes-store/themes-marketplace-preferences`
);
const newThemePreferences = [];
for (let [entry, label] of Object.entries(preferences)) {
const [_, negation = '', os = '', property] = /(!?)(?:(macos|windows|linux):)?([A-z0-9-_.]+)/g.exec(entry);
const isNegation = negation === '!';
if (
(isNegation && os === gZenOperatingSystemCommonUtils.currentOperatingSystem) ||
(os !== '' && os !== gZenOperatingSystemCommonUtils.currentOperatingSystem && !isNegation)
) {
continue;
}
newThemePreferences.push({
property,
label,
type: 'checkbox',
disabledOn: os !== '' ? [os] : [],
});
}
return newThemePreferences;
}
return preferences.filter(
({ disabledOn = [] }) => !disabledOn.includes(gZenOperatingSystemCommonUtils.currentOperatingSystem)
);
},
throttle(mainFunction, delay) {
let timerFlag = null;
return (...args) => {
if (timerFlag === null) {
mainFunction(...args);
timerFlag = setTimeout(() => {
timerFlag = null;
}, delay);
}
};
},
debounce(mainFunction, wait) {
let timerFlag;
return (...args) => {
clearTimeout(timerFlag);
timerFlag = setTimeout(() => {
mainFunction(...args);
}, wait);
};
},
};

View file

@ -0,0 +1,320 @@
const kZenStylesheetThemeHeader = '/* Zen Themes - Generated by ZenThemesImporter.';
const kZenStylesheetThemeHeaderBody = `* DO NOT EDIT THIS FILE DIRECTLY!
* Your changes will be overwritten.
* Instead, go to the preferences and edit the themes there.
*/
`;
const kenStylesheetFooter = `
/* End of Zen Themes */
`;
const getCurrentDateTime = () =>
new Intl.DateTimeFormat('en-US', {
dateStyle: 'full',
timeStyle: 'full',
}).format(new Date().getTime());
var gZenStylesheetManager = {
async writeStylesheet(path, themes) {
let content = kZenStylesheetThemeHeader;
content += `\n* FILE GENERATED AT: ${getCurrentDateTime()}\n`;
content += kZenStylesheetThemeHeaderBody;
for (let theme of themes) {
if (theme.enabled !== undefined && !theme.enabled) {
continue;
}
content += this.getThemeCSS(theme);
}
content += kenStylesheetFooter;
const buffer = new TextEncoder().encode(content);
await IOUtils.write(path, buffer);
},
getThemeCSS(theme) {
let css = '\n';
css += `/* Name: ${theme.name} */\n`;
css += `/* Description: ${theme.description} */\n`;
css += `/* Author: @${theme.author} */\n`;
if (theme._readmeURL) {
css += `/* Readme: ${theme.readme} */\n`;
}
css += `@import url("${theme._chromeURL}");\n`;
return css;
},
};
var gZenThemesImporter = new (class {
constructor() {
console.info('[ZenThemesImporter]: Initializing Zen Themes Importer');
try {
window.SessionStore.promiseInitialized.then(async () => {
if (Services.prefs.getBoolPref('zen.themes.disable-all', false)) {
console.log('[ZenThemesImporter]: Disabling all themes.');
return;
}
const themes = await this.getEnabledThemes();
const themesWithPreferences = await Promise.all(
themes.map(async (theme) => {
const preferences = await ZenThemesCommon.getThemePreferences(theme);
return {
name: theme.name,
enabled: theme.enabled,
preferences,
};
})
);
this.writeToDom(themesWithPreferences);
await this.insertStylesheet();
});
console.info('[ZenThemesImporter]: Zen Themes imported');
} catch (e) {
console.error('[ZenThemesImporter]: Error importing Zen Themes: ', e);
}
Services.prefs.addObserver('zen.themes.updated-value-observer', this.rebuildThemeStylesheet.bind(this), false);
Services.prefs.addObserver('zen.themes.disable-all', this.handleDisableThemes.bind(this), false);
}
get sss() {
if (!this._sss) {
this._sss = Cc['@mozilla.org/content/style-sheet-service;1'].getService(Ci.nsIStyleSheetService);
}
return this._sss;
}
get styleSheetPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes.css');
}
async handleDisableThemes() {
if (Services.prefs.getBoolPref('zen.themes.disable-all', false)) {
console.log('[ZenThemesImporter]: Disabling themes module.');
await this.removeStylesheet();
} else {
console.log('[ZenThemesImporter]: Enabling themes module.');
await this.rebuildThemeStylesheet();
}
}
get styleSheetURI() {
if (!this._styleSheetURI) {
this._styleSheetURI = Services.io.newFileURI(new FileUtils.File(this.styleSheetPath));
}
return this._styleSheetURI;
}
getStylesheetURIForTheme(theme) {
return Services.io.newFileURI(new FileUtils.File(PathUtils.join(ZenThemesCommon.getThemeFolder(theme.id), 'chrome.css')));
}
async insertStylesheet() {
if (await IOUtils.exists(this.styleSheetPath)) {
await this.sss.loadAndRegisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET);
}
if (this.sss.sheetRegistered(this.styleSheetURI, this.sss.AGENT_SHEET)) {
console.debug('[ZenThemesImporter]: Sheet successfully registered');
}
}
async removeStylesheet() {
await this.sss.unregisterSheet(this.styleSheetURI, this.sss.AGENT_SHEET);
await IOUtils.remove(this.styleSheetPath, { ignoreAbsent: true });
if (!this.sss.sheetRegistered(this.styleSheetURI, this.sss.AGENT_SHEET) && !(await IOUtils.exists(this.styleSheetPath))) {
console.debug('[ZenThemesImporter]: Sheet successfully unregistered');
}
}
async rebuildThemeStylesheet() {
if (Services.focus.activeWindow !== window) {
return;
}
ZenThemesCommon.themes = null;
await this.removeStylesheet();
const themes = await this.getEnabledThemes();
await this.writeStylesheet(themes);
const themesWithPreferences = await Promise.all(
themes.map(async (theme) => {
const preferences = await ZenThemesCommon.getThemePreferences(theme);
return {
name: theme.name,
enabled: theme.enabled,
preferences,
};
})
);
this.setDefaults(themesWithPreferences);
this.writeToDom(themesWithPreferences);
await this.insertStylesheet();
}
async getEnabledThemes() {
const themeObject = await ZenThemesCommon.getThemes();
const themes = Object.values(themeObject).filter((theme) => theme.enabled === undefined || theme.enabled);
const themeList = themes.map(({ name }) => name).join(', ');
const message =
themeList !== ''
? `[ZenThemesImporter]: Loading enabled Zen themes: ${themeList}.`
: '[ZenThemesImporter]: No enabled Zen themes.';
console.log(message);
return themes;
}
setDefaults(themesWithPreferences) {
for (const { preferences, enabled } of themesWithPreferences) {
if (enabled !== undefined && !enabled) {
continue;
}
for (const { type, property, defaultValue } of preferences) {
if (defaultValue === undefined) {
continue;
}
switch (type) {
case 'checkbox': {
const value = Services.prefs.getBoolPref(property, false);
if (typeof defaultValue !== 'boolean') {
console.log(`[ZenThemesImporter]: Warning, invalid data type received for expected type boolean, skipping.`);
continue;
}
if (!value) {
Services.prefs.setBoolPref(property, defaultValue);
}
break;
}
default: {
const value = Services.prefs.getStringPref(property, 'zen-property-no-saved');
if (typeof defaultValue !== 'string' && typeof defaultValue !== 'number') {
console.log(`[ZenThemesImporter]: Warning, invalid data type received (${typeof defaultValue}), skipping.`);
continue;
}
if (value === 'zen-property-no-saved') {
Services.prefs.setStringPref(property, defaultValue.toString());
}
}
}
}
}
}
writeToDom(themesWithPreferences) {
for (const browser of ZenMultiWindowFeature.browsers) {
for (const { enabled, preferences, name } of themesWithPreferences) {
const sanitizedName = `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-z_-]+/g, '')}`;
if (enabled !== undefined && !enabled) {
const element = browser.document.getElementById(sanitizedName);
if (element) {
element.remove();
}
for (const { property } of preferences.filter(({ type }) => type !== 'checkbox')) {
const sanitizedProperty = property?.replaceAll(/\./g, '-');
browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`);
}
continue;
}
for (const { property, type } of preferences) {
const value = Services.prefs.getStringPref(property, '');
const sanitizedProperty = property?.replaceAll(/\./g, '-');
switch (type) {
case 'dropdown': {
if (value !== '') {
let element = browser.document.getElementById(sanitizedName);
if (!element) {
element = browser.document.createElement('div');
element.style.display = 'none';
element.setAttribute('id', sanitizedName);
browser.document.body.appendChild(element);
}
element.setAttribute(sanitizedProperty, value);
}
break;
}
case 'string': {
if (value === '') {
browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`);
} else {
browser.document.querySelector(':root').style.setProperty(`--${sanitizedProperty}`, value);
}
break;
}
default: {
}
}
}
}
}
}
async writeStylesheet(themeList = []) {
const themes = [];
ZenThemesCommon.themes = null;
for (let theme of themeList) {
theme._chromeURL = this.getStylesheetURIForTheme(theme).spec;
themes.push(theme);
}
await gZenStylesheetManager.writeStylesheet(this.styleSheetPath, themes);
}
})();
gZenActorsManager.addJSWindowActor("ZenThemeMarketplace", {
parent: {
esModuleURI: "chrome://browser/content/zen-components/actors/ZenThemeMarketplaceParent.sys.mjs",
},
child: {
esModuleURI: "chrome://browser/content/zen-components/actors/ZenThemeMarketplaceChild.sys.mjs",
events: {
DOMContentLoaded: {},
},
},
matches: ["https://*.zen-browser.app/*", "about:preferences"],
allFrames: true,
});

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,400 @@
var ZenWorkspacesStorage = {
async init() {
console.log('ZenWorkspacesStorage: Initializing...');
await this._ensureTable();
},
async _ensureTable() {
await 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,
is_default INTEGER NOT NULL DEFAULT 0,
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)
`);
});
},
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<string>} 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 PlacesUtils.withConnectionWrapper('ZenWorkspacesStorage.saveWorkspace', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
// Handle default workspace
if (workspace.default) {
await db.execute(`UPDATE zen_workspaces SET is_default = 0 WHERE uuid != :uuid`, { uuid: workspace.uuid });
const unsetDefaultRows = await db.execute(`SELECT uuid FROM zen_workspaces WHERE is_default = 0 AND uuid != :uuid`, { uuid: workspace.uuid });
for (const row of unsetDefaultRows) {
changedUUIDs.add(row.getResultByName('uuid'));
}
}
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, is_default, container_id, created_at, updated_at, "position",
theme_type, theme_colors, theme_opacity, theme_rotation, theme_texture
) VALUES (
:uuid, :name, :icon, :is_default, :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,
is_default: workspace.default ? 1 : 0,
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 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'),
default: !!row.getResultByName('is_default'),
containerTabId: row.getResultByName('container_id'),
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 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 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 setDefaultWorkspace(uuid, notifyObservers = true) {
const changedUUIDs = [];
await PlacesUtils.withConnectionWrapper('ZenWorkspacesStorage.setDefaultWorkspace', async (db) => {
await db.executeTransaction(async () => {
const now = Date.now();
// Unset the default flag for all other workspaces
await db.execute(`UPDATE zen_workspaces SET is_default = 0 WHERE uuid != :uuid`, { uuid });
// Collect UUIDs of workspaces that were unset as default
const unsetDefaultRows = await db.execute(`SELECT uuid FROM zen_workspaces WHERE is_default = 0 AND uuid != :uuid`, { uuid });
for (const row of unsetDefaultRows) {
changedUUIDs.push(row.getResultByName('uuid'));
}
// Set the default flag for the specified workspace
await db.execute(`UPDATE zen_workspaces SET is_default = 1 WHERE uuid = :uuid`, { uuid });
// Record the change for the specified workspace
await db.execute(`
INSERT OR REPLACE INTO zen_workspaces_changes (uuid, timestamp)
VALUES (:uuid, :timestamp)
`, {
uuid,
timestamp: Math.floor(now / 1000)
});
// Add the main workspace UUID to the changed set
changedUUIDs.push(uuid);
await this.updateLastChangeTimestamp(db);
});
});
if (notifyObservers) {
this._notifyWorkspacesChanged("zen-workspace-updated", changedUUIDs);
}
},
async markChanged(uuid) {
await 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 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 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 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 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 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));
},
};

View file

@ -0,0 +1,452 @@
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}`);
}
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);

View file

@ -0,0 +1,119 @@
export class ZenGlanceChild extends JSWindowActorChild {
constructor() {
super();
this.mouseUpListener = this.handleMouseUp.bind(this);
this.mouseDownListener = this.handleMouseDown.bind(this);
this.clickListener = this.handleClick.bind(this);
}
async handleEvent(event) {
switch (event.type) {
case 'DOMContentLoaded':
await this.initiateGlance();
break;
default:
}
}
async getActivationMethod() {
if (this._activationMethod === undefined) {
this._activationMethod = await this.sendQuery('ZenGlance:GetActivationMethod');
}
return this._activationMethod;
}
async getHoverActivationDelay() {
if (this._hoverActivationDelay === undefined) {
this._hoverActivationDelay = await this.sendQuery('ZenGlance:GetHoverActivationDelay');
}
return this._hoverActivationDelay;
}
async receiveMessage(message) {
switch (message.name) {
}
}
async initiateGlance() {
this.mouseIsDown = false;
const activationMethod = await this.getActivationMethod();
if (activationMethod === 'mantain') {
this.contentWindow.addEventListener('mousedown', this.mouseDownListener);
this.contentWindow.addEventListener('mouseup', this.mouseUpListener);
this.contentWindow.document.removeEventListener('click', this.clickListener);
} else if (activationMethod === 'ctrl' || activationMethod === 'alt' || activationMethod === 'shift') {
this.contentWindow.document.addEventListener('click', this.clickListener);
this.contentWindow.removeEventListener('mousedown', this.mouseDownListener);
this.contentWindow.removeEventListener('mouseup', this.mouseUpListener);
}
}
ensureOnlyKeyModifiers(event) {
return !(event.ctrlKey ^ event.altKey ^ event.shiftKey ^ event.metaKey);
}
openGlance(target) {
const rect = target.getBoundingClientRect();
this.sendAsyncMessage('ZenGlance:OpenGlance', {
url: target.href,
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
});
}
handleMouseUp(event) {
if (this.hasClicked) {
event.preventDefault();
event.stopPropagation();
this.hasClicked = false;
}
this.mouseIsDown = null;
}
async handleMouseDown(event) {
const target = event.target.closest('A');
console.log('target', target);
if (!target) {
return;
}
this.mouseIsDown = target;
const hoverActivationDelay = await this.getHoverActivationDelay();
this.contentWindow.setTimeout(() => {
if (this.mouseIsDown === target) {
this.hasClicked = true;
this.openGlance(target);
}
}, hoverActivationDelay);
}
handleClick(event) {
if (this.ensureOnlyKeyModifiers(event)) {
return;
}
const activationMethod = this._activationMethod;
if (activationMethod === 'ctrl' && !event.ctrlKey) {
return;
} else if (activationMethod === 'alt' && !event.altKey) {
return;
} else if (activationMethod === 'shift' && !event.shiftKey) {
return;
} else if (activationMethod === 'meta' && !event.metaKey) {
return;
}else if (activationMethod === 'mantain' || typeof activationMethod === 'undefined') {
return;
}
// get closest A element
const target = event.target.closest('A');
if (target) {
event.preventDefault();
event.stopPropagation();
this.openGlance(target);
}
}
}

View file

@ -0,0 +1,24 @@
export class ZenGlanceParent extends JSWindowActorParent {
constructor() {
super();
}
async receiveMessage(message) {
switch (message.name) {
case 'ZenGlance:GetActivationMethod': {
return Services.prefs.getStringPref('zen.glance.activation-method', 'ctrl');
}
case 'ZenGlance:GetHoverActivationDelay': {
return Services.prefs.getIntPref('zen.glance.hold-duration', 500);
}
case 'ZenGlance:OpenGlance': {
this.openGlance(this.browsingContext.topChromeWindow, message.data);
break;
}
}
}
openGlance(window, data) {
window.gZenGlanceManager.openGlance(data);
}
}

View file

@ -0,0 +1,142 @@
export class ZenThemeMarketplaceChild extends JSWindowActorChild {
constructor() {
super();
}
handleEvent(event) {
switch (event.type) {
case 'DOMContentLoaded':
this.initiateThemeMarketplace();
this.contentWindow.document.addEventListener('ZenCheckForThemeUpdates', this.checkForThemeUpdates.bind(this));
break;
default:
}
}
// This function will be caleld from about:preferences
checkForThemeUpdates(event) {
event.preventDefault();
this.sendAsyncMessage('ZenThemeMarketplace:CheckForUpdates');
}
initiateThemeMarketplace() {
this.contentWindow.setTimeout(() => {
this.addIntallButtons();
}, 0);
}
get actionButton() {
return this.contentWindow.document.getElementById('install-theme');
}
get actionButtonUnnstall() {
return this.contentWindow.document.getElementById('install-theme-uninstall');
}
async receiveMessage(message) {
switch (message.name) {
case 'ZenThemeMarketplace:ThemeChanged': {
const themeId = message.data.themeId;
const actionButton = this.actionButton;
const actionButtonInstalled = this.actionButtonUnnstall;
if (actionButton && actionButtonInstalled) {
actionButton.disabled = false;
actionButtonInstalled.disabled = false;
if (await this.isThemeInstalled(themeId)) {
actionButton.classList.add('hidden');
actionButtonInstalled.classList.remove('hidden');
} else {
actionButton.classList.remove('hidden');
actionButtonInstalled.classList.add('hidden');
}
}
break;
}
case 'ZenThemeMarketplace:CheckForUpdatesFinished': {
const updates = message.data.updates;
this.contentWindow.document.dispatchEvent(
new CustomEvent('ZenThemeMarketplace:CheckForUpdatesFinished', { detail: { updates } })
);
break;
}
case 'ZenThemeMarketplace:GetThemeInfo': {
const themeId = message.data.themeId;
const theme = await this.getThemeInfo(themeId);
return theme;
}
}
}
async addIntallButtons() {
const actionButton = this.actionButton;
const actionButtonUnnstall = this.actionButtonUnnstall;
const errorMessage = this.contentWindow.document.getElementById('install-theme-error');
if (!actionButton || !actionButtonUnnstall) {
return;
}
errorMessage.classList.add('hidden');
const themeId = actionButton.getAttribute('zen-theme-id');
if (await this.isThemeInstalled(themeId)) {
actionButtonUnnstall.classList.remove('hidden');
} else {
actionButton.classList.remove('hidden');
}
actionButton.addEventListener('click', this.installTheme.bind(this));
actionButtonUnnstall.addEventListener('click', this.uninstallTheme.bind(this));
}
async isThemeInstalled(themeId) {
return await this.sendQuery('ZenThemeMarketplace:IsThemeInstalled', { themeId });
}
addTheme(theme) {
this.sendAsyncMessage('ZenThemeMarketplace:InstallTheme', { theme });
}
getThemeAPIUrl(themeId) {
return `https://zen-browser.github.io/theme-store/themes/${themeId}/theme.json`;
}
async getThemeInfo(themeId) {
const url = this.getThemeAPIUrl(themeId);
console.info('ZTM: Fetching theme info from: ', url);
const data = await fetch(url, {
mode: 'no-cors',
});
if (data.ok) {
try {
const obj = await data.json();
return obj;
} catch (e) {
console.error('ZTM: Error parsing theme info: ', e);
}
} else console.log(data.status);
return null;
}
async uninstallTheme(event) {
const button = event.target;
button.disabled = true;
const themeId = button.getAttribute('zen-theme-id');
console.info('ZTM: Uninstalling theme with id: ', themeId);
this.sendAsyncMessage('ZenThemeMarketplace:UninstallTheme', { themeId });
}
async installTheme(event) {
const button = event.target;
button.disabled = true;
const themeId = button.getAttribute('zen-theme-id');
console.info('ZTM: Installing theme with id: ', themeId);
const theme = await this.getThemeInfo(themeId);
if (!theme) {
console.error('ZTM: Error fetching theme info');
return;
}
this.addTheme(theme);
}
}

View file

@ -0,0 +1,186 @@
export class ZenThemeMarketplaceParent extends JSWindowActorParent {
constructor() {
super();
}
async receiveMessage(message) {
switch (message.name) {
case 'ZenThemeMarketplace:InstallTheme': {
console.info('ZenThemeMarketplaceParent: Updating themes');
const theme = message.data.theme;
theme.enabled = true;
const themes = await this.getThemes();
themes[theme.id] = theme;
this.updateThemes(themes);
this.updateChildProcesses(theme.id);
break;
}
case 'ZenThemeMarketplace:UninstallTheme': {
console.info('ZenThemeMarketplaceParent: Uninstalling theme');
const themeId = message.data.themeId;
const themes = await this.getThemes();
delete themes[themeId];
this.removeTheme(themeId);
this.updateThemes(themes);
this.updateChildProcesses(themeId);
break;
}
case 'ZenThemeMarketplace:IsThemeInstalled': {
const themeId = message.data.themeId;
const themes = await this.getThemes();
return themes[themeId] ? true : false;
}
case 'ZenThemeMarketplace:CheckForUpdates': {
this.checkForThemeUpdates();
break;
}
}
}
compareversion(version1, version2) {
var result = false;
if (typeof version1 !== 'object') {
version1 = version1.toString().split('.');
}
if (typeof version2 !== 'object') {
version2 = version2.toString().split('.');
}
for (var i = 0; i < Math.max(version1.length, version2.length); i++) {
if (version1[i] == undefined) {
version1[i] = 0;
}
if (version2[i] == undefined) {
version2[i] = 0;
}
if (Number(version1[i]) < Number(version2[i])) {
result = true;
break;
}
if (version1[i] != version2[i]) {
break;
}
}
return result;
}
async checkForThemeUpdates() {
console.info('ZenThemeMarketplaceParent: Checking for theme updates');
let updates = [];
this._themes = null;
for (const theme of Object.values(await this.getThemes())) {
const themeInfo = await this.sendQuery('ZenThemeMarketplace:GetThemeInfo', { themeId: theme.id });
if (!themeInfo) {
continue;
}
if (!this.compareversion(themeInfo.version, theme.version || '0.0.0') && themeInfo.version != theme.version) {
console.info('ZenThemeMarketplaceParent: Theme update found', theme.id, theme.version, themeInfo.version);
themeInfo.enabled = theme.enabled;
updates.push(themeInfo);
await this.removeTheme(theme.id, false);
this._themes[themeInfo.id] = themeInfo;
}
}
await this.updateThemes(this._themes);
this.sendAsyncMessage('ZenThemeMarketplace:CheckForUpdatesFinished', { updates });
}
async updateChildProcesses(themeId) {
this.sendAsyncMessage('ZenThemeMarketplace:ThemeChanged', { themeId });
}
async getThemes() {
if (!this._themes) {
if (!(await IOUtils.exists(this.themesDataFile))) {
await IOUtils.writeJSON(this.themesDataFile, {});
}
this._themes = await IOUtils.readJSON(this.themesDataFile);
}
return this._themes;
}
async updateThemes(themes) {
this._themes = themes;
await IOUtils.writeJSON(this.themesDataFile, themes);
await this.checkForThemeChanges();
}
getStyleSheetFullContent(style = '') {
let stylesheet = '@-moz-document url-prefix("chrome:") {';
for (const line of style.split('\n')) {
stylesheet += ` ${line}`;
}
stylesheet += '}';
return stylesheet;
}
async downloadUrlToFile(url, path, isStyleSheet = false) {
try {
const response = await fetch(url);
const data = await response.text();
const content = isStyleSheet ? this.getStyleSheetFullContent(data) : data;
// convert the data into a Uint8Array
let buffer = new TextEncoder().encode(content);
await IOUtils.write(path, buffer);
} catch (e) {
console.error('ZenThemeMarketplaceParent: Error downloading file', url, e);
}
}
async downloadThemeFileContents(theme) {
const themePath = PathUtils.join(this.themesRootPath, theme.id);
await IOUtils.makeDirectory(themePath, { ignoreExisting: true });
await this.downloadUrlToFile(theme.style, PathUtils.join(themePath, 'chrome.css'), true);
await this.downloadUrlToFile(theme.readme, PathUtils.join(themePath, 'readme.md'));
if (theme.preferences) {
await this.downloadUrlToFile(theme.preferences, PathUtils.join(themePath, 'preferences.json'));
}
}
get themesRootPath() {
return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
}
get themesDataFile() {
return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
}
triggerThemeUpdate() {
const pref = 'zen.themes.updated-value-observer';
Services.prefs.setBoolPref(pref, !Services.prefs.getBoolPref(pref));
}
async installTheme(theme) {
await this.downloadThemeFileContents(theme);
}
async checkForThemeChanges() {
const themes = await this.getThemes();
const themeIds = Object.keys(themes);
let changed = false;
for (const themeId of themeIds) {
const theme = themes[themeId];
if (!theme) {
continue;
}
const themePath = PathUtils.join(this.themesRootPath, themeId);
if (!(await IOUtils.exists(themePath))) {
await this.installTheme(theme);
changed = true;
}
}
if (changed) {
this.triggerThemeUpdate();
}
}
async removeTheme(themeId, triggerUpdate = true) {
const themePath = PathUtils.join(this.themesRootPath, themeId);
await IOUtils.remove(themePath, { recursive: true, ignoreAbsent: true });
if (triggerUpdate) {
this.triggerThemeUpdate();
}
}
}