mirror of
https://github.com/zen-browser/desktop.git
synced 2025-07-07 20:39:59 +02:00
Translated files to a single monorepo
This commit is contained in:
parent
f498a64413
commit
ec65a69496
25 changed files with 8696 additions and 26 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
|
19
src/browser/base/zen-components/ZenActorsManager.mjs
Normal file
19
src/browser/base/zen-components/ZenActorsManager.mjs
Normal 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}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
55
src/browser/base/zen-components/ZenCommonUtils.mjs
Normal file
55
src/browser/base/zen-components/ZenCommonUtils.mjs
Normal 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 });
|
||||||
|
}
|
||||||
|
}
|
253
src/browser/base/zen-components/ZenCompactMode.mjs
Normal file
253
src/browser/base/zen-components/ZenCompactMode.mjs
Normal 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');
|
||||||
|
},
|
||||||
|
};
|
352
src/browser/base/zen-components/ZenGlanceManager.mjs
Normal file
352
src/browser/base/zen-components/ZenGlanceManager.mjs
Normal 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();
|
||||||
|
}
|
722
src/browser/base/zen-components/ZenGradientGenerator.mjs
Normal file
722
src/browser/base/zen-components/ZenGradientGenerator.mjs
Normal 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;
|
||||||
|
}
|
1014
src/browser/base/zen-components/ZenKeyboardShortcuts.mjs
Normal file
1014
src/browser/base/zen-components/ZenKeyboardShortcuts.mjs
Normal file
File diff suppressed because it is too large
Load diff
422
src/browser/base/zen-components/ZenPinnedTabManager.mjs
Normal file
422
src/browser/base/zen-components/ZenPinnedTabManager.mjs
Normal 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();
|
||||||
|
}
|
332
src/browser/base/zen-components/ZenPinnedTabsStorage.mjs
Normal file
332
src/browser/base/zen-components/ZenPinnedTabsStorage.mjs
Normal 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));
|
||||||
|
}
|
||||||
|
};
|
138
src/browser/base/zen-components/ZenProfileDialogUI.mjs
Normal file
138
src/browser/base/zen-components/ZenProfileDialogUI.mjs
Normal 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
788
src/browser/base/zen-components/ZenSidebarManager.mjs
Normal file
788
src/browser/base/zen-components/ZenSidebarManager.mjs
Normal 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();
|
283
src/browser/base/zen-components/ZenTabUnloader.mjs
Normal file
283
src/browser/base/zen-components/ZenTabUnloader.mjs
Normal 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();
|
||||||
|
}
|
176
src/browser/base/zen-components/ZenThemeBuilder.mjs
Normal file
176
src/browser/base/zen-components/ZenThemeBuilder.mjs
Normal 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));
|
||||||
|
},
|
||||||
|
};
|
120
src/browser/base/zen-components/ZenThemesCommon.mjs
Normal file
120
src/browser/base/zen-components/ZenThemesCommon.mjs
Normal 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);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
320
src/browser/base/zen-components/ZenThemesImporter.mjs
Normal file
320
src/browser/base/zen-components/ZenThemesImporter.mjs
Normal 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,
|
||||||
|
});
|
1146
src/browser/base/zen-components/ZenViewSplitter.mjs
Normal file
1146
src/browser/base/zen-components/ZenViewSplitter.mjs
Normal file
File diff suppressed because it is too large
Load diff
1211
src/browser/base/zen-components/ZenWorkspaces.mjs
Normal file
1211
src/browser/base/zen-components/ZenWorkspaces.mjs
Normal file
File diff suppressed because it is too large
Load diff
400
src/browser/base/zen-components/ZenWorkspacesStorage.mjs
Normal file
400
src/browser/base/zen-components/ZenWorkspacesStorage.mjs
Normal 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));
|
||||||
|
},
|
||||||
|
};
|
452
src/browser/base/zen-components/ZenWorkspacesSync.mjs
Normal file
452
src/browser/base/zen-components/ZenWorkspacesSync.mjs
Normal 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);
|
||||||
|
|
||||||
|
|
119
src/browser/base/zen-components/actors/ZenGlanceChild.sys.mjs
Normal file
119
src/browser/base/zen-components/actors/ZenGlanceChild.sys.mjs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue