Merge branch 'dev' into glance-buttons-overflow

This commit is contained in:
Vrezh Fedora
2025-03-14 22:22:20 +09:00
212 changed files with 2915 additions and 4476 deletions

View File

@@ -37,6 +37,9 @@ var gZenCompactModeManager = {
this.addMouseActions();
this.addContextMenu();
// Clear hover states when window state changes (minimize, maximize, etc.)
window.addEventListener('sizemodechange', () => this._clearAllHoverStates());
},
get preference() {
@@ -141,10 +144,17 @@ var gZenCompactModeManager = {
this.animateCompactMode();
},
getAndApplySidebarWidth() {
// NOTE: Dont actually use event, it's just so we make sure
// the caller is from the ResizeObserver
getAndApplySidebarWidth(event = undefined) {
let sidebarWidth = this.sidebar.getBoundingClientRect().width;
if (sidebarWidth > 1) {
gZenUIManager.restoreScrollbarState();
// Second variable to get the genuine width of the sidebar
this.sidebar.style.setProperty('--actual-zen-sidebar-width', `${sidebarWidth}px`);
if (event && this.preference) {
return;
}
this.sidebar.style.setProperty('--zen-sidebar-width', `${sidebarWidth}px`);
}
return sidebarWidth;
@@ -158,10 +168,15 @@ var gZenCompactModeManager = {
const canAnimate =
lazyCompactMode.COMPACT_MODE_CAN_ANIMATE_SIDEBAR &&
!this.sidebar.hasAttribute('zen-user-show') &&
!this.sidebar.hasAttribute('zen-has-empty-tab') &&
!this.sidebar.hasAttribute('zen-has-hover');
// Do this so we can get the correct width ONCE compact mode styled have been applied
const titlebar = this.sidebar.querySelector('#titlebar');
if (canAnimate) {
this.sidebar.setAttribute('animate', 'true');
titlebar.setAttribute('has-animated-padding', 'true');
} else {
titlebar.removeAttribute('has-animated-padding');
}
this.sidebar.style.removeProperty('margin-right');
this.sidebar.style.removeProperty('margin-left');
@@ -191,15 +206,18 @@ var gZenCompactModeManager = {
)
.then(() => {
window.requestAnimationFrame(() => {
this.sidebar.style.transition = 'none';
this.sidebar.removeAttribute('animate');
this.sidebar.style.visibility = 'hidden';
this.sidebar.style.removeProperty('margin-right');
this.sidebar.style.removeProperty('margin-left');
this.sidebar.style.removeProperty('transform');
this.sidebar.style.transition = 'none';
setTimeout(() => {
this._animating = false;
this.sidebar.style.removeProperty('visibility');
this.sidebar.style.removeProperty('transition');
});
this.sidebar.style.removeProperty('opacity');
}, 300);
});
});
} else if (canHideSidebar && !isCompactMode) {
@@ -308,9 +326,6 @@ var gZenCompactModeManager = {
{
element: gZenVerticalTabsManager.actualWindowButtons,
},
{
element: gZenVerticalTabsManager.actualWindowButtons,
},
];
},
@@ -346,14 +361,13 @@ var gZenCompactModeManager = {
addMouseActions() {
for (let i = 0; i < this.hoverableElements.length; i++) {
let target = this.hoverableElements[i].element;
target.addEventListener('mouseenter', (event) => {
if (!event.target.matches(':hover')) return;
const onEnter = (event) => {
if (event.type === 'mouseenter' && !event.target.matches(':hover')) return;
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
const onLeave = (event) => {
if (AppConstants.platform == 'macosx') {
const buttonRect = gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect();
const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
@@ -368,6 +382,12 @@ var gZenCompactModeManager = {
}
}
// When moving the cursor between the url bar and bookmarks, or in-between bookmarks in the bookmark bar, the
// mouseLeave event is triggered without a relatedTarget.
if (event.relatedTarget == null) {
return;
}
// If it's a child element but not the target, ignore the event
if (target.contains(event.explicitOriginalTarget) && event.explicitOriginalTarget !== target) {
return;
@@ -378,7 +398,13 @@ var gZenCompactModeManager = {
} else {
this._removeHoverFrames[target.id] = window.requestAnimationFrame(() => target.removeAttribute('zen-has-hover'));
}
});
};
target.addEventListener('mouseenter', onEnter);
target.addEventListener('dragover', onEnter);
target.addEventListener('mouseleave', onLeave);
target.addEventListener('dragleave', onLeave);
}
document.documentElement.addEventListener('mouseleave', (event) => {
@@ -427,4 +453,15 @@ var gZenCompactModeManager = {
let toolbar = document.getElementById('zen-appcontent-navbar-container');
toolbar.toggleAttribute('zen-user-show');
},
_clearAllHoverStates() {
// Clear hover attributes from all hoverable elements
for (let entry of this.hoverableElements) {
const target = entry.element;
if (target && !target.matches(':hover') && target.hasAttribute('zen-has-hover')) {
target.removeAttribute('zen-has-hover');
this.clearFlashTimeout('has-hover' + target.id);
}
}
},
};

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,9 @@
#glances = new Map();
#currentGlanceID = null;
#confirmationTimeout = null;
init() {
window.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('TabClose', this.onTabClose.bind(this));
window.addEventListener('TabSelect', this.onLocationChange.bind(this));
@@ -33,16 +34,6 @@
return this.#glances.get(this.#currentGlanceID)?.parentTab;
}
// If the user presses Escape while a Glance is open, close it
onKeyDown(event) {
if (event.defaultPrevented) return;
if (event.key === 'Escape' && this.#currentGlanceID) {
event.preventDefault();
event.stopPropagation();
this.closeGlance({ onTabClose: true });
}
}
onOverlayClick(event) {
// If user clicks outside content area, close glance
if (event.target === this.overlay && event.originalTarget !== this.contentWrapper) {
@@ -67,7 +58,7 @@
// Figure out where to insert new tabs
getTabPosition(tab) {
return Math.max(gBrowser._numVisiblePinTabs, tab._tPos);
return Math.max(gBrowser.pinnedTabCount, tab._tPos);
}
// Create a new tab for Glance
@@ -77,7 +68,7 @@
skipBackgroundNotify: true,
insertTab: true,
skipLoad: false,
index: this.getTabPosition(currentTab),
index: this.getTabPosition(currentTab) + 1,
};
currentTab._selected = true;
@@ -247,11 +238,26 @@
});
}
closeGlance({ noAnimation = false, onTabClose = false, setNewID = null, isDifferent = false } = {}) {
closeGlance({ noAnimation = false, onTabClose = false, setNewID = null, isDifferent = false, hasFocused = false } = {}) {
if (this._animating || !this.#currentBrowser || this.animatingOpen || this._duringOpening) {
return;
}
let { permitUnload } = this.#currentBrowser.permitUnload();
if (!permitUnload) {
return;
}
if (onTabClose && hasFocused && !this.#confirmationTimeout) {
const cancelButton = document.getElementById('zen-glance-sidebar-close');
cancelButton.setAttribute('waitconfirmation', true);
this.#confirmationTimeout = setTimeout(() => {
cancelButton.removeAttribute('waitconfirmation');
this.#confirmationTimeout = null;
}, 3000);
return;
}
this.browserWrapper.removeAttribute('has-finished-animation');
if (noAnimation) {
this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer').removeAttribute('style');
@@ -301,16 +307,15 @@
.then(() => {
this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer').removeAttribute('style');
});
// Animate the browser wrapper back
this.browserWrapper.style.opacity = 1;
gZenUIManager.motion
.animate(
this.browserWrapper,
{
...originalPosition,
opacity: 0.3,
opacity: 0,
},
{ type: 'spring', bounce: 0, duration: 0.4, easing: 'ease' }
{ type: 'spring', bounce: 0, duration: 0.5, easing: 'ease-in' }
)
.then(() => {
this.browserWrapper.removeAttribute('animate');
@@ -336,8 +341,7 @@
this.#currentParentTab._visuallySelected = false;
}
// Reset everything
const prevOverlay = this.overlay;
// reset everything
this.browserWrapper = null;
this.overlay = null;
this.contentWrapper = null;
@@ -349,7 +353,7 @@
gBrowser.selectedTab = this.#currentParentTab;
}
this._ignoreClose = true;
gBrowser.removeTab(this.lastCurrentTab, { animate: true });
gBrowser.removeTab(this.lastCurrentTab, { animate: true, skipPermitUnload: true });
gBrowser.tabContainer._invalidateCachedTabs();
this.#currentParentTab.removeAttribute('glance-id');
@@ -437,9 +441,18 @@
}
}
// If location changes to a tab with a different glance-id, close the old one
clearConfirmationTimeout() {
if (this.#confirmationTimeout) {
clearTimeout(this.#confirmationTimeout);
this.#confirmationTimeout = null;
}
document.getElementById('zen-glance-sidebar-close')?.removeAttribute('waitconfirmation');
}
// note: must be sync to avoid timing issues
onLocationChange(event) {
const tab = event.target;
this.clearConfirmationTimeout();
if (this.animatingFullOpen || this.closingGlance) {
return;
}
@@ -502,7 +515,6 @@
owner &&
owner.pinned &&
this._lazyPref.SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE &&
owner.linkedBrowser?.docShellIsActive &&
owner.linkedBrowser?.browsingContext?.isAppTab &&
this.tabDomainsDiffer(owner, uri) &&
Services.prefs.getBoolPref('zen.glance.enabled', true)
@@ -515,7 +527,11 @@
try {
if (this.shouldOpenTabInGlance(tab, uri)) {
const browserRect = gBrowser.tabbox.getBoundingClientRect();
this.openGlance({ url: undefined, x: browserRect.width / 2, y: browserRect.height / 2, width: 0, height: 0 });
this.openGlance(
{ url: undefined, x: browserRect.width / 2, y: browserRect.height / 2, width: 0, height: 0 },
tab,
tab.owner
);
}
} catch (e) {
console.error(e);
@@ -610,8 +626,12 @@
esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceChild.sys.mjs',
events: {
DOMContentLoaded: {},
keydown: {
capture: true,
},
},
},
allFrames: true,
matches: ['https://*/*'],
});
}

View File

@@ -863,7 +863,7 @@
getToolbarModifiedBase() {
return this.isDarkMode
? 'color-mix(in srgb, var(--zen-themed-toolbar-bg) 80%, #fff 20%)'
: 'color-mix(in srgb, var(--zen-themed-toolbar-bg) 95%, #000 5%)';
: 'color-mix(in srgb, var(--zen-themed-toolbar-bg) 95%, #000 6%)';
}
getSingleRGBColor(color, forToolbar = false) {

View File

@@ -0,0 +1,322 @@
class ZenMediaController {
_currentMediaController = null;
_currentBrowser = null;
_mediaUpdateInterval = null;
mediaTitle = null;
mediaArtist = null;
mediaControlBar = null;
mediaServiceIcon = null;
mediaServiceTitle = null;
mediaProgressBar = null;
mediaCurrentTime = null;
mediaDuration = null;
mediaFocusButton = null;
mediaProgressBarContainer = null;
supportedKeys = ['playpause', 'previoustrack', 'nexttrack'];
init() {
this.mediaTitle = document.querySelector('#zen-media-title');
this.mediaArtist = document.querySelector('#zen-media-artist');
this.mediaControlBar = document.querySelector('#zen-media-controls-toolbar');
this.mediaServiceIcon = document.querySelector('#zen-media-service-button > image');
this.mediaServiceTitle = document.querySelector('#zen-media-service-title');
this.mediaProgressBar = document.querySelector('#zen-media-progress-bar');
this.mediaCurrentTime = document.querySelector('#zen-media-current-time');
this.mediaDuration = document.querySelector('#zen-media-duration');
this.mediaFocusButton = document.querySelector('#zen-media-focus-button');
this.mediaProgressBarContainer = document.querySelector('#zen-media-progress-hbox');
window.addEventListener('TabSelect', (event) => {
if (this._currentBrowser) {
if (event.target.linkedBrowser.browserId === this._currentBrowser.browserId) {
gZenUIManager.motion
.animate(this.mediaControlBar, {
opacity: [1, 0],
y: [0, 10],
})
.then(() => {
this.mediaControlBar.setAttribute('hidden', 'true');
});
} else if (this.mediaControlBar.hasAttribute('hidden')) {
this.mediaControlBar.removeAttribute('hidden');
window.requestAnimationFrame(() => {
this.mediaControlBar.style.height =
this.mediaControlBar.querySelector('toolbaritem').getBoundingClientRect().height + 'px';
gZenUIManager.motion.animate(
this.mediaControlBar,
{
opacity: [0, 1],
y: [10, 0],
},
{}
);
});
}
gZenUIManager.updateTabsToolbar();
}
});
window.addEventListener('TabClose', (event) => {
if (this._currentBrowser) {
if (event.target.linkedBrowser.browserId === this._currentBrowser.browserId) {
this.deinitMediaController(this._currentMediaController);
}
}
});
}
/**
* Deinitializes a media controller, removing all event listeners and resetting state.
* @param {Object} mediaController - The media controller to deinitialize.
*/
deinitMediaController(mediaController) {
if (!mediaController) return;
mediaController.onpositionstatechange = null;
mediaController.onplaybackstatechange = null;
mediaController.onsupportedkeyschange = null;
mediaController.onmetadatachange = null;
mediaController.ondeactivated = null;
this._currentMediaController = null;
this._currentBrowser = null;
if (this._mediaUpdateInterval) {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
this.mediaControlBar.setAttribute('hidden', 'true');
this.mediaControlBar.removeAttribute('muted');
this.mediaControlBar.classList.remove('playing');
gZenUIManager.updateTabsToolbar();
}
/**
* Sets up the media control UI with metadata and position state.
* @param {Object} metadata - The media metadata (title, artist, etc.).
* @param {Object} positionState - The position state (position, duration).
*/
setupMediaControl(metadata, positionState) {
if (!this.mediaControlBar.classList.contains('playing')) {
this.mediaControlBar.classList.add('playing');
}
// Have it displayed as e.g. <white>youtube</white><grey>.com</grey>
let host = this._currentBrowser._originalURI.displayHost;
if (host.startsWith('www.')) host = host.slice(4);
// note: we might have subdomains, so we need to split the host
const [service, ...tld] = host.split('.');
this.mediaServiceTitle.querySelector('.service').textContent = service;
this.mediaServiceTitle.querySelector('.tld').textContent = '.' + tld.join('.');
this.mediaServiceIcon.src = this._currentBrowser.mIconURL;
this.mediaFocusButton.style.listStyleImage = `url(${this._currentBrowser.mIconURL})`;
this.mediaTitle.textContent = metadata.title || '';
this.mediaArtist.textContent = metadata.artist || '';
gZenUIManager.updateTabsToolbar();
this._currentPosition = positionState.position;
this._currentDuration = positionState.duration;
this.updateMediaPosition();
for (const key of this.supportedKeys) {
const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
button.disabled = !this._currentMediaController.supportedKeys.includes(key);
}
}
/**
* @param {Object} mediaController - The media controller to activate.
* @param {Object} browser - The browser associated with the media controller.
*/
activateMediaControls(mediaController, browser) {
this.updateMuteState();
if (this._currentBrowser?.browserId === browser.browserId) return;
else {
this.deinitMediaController(this._currentMediaController);
this._currentMediaController = mediaController;
this._currentBrowser = browser;
}
mediaController.onpositionstatechange = this.onPositionstateChange.bind(this);
mediaController.onplaybackstatechange = this.onPlaybackstateChange.bind(this);
mediaController.onsupportedkeyschange = this.onSupportedKeysChange.bind(this);
mediaController.onmetadatachange = this.onMetadataChange.bind(this);
mediaController.ondeactivated = this.onDeactivated.bind(this);
const metadata = mediaController.getMetadata();
const positionState = mediaController.getPositionState();
this.setupMediaControl(metadata, positionState);
}
/**
* @param {Event} event - The deactivation event.
*/
onDeactivated(event) {
if (event.target === this._currentMediaController) {
this.deinitMediaController(event.target);
}
}
/**
* Updates playback state and UI based on changes.
* @param {Event} event - The playback state change event.
*/
onPlaybackstateChange(event) {
this.mediaControlBar.classList.toggle('playing', event.target.isPlaying);
}
/**
* Updates supported keys in the UI.
* @param {Event} event - The supported keys change event.
*/
onSupportedKeysChange(event) {
for (const key of this.supportedKeys) {
const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
button.disabled = !event.target.supportedKeys.includes(key);
}
}
/**
* Updates position state and UI when the media position changes.
* @param {Event} event - The position state change event.
*/
onPositionstateChange(event) {
if (event.target !== this._currentMediaController) return;
this._currentPosition = event.position;
this._currentDuration = event.duration;
this.updateMediaPosition();
}
/**
* Updates the media progress bar and time display.
*/
updateMediaPosition() {
if (this._mediaUpdateInterval) {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
if (this._currentDuration >= 900_000) return this.mediaProgressBarContainer.setAttribute('hidden', 'true');
else this.mediaProgressBarContainer.removeAttribute('hidden');
this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
this.mediaDuration.textContent = this.formatSecondsToTime(this._currentDuration);
this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
if (this._currentMediaController?.isPlaying) {
this._mediaUpdateInterval = setInterval(() => {
if (this._currentMediaController?.isPlaying) {
this._currentPosition += 1;
if (this._currentPosition > this._currentDuration) {
this._currentPosition = this._currentDuration;
}
this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
} else {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
}, 1000);
}
}
/**
* Formats seconds into a hours:minutes:seconds string.
* @param {number} seconds - The time in seconds.
* @returns {string} Formatted time string.
*/
formatSecondsToTime(seconds) {
if (!seconds || isNaN(seconds)) return '0:00';
const totalSeconds = Math.max(0, Math.ceil(seconds));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60).toString();
const secs = (totalSeconds % 60).toString();
if (hours > 0) {
return `${hours}:${minutes.padStart(2, '0')}:${secs.padStart(2, '0')}`;
}
return `${minutes}:${secs.padStart(2, '0')}`;
}
/**
* Updates metadata in the UI.
* @param {Event} event - The metadata change event.
*/
onMetadataChange(event) {
const metadata = event.target.getMetadata();
this.mediaTitle.textContent = metadata.title || '';
this.mediaArtist.textContent = metadata.artist || '';
}
onMediaPlayPrev() {
if (this._currentMediaController?.supportedKeys.includes('previoustrack')) {
this._currentMediaController.prevTrack();
}
}
onMediaPlayNext() {
if (this._currentMediaController?.supportedKeys.includes('nexttrack')) {
this._currentMediaController.nextTrack();
}
}
onMediaSeekDrag(event) {
this._currentMediaController?.pause();
const newTime = (event.target.value / 100) * this._currentDuration;
this.mediaCurrentTime.textContent = this.formatSecondsToTime(newTime);
}
onMediaSeekComplete(event) {
const newPosition = (event.target.value / 100) * this._currentDuration;
if (this._currentMediaController?.supportedKeys.includes('seekto')) {
this._currentMediaController.seekTo(newPosition);
this._currentMediaController.play();
}
}
onMediaFocus() {
this._currentMediaController?.focus();
}
onMediaMute() {
if (!this.mediaControlBar.hasAttribute('muted')) {
this._currentBrowser.mute();
this.mediaControlBar.setAttribute('muted', '');
} else {
this._currentBrowser.unmute();
this.mediaControlBar.removeAttribute('muted');
}
}
onMediaToggle() {
if (this.mediaControlBar.classList.contains('playing')) {
this._currentMediaController?.pause();
} else {
this._currentMediaController?.play();
}
}
updateMuteState() {
if (!this._currentBrowser) return;
if (this._currentBrowser._audioMuted) {
this.mediaControlBar.setAttribute('muted', '');
} else {
this.mediaControlBar.removeAttribute('muted');
}
}
}
window.gZenMediaController = new ZenMediaController();

View File

@@ -211,8 +211,9 @@
continue;
}
if (pin.title && pin.editedTitle) {
gBrowser._setTabLabel(tab, pin.title);
if (pin.title && (pin.editedTitle || tab.hasAttribute('zen-has-static-label'))) {
tab.removeAttribute('zen-has-static-label'); // So we can set it again
gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true });
tab.setAttribute('zen-has-static-label', 'true');
}
}
@@ -339,7 +340,7 @@
tab.position = tab._tPos;
for (let otherTab of gBrowser.tabs) {
if (otherTab.pinned) {
if (otherTab.pinned && otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')) {
const actualPin = this._pinsCache.find((pin) => pin.uuid === otherTab.getAttribute('zen-pin-id'));
if (!actualPin) {
continue;
@@ -356,6 +357,13 @@
}
actualPin.position = tab.position;
actualPin.isEssential = tab.hasAttribute('zen-essential');
// There was a bug where the title and hasStaticLabel attribute were not being set
// This is a workaround to fix that
if (tab.hasAttribute('zen-has-static-label')) {
actualPin.editedTitle = true;
actualPin.title = tab.label;
}
await this.savePin(actualPin);
}
@@ -458,6 +466,7 @@
if (!isClosing) {
tab.removeAttribute('zen-pin-id');
tab.removeAttribute('zen-essential'); // Just in case
if (!tab.hasAttribute('zen-workspace-id') && ZenWorkspaces.workspaceEnabled) {
const workspace = await ZenWorkspaces.getActiveWorkspace();
@@ -600,7 +609,14 @@
}
addToEssentials(tab) {
const tabs = tab ? [tab] : TabContextMenu.contextTab.multiselected ? gBrowser.selectedTabs : [TabContextMenu.contextTab];
const tabs = tab
? // if it's already an array, dont make it [tab]
tab?.length
? tab
: [tab]
: TabContextMenu.contextTab.multiselected
? gBrowser.selectedTabs
: [TabContextMenu.contextTab];
for (let i = 0; i < tabs.length; i++) {
const tab = tabs[i];
if (tab.hasAttribute('zen-essential')) {
@@ -621,8 +637,8 @@
} else {
gBrowser.pinTab(tab);
}
this.onTabIconChanged(tab);
this._onTabMove(tab);
this.onTabIconChanged(tab);
}
gZenUIManager.updateTabsToolbar();
}
@@ -701,72 +717,77 @@
}
moveToAnotherTabContainerIfNecessary(event, movingTabs) {
const pinnedTabsTarget =
event.target.closest('#vertical-pinned-tabs-container') || event.target.closest('.zen-current-workspace-indicator');
const essentialTabsTarget = event.target.closest('#zen-essentials-container');
const tabsTarget = event.target.closest('#tabbrowser-arrowscrollbox');
try {
const pinnedTabsTarget =
event.target.closest('#vertical-pinned-tabs-container') || event.target.closest('.zen-current-workspace-indicator');
const essentialTabsTarget = event.target.closest('#zen-essentials-container');
const tabsTarget = event.target.closest('#tabbrowser-arrowscrollbox');
let isVertical = this.expandedSidebarMode;
let moved = false;
for (const draggedTab of movingTabs) {
let isRegularTabs = false;
// Check for pinned tabs container
if (pinnedTabsTarget) {
if (!draggedTab.pinned) {
gBrowser.pinTab(draggedTab);
moved = true;
} else if (draggedTab.hasAttribute('zen-essential')) {
this.removeEssentials(draggedTab, false);
moved = true;
}
}
// Check for essentials container
else if (essentialTabsTarget) {
if (!draggedTab.hasAttribute('zen-essential')) {
this.addToEssentials(draggedTab);
moved = true;
isVertical = false;
}
}
// Check for normal tabs container
else if (tabsTarget || event.target.id === 'zen-tabs-wrapper') {
if (draggedTab.pinned && !draggedTab.hasAttribute('zen-essential')) {
gBrowser.unpinTab(draggedTab);
moved = true;
isRegularTabs = true;
} else if (draggedTab.hasAttribute('zen-essential')) {
this.removeEssentials(draggedTab);
moved = true;
isRegularTabs = true;
}
}
// If the tab was moved, adjust its position relative to the target tab
if (moved) {
const targetTab = event.target.closest('.tabbrowser-tab');
if (targetTab) {
const rect = targetTab.getBoundingClientRect();
let newIndex = targetTab._tPos;
if (isVertical) {
const middleY = targetTab.screenY + rect.height / 2;
if (!isRegularTabs && event.screenY > middleY) {
newIndex++;
} else if (isRegularTabs && event.screenY < middleY) {
newIndex--;
}
} else {
const middleX = targetTab.screenX + rect.width / 2;
if (event.screenX > middleX) {
newIndex++;
}
let isVertical = this.expandedSidebarMode;
let moved = false;
for (const draggedTab of movingTabs) {
let isRegularTabs = false;
// Check for pinned tabs container
if (pinnedTabsTarget) {
if (!draggedTab.pinned) {
gBrowser.pinTab(draggedTab);
moved = true;
} else if (draggedTab.hasAttribute('zen-essential')) {
this.removeEssentials(draggedTab, false);
moved = true;
}
}
// Check for essentials container
else if (essentialTabsTarget) {
if (!draggedTab.hasAttribute('zen-essential') && !draggedTab?.group?.hasAttribute('split-view-group')) {
this.addToEssentials(draggedTab);
moved = true;
isVertical = false;
}
}
// Check for normal tabs container
else if (tabsTarget || event.target.id === 'zen-tabs-wrapper') {
if (draggedTab.pinned && !draggedTab.hasAttribute('zen-essential')) {
gBrowser.unpinTab(draggedTab);
moved = true;
isRegularTabs = true;
} else if (draggedTab.hasAttribute('zen-essential')) {
this.removeEssentials(draggedTab);
moved = true;
isRegularTabs = true;
}
}
// If the tab was moved, adjust its position relative to the target tab
if (moved) {
const targetTab = event.target.closest('.tabbrowser-tab');
if (targetTab) {
const rect = targetTab.getBoundingClientRect();
let newIndex = targetTab._tPos;
if (isVertical) {
const middleY = targetTab.screenY + rect.height / 2;
if (!isRegularTabs && event.screenY > middleY) {
newIndex++;
} else if (isRegularTabs && event.screenY < middleY) {
newIndex--;
}
} else {
const middleX = targetTab.screenX + rect.width / 2;
if (event.screenX > middleX) {
newIndex++;
}
}
gBrowser.moveTabTo(draggedTab, newIndex);
}
gBrowser.moveTabTo(draggedTab, newIndex);
}
}
}
return moved;
return moved;
} catch (ex) {
console.error('Error moving tabs:', ex);
return false;
}
}
async onLocationChange(browser) {
@@ -837,6 +858,15 @@
// update the label for the same pin across all windows
for (const browser of browsers) {
const tabs = browser.gBrowser.tabs;
// Fix pinned cache for the browser
const browserCache = browser.gZenPinnedTabManager?._pinsCache;
if (browserCache) {
const pin = browserCache.find((pin) => pin.uuid === uuid);
if (pin) {
pin.title = newTitle;
pin.editedTitle = isEdited;
}
}
for (let i = 0; i < tabs.length; i++) {
const tabToEdit = tabs[i];
if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) {

View File

@@ -176,7 +176,7 @@
<checkbox data-l10n-id="zen-rice-share-include-workspace-themes" id="zen-rice-share-include-workspace-themes" />
</vbox>
<html:moz-button-group class="panel-footer">
<button onclick="gZenThemePicker.riceManager.cancel()" class="footer-button" data-l10n-id="zen-rice-share-cancel" />
<button onclick="gZenThemePicker.riceManager.cancel()" class="footer-button" data-l10n-id="zen-general-cancel" />
<button onclick="gZenThemePicker.riceManager.submit()" class="footer-button" data-l10n-id="zen-rice-share-save" default="true" slot="primary" id="zen-rice-share-save" disabled="true" />
</html:moz-button-group>
</vbox>

View File

@@ -27,7 +27,6 @@ class ZenBrowserManagerSidebar extends ZenDOMOperatedFeature {
this.onlySafeWidthAndHeight();
this.initProgressListener();
this.update();
this.close(); // avoid caching
this.tabBox.prepend(this.sidebarWrapper);
this.listenForPrefChanges();
@@ -102,7 +101,7 @@ class ZenBrowserManagerSidebar extends ZenDOMOperatedFeature {
syncPinnedState() {
const sidebar = document.getElementById('zen-sidebar-web-panel');
const pinButton = document.getElementById('zen-sidebar-web-panel-pinned');
if (sidebar.hasAttribute('pinned')) {
pinButton.setAttribute('pinned', 'true');
} else {
@@ -182,8 +181,8 @@ class ZenBrowserManagerSidebar extends ZenDOMOperatedFeature {
const leftMouseOffset = startLeft - mouseDownEvent.screenX;
const moveListener = (mouseMoveEvent) => {
window.requestAnimationFrame(() => {
let top = mouseMoveEvent.screenY + topMouseOffset;
let left = mouseMoveEvent.screenX + leftMouseOffset;
let top = mouseMoveEvent.clientY + topMouseOffset;
let left = mouseMoveEvent.clientX + leftMouseOffset;
const wrapperBounds = this.sidebarWrapper.getBoundingClientRect();
top = Math.max(0, Math.min(top, wrapperBounds.height - sideBarHeight));
@@ -557,16 +556,18 @@ class ZenBrowserManagerSidebar extends ZenDOMOperatedFeature {
_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);
}
});
if (Services.prefs.getBoolPref('zen.sidebar.use-google-favicons')) {
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) {

View File

@@ -264,7 +264,7 @@
(tab.pinned && !ignoreTimestamp) ||
tab.selected ||
(tab.multiselected && !ignoreTimestamp) ||
tab.hasAttribute('busy') ||
(tab.hasAttribute('busy') && !ignoreTimestamp) ||
tab.hasAttribute('pending') ||
!tab.linkedPanel ||
tab.splitView ||
@@ -274,7 +274,8 @@
(tab.pictureinpicture && !ignoreTimestamp) ||
(tab.soundPlaying && !ignoreTimestamp) ||
(tab.zenIgnoreUnload && !ignoreTimestamp) ||
excludedUrls.some((url) => url.test(tab.linkedBrowser?.currentURI.spec))
(excludedUrls.some((url) => url.test(tab.linkedBrowser?.currentURI.spec)) &&
tab.linkedBrowser?.currentURI.spec !== 'about:blank')
) {
return false;
}

View File

@@ -51,7 +51,7 @@ class SplitNode extends SplitLeafNode {
addChild(child) {
child.parent = this;
this._children.push(child);
this._children.unshift(child);
}
}
@@ -69,6 +69,8 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
_edgeHoverSize;
minResizeWidth;
_lastOpenedTab = null;
MAX_TABS = 4;
init() {
@@ -81,9 +83,17 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
ChromeUtils.defineLazyGetter(this, 'dropZone', () => document.getElementById('zen-splitview-dropzone'));
window.addEventListener('TabClose', this.handleTabClose.bind(this));
window.addEventListener('TabSelect', this.onTabSelect.bind(this));
this.initializeContextMenu();
this.insertPageActionButton();
this.insertIntoContextMenu();
// Add drag over listener to the browser view
if (Services.prefs.getBoolPref('zen.splitView.enable-tab-drop')) {
const tabBox = document.getElementById('tabbrowser-tabbox');
tabBox.addEventListener('dragover', this.onBrowserDragOverToSplit.bind(this));
this.onBrowserDragEndToSplit = this.onBrowserDragEndToSplit.bind(this);
}
}
insertIntoContextMenu() {
@@ -102,6 +112,9 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
*/
handleTabClose(event) {
const tab = event.target;
if (tab === this._lastOpenedTab) {
this._lastOpenedTab = null;
}
const groupIndex = this._data.findIndex((group) => group.tabs.includes(tab));
if (groupIndex < 0) {
return;
@@ -109,6 +122,18 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
this.removeTabFromGroup(tab, groupIndex, event.forUnsplit);
}
/**
* @param {Event} event - The event that triggered the tab select.
* @description Handles the tab select event.
* @returns {void}
*/
onTabSelect(event) {
const previousTab = event.detail.previousTab;
if (previousTab && !previousTab.hasAttribute('zen-empty-tab')) {
this._lastOpenedTab = previousTab;
}
}
/**
* Removes a tab from a group.
*
@@ -142,6 +167,188 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
}
}
onBrowserDragOverToSplit(event) {
var dt = event.dataTransfer;
var draggedTab;
if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) {
// tab copy or move
draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
// not our drop then
if (!draggedTab || gBrowser.selectedTab.hasAttribute('zen-empty-tab')) {
return;
}
draggedTab.container._finishMoveTogetherSelectedTabs(draggedTab);
}
if (
!draggedTab ||
this._canDrop ||
this._hasAnimated ||
this.fakeBrowser ||
!this._lastOpenedTab ||
(this._lastOpenedTab &&
(this._lastOpenedTab.getAttribute('zen-workspace-id') !== draggedTab.getAttribute('zen-workspace-id') ||
this._lastOpenedTab.hasAttribute('zen-essential')))
) {
return;
}
if (draggedTab.splitView) {
return;
}
const currentView = this._data[this._lastOpenedTab.splitViewValue];
if (currentView?.tabs.length >= this.MAX_TABS) {
return;
}
const panelsRect = gBrowser.tabbox.getBoundingClientRect();
const panelsWidth = panelsRect.width;
if (
event.clientX > panelsRect.left + panelsWidth - 10 ||
event.clientX < panelsRect.left + 10 ||
event.clientY < panelsRect.top + 10 ||
event.clientY > panelsRect.bottom - 10
) {
return;
}
// first quarter or last quarter of the screen, but not the middle
if (!(event.clientX < panelsRect.left + panelsWidth / 4 || event.clientX > panelsRect.left + (panelsWidth / 4) * 3)) {
return;
}
dt.mozCursor = 'default';
const oldTab = this._lastOpenedTab;
this._canDrop = true;
{
this._draggingTab = draggedTab;
gBrowser.selectedTab = oldTab;
this._hasAnimated = true;
for (const tab of gBrowser.tabs) {
tab.style.removeProperty('transform');
}
const panelsWidth = gBrowser.tabbox.getBoundingClientRect().width;
const halfWidth = panelsWidth / 2;
const side = event.clientX > halfWidth ? 'right' : 'left';
this.fakeBrowser = document.createXULElement('vbox');
this.fakeBrowser.addEventListener('dragleave', this.onBrowserDragEndToSplit);
window.addEventListener('dragend', this.onBrowserDragEndToSplit, { once: true });
const padding = Services.prefs.getIntPref('zen.theme.content-element-separation', 0);
this.fakeBrowser.setAttribute('flex', '1');
this.fakeBrowser.id = 'zen-split-view-fake-browser';
gBrowser.tabbox.appendChild(this.fakeBrowser);
this.fakeBrowser.style.setProperty('--zen-split-view-fake-icon', `url(${draggedTab.getAttribute('image')})`);
draggedTab._visuallySelected = true;
this.fakeBrowser.setAttribute('side', side);
this._finishAllAnimatingPromise = Promise.all([
gZenUIManager.motion.animate(
gBrowser.tabbox,
side === 'left'
? {
paddingLeft: [0, `${halfWidth}px`],
paddingRight: 0,
}
: {
paddingRight: [0, `${halfWidth}px`],
paddingLeft: 0,
},
{
duration: 0.08,
easing: 'ease-out',
}
),
gZenUIManager.motion.animate(
this.fakeBrowser,
{
width: [0, `${halfWidth - padding}px`],
...(side === 'left'
? {
marginLeft: [0, `${-halfWidth}px`],
}
: {}),
},
{
duration: 0.08,
easing: 'ease-out',
}
),
]);
if (this._finishAllAnimatingPromise) {
this._finishAllAnimatingPromise.then(() => {
this._canDrop = true;
draggedTab._visuallySelected = true;
});
}
}
}
onBrowserDragEndToSplit(event) {
if (!this._canDrop) {
return;
}
const panelsRect = gBrowser.tabbox.getBoundingClientRect();
const fakeBrowserRect = this.fakeBrowser && this.fakeBrowser.getBoundingClientRect();
if (
(event.target.closest('#tabbrowser-tabbox') && event.target != this.fakeBrowser) ||
(fakeBrowserRect &&
event.clientX > fakeBrowserRect.left &&
event.clientX < fakeBrowserRect.left + fakeBrowserRect.width &&
event.clientY > fakeBrowserRect.top &&
event.clientY < fakeBrowserRect.top + fakeBrowserRect.height) ||
(event.screenX === 0 && event.screenY === 0) // It's equivalent to 0 if the event has been dropped
) {
return;
}
if (!this._hasAnimated || !this.fakeBrowser) {
return;
}
const panelsWidth = panelsRect.width;
const halfWidth = panelsWidth / 2;
const padding = Services.prefs.getIntPref('zen.theme.content-element-separation', 0);
if (!this.fakeBrowser) {
return;
}
this.fakeBrowser.classList.add('fade-out');
const side = this.fakeBrowser.getAttribute('side');
if (this._draggingTab) this._draggingTab.setAttribute('zen-has-splitted', 'true');
this._lastOpenedTab = gBrowser.selectedTab;
this._draggingTab = null;
try {
Promise.all([
gZenUIManager.motion.animate(
gBrowser.tabbox,
side === 'left'
? {
paddingLeft: [`${halfWidth}px`, 0],
}
: {
paddingRight: [`${halfWidth}px`, 0],
},
{
duration: 0.1,
easing: 'ease-out',
}
),
gZenUIManager.motion.animate(
this.fakeBrowser,
{
width: [`${halfWidth - padding * 2}px`, 0],
...(side === 'left'
? {
marginLeft: [`${-halfWidth}px`, 0],
}
: {}),
},
{
duration: 0.1,
easing: 'ease-out',
}
),
]).then(() => {
this._canDrop = false;
this._maybeRemoveFakeBrowser();
});
} catch (e) {
this._canDrop = false;
this._maybeRemoveFakeBrowser();
}
}
/**
* Remove a SplitNode from its tree and the view
* @param {SplitNode} toRemove
@@ -204,14 +411,13 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
afterRearangeAction() {
document.getElementById('zenSplitViewModifier').hidePopup();
ConfirmationHint.show(document.getElementById('zen-split-views-box'), 'zen-split-view-modifier-enabled-toast', {
gZenUIManager.showToast('zen-split-view-modifier-enabled-toast', {
descriptionId: 'zen-split-view-modifier-enabled-toast-description',
showDescription: true,
});
}
afterRearangeRemove() {
ConfirmationHint.show(document.getElementById('zen-split-views-box'), 'zen-split-view-modifier-disabled-toast');
gZenUIManager.showToast('zen-split-view-modifier-disabled-toast');
}
toggleWrapperDisplay(value) {
@@ -289,7 +495,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
let isSplitHeaderDrag = false;
const container = event.target.closest('.browserSidebarContainer[zen-split]');
if (container && event.offsetY < 20) {
if (container && event.target.closest('.zen-tab-rearrange-button')) {
// Split tab header drag case
const containerRect = container.getBoundingClientRect();
const clickX = event.clientX - containerRect.left;
@@ -549,6 +755,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
*/
resetTabState(tab, forUnsplit) {
tab.splitView = false;
delete tab.splitViewValue;
tab.removeAttribute('split-view');
tab.linkedBrowser.zenModeActive = false;
const container = tab.linkedBrowser.closest('.browserSidebarContainer');
@@ -679,7 +886,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
return false;
}
for (const tab of window.gBrowser.selectedTabs) {
if (tab.splitView) {
if (tab.splitView || tab.hasAttribute('zen-empty-tab') || tab.hasAttribute('zen-essential')) {
return false;
}
}
@@ -699,6 +906,27 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
this.updateSplitView(tab);
tab.linkedBrowser.docShellIsActive = true;
}
this._maybeRemoveFakeBrowser();
}
/**
* @param {Tab} tab
*/
_moveTabsToContainer(tabs, relativeTab) {
const relativeTabIsPinned = relativeTab.pinned;
const relativeTabIsEssential = relativeTab.hasAttribute('zen-essential');
if (relativeTabIsEssential) {
gZenPinnedTabManager.addToEssentials(tabs);
} else {
for (const tab of tabs) {
if (relativeTabIsPinned) {
gBrowser.pinTab(tab);
} else {
gBrowser.unpinTab(tab);
}
}
}
}
/**
@@ -707,14 +935,13 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
* @param {Tab[]} tabs - The tabs to split.
* @param {string} gridType - The type of grid layout.
*/
splitTabs(tabs, gridType) {
const firstisPinned = tabs[0].pinned;
tabs = tabs.filter(
(t) => t.pinned === firstisPinned && !t.hidden && !t.hasAttribute('zen-empty-tab') && !t.hasAttribute('zen-essential')
);
splitTabs(tabs, gridType, initialIndex = 0) {
// TODO: Add support for splitting essential tabs
tabs = tabs.filter((t) => !t.hidden && !t.hasAttribute('zen-empty-tab') && !t.hasAttribute('zen-essential'));
if (tabs.length < 2 || tabs.length > this.MAX_TABS) {
return;
}
this._moveTabsToContainer(tabs, tabs[initialIndex]);
const existingSplitTab = tabs.find((tab) => tab.splitView);
if (existingSplitTab) {
@@ -861,9 +1088,10 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
applyGridToTabs(tabs) {
tabs.forEach((tab, index) => {
tab.splitView = true;
tab.splitViewValue = this.currentView;
tab.setAttribute('split-view', 'true');
const container = tab.linkedBrowser.closest('.browserContainer');
if (!container.querySelector('.zen-view-splitter-header')) {
const container = tab.linkedBrowser?.closest('.browserSidebarContainer');
if (!container?.querySelector('.zen-tab-rearrange-button')) {
// insert a header into the container
const header = this._createHeader(container);
container.insertBefore(header, container.firstChild);
@@ -879,6 +1107,8 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
* @private
*/
_createHeader(container) {
const headerContainer = document.createElement('div');
headerContainer.classList.add('zen-view-splitter-header-container');
const header = document.createElement('div');
header.classList.add('zen-view-splitter-header');
const removeButton = document.createXULElement('toolbarbutton');
@@ -886,12 +1116,16 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
removeButton.addEventListener('click', () => {
this.removeTabFromSplit(container);
});
const rearrangeButton = document.createXULElement('toolbarbutton');
rearrangeButton.classList.add('zen-tab-rearrange-button');
header.appendChild(rearrangeButton);
header.appendChild(removeButton);
return header;
headerContainer.appendChild(header);
return headerContainer;
}
_removeHeader(container) {
const header = container.querySelector('.zen-view-splitter-header');
const header = container.querySelector('.zen-view-splitter-header-container');
if (header) {
header.remove();
}
@@ -1029,7 +1263,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
this.tabBrowserPanel.setAttribute('zen-split-resizing', true);
const isVertical = event.target.getAttribute('orient') === 'vertical';
const dimension = isVertical ? 'width' : 'height';
const clientAxis = isVertical ? 'screenX' : 'screenY';
const clientAxis = isVertical ? 'clientX' : 'clientY';
const gridIdx = parseInt(event.target.getAttribute('gridIdx'));
const startPosition = event[clientAxis];
@@ -1245,7 +1479,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
return;
}
const tabs = gBrowser.visibleTabs;
if (tabs.length < 2) {
if (tabs.length < 2 || this.currentView >= 0) {
return;
}
let nextTabIndex = tabs.indexOf(gBrowser.selectedTab) + 1;
@@ -1275,6 +1509,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
const tab = gBrowser.getTabForBrowser(browser);
if (tab) {
const groupIndex = this._data.findIndex((group) => group.tabs.includes(tab));
this.deactivateCurrentSplitView();
if (groupIndex >= 0) {
this.removeTabFromGroup(tab, groupIndex, true);
}
@@ -1282,6 +1517,20 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
}
};
_maybeRemoveFakeBrowser(select = true) {
gBrowser.tabbox.removeAttribute('style');
if (this.fakeBrowser) {
delete this._hasAnimated;
this.fakeBrowser.remove();
this.fakeBrowser = null;
if (this._draggingTab) this._draggingTab._visuallySelected = false;
if (select) {
gBrowser.selectedTab = this._draggingTab;
this._draggingTab = null;
}
}
}
/**
* @description moves the tab to the split view if dragged on a browser
* @param event - The event
@@ -1289,18 +1538,41 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
* @returns {boolean} true if the tab was moved to the split view
*/
moveTabToSplitView(event, draggedTab) {
const dropTarget = document.elementFromPoint(event.clientX, event.clientY);
const canDrop = this._canDrop;
this._canDrop = false;
if (!canDrop || !this.fakeBrowser) {
this._maybeRemoveFakeBrowser(false);
return false;
}
const dropSide = this.fakeBrowser?.getAttribute('side');
const containerRect = this.fakeBrowser.getBoundingClientRect();
const padding = Services.prefs.getIntPref('zen.theme.content-element-separation', 0);
const dropTarget = document.elementFromPoint(
dropSide === 'left' ? containerRect.left + containerRect.width + padding + 5 : containerRect.left - padding - 5,
event.clientY
);
const browser = dropTarget?.closest('browser');
if (!browser) {
this._maybeRemoveFakeBrowser(false);
return false;
}
gBrowser.selectedTab = this._draggingTab;
this._draggingTab = null;
const browserContainer = draggedTab.linkedBrowser?.closest('.browserSidebarContainer');
if (browserContainer) {
browserContainer.style.opacity = '0';
}
const droppedOnTab = gBrowser.getTabForBrowser(browser);
if (droppedOnTab && droppedOnTab !== draggedTab) {
// Calculate which side of the target browser the drop occurred
const browserRect = browser.getBoundingClientRect();
const hoverSide = this.calculateHoverSide(event.clientX, event.clientY, browserRect);
// const browserRect = browser.getBoundingClientRect();
// const hoverSide = this.calculateHoverSide(event.clientX, event.clientY, browserRect);
const hoverSide = dropSide;
if (droppedOnTab.splitView) {
// Add to existing split view
@@ -1311,6 +1583,7 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
// First move the tab to the split view group
let splitGroup = droppedOnTab.group;
if (splitGroup && (!draggedTab.group || draggedTab.group !== splitGroup)) {
this._moveTabsToContainer([draggedTab], droppedOnTab);
gBrowser.moveTabToGroup(draggedTab, splitGroup);
}
@@ -1336,29 +1609,80 @@ class ZenViewSplitter extends ZenDOMOperatedFeature {
}
} else {
// Create new split view with layout based on drop position
let gridType;
switch (hoverSide) {
case 'left':
case 'right':
gridType = 'vsep';
break;
case 'top':
case 'bottom':
gridType = 'hsep';
break;
default:
gridType = 'grid';
}
let gridType = 'vsep';
//switch (hoverSide) {
// case 'left':
// case 'right':
// gridType = 'vsep';
// break;
// case 'top':
// case 'bottom':
// gridType = 'hsep';
// break;
// default:
// gridType = 'grid';
//}
// Put tabs in correct order based on drop side
const tabs = ['left', 'top'].includes(hoverSide) ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab];
this.splitTabs(tabs, gridType);
// Put tabs always as if it was dropped from the left
this.splitTabs(dropSide == 'left' ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab], gridType, 1);
}
}
if (this._finishAllAnimatingPromise) {
this._finishAllAnimatingPromise.then(() => {
this._maybeRemoveFakeBrowser(false);
});
}
if (browserContainer) {
gZenUIManager.motion
.animate(
browserContainer,
{
scale: [0.97, 1],
opacity: [0, 1],
},
{
type: 'spring',
bounce: 0.4,
duration: 0.2,
delay: 0.1,
}
)
.then(() => {
this._maybeRemoveFakeBrowser(false);
this._finishAllAnimatingPromise = null;
});
}
return true;
}
handleTabDrop(event, urls, replace, inBackground) {
if (replace || urls.length !== 1) {
return false;
}
const url = urls[0];
if (!url.startsWith('panel-')) {
return false;
}
const browserContainer = document.getElementById(url);
const browser = browserContainer?.querySelector('browser');
if (!browser) {
return false;
}
const tab = gBrowser.getTabForBrowser(browser);
if (!tab) {
return false;
}
if (tab.splitView) {
// Unsplit the tab and exit from the drag view
this.dropZone?.removeAttribute('enabled');
this.disableTabRearrangeView(event);
this.removeTabFromSplit(browserContainer);
return true;
}
return false;
}
/**
* Gets or creates a tab group for split view tabs
* @param {Array} tabs Initial tabs to add to the group if creating new

View File

@@ -226,6 +226,7 @@
const sidebar = document.getElementById('zen-welcome-page-sidebar');
sidebar.style.width = '100%';
sidebar.appendChild(heart);
sidebar.setAttribute('animate-heart', 'true');
await animate(
'#zen-welcome-heart',
{ opacity: [0, 1, 1, 1, 0], scale: [0.5, 1, 1.2, 1, 1.2] },
@@ -334,17 +335,17 @@
<html:div></html:div>
</hbox>
<html:div id="zen-welcome-initial-essentials-browser-sidebar-essentials">
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://web.whatsapp.com" style="--zen-tab-icon: url('https://web.whatsapp.com/favicon.ico');">
<html:div class="tabbrowser-tab" fadein="" data-url="https://web.whatsapp.com" style="--zen-tab-icon: url('https://web.whatsapp.com/favicon.ico');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
</html:div>
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://discord.com" style="--zen-tab-icon: url('http://www.google.com/s2/favicons?domain=discord.com');">
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://discord.com" style="--zen-tab-icon: url('https://www.google.com/s2/favicons?domain=discord.com');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
</html:div>
<html:div class="tabbrowser-tab" fadein="" data-url="https://reddit.com" style="--zen-tab-icon: url('https://www.redditstatic.com/desktop2x/img/favicon/favicon-96x96.png');">
<html:div class="tabbrowser-tab" fadein="" data-url="https://trello.com" style="--zen-tab-icon: url('https://trello.com/favicon.ico');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
@@ -354,7 +355,7 @@
<html:div class="tab-background"></html:div>
</stack>
</html:div>
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://google.com" style="--zen-tab-icon: url('https://www.google.com/s2/favicons?domain=google.com');">
<html:div class="tabbrowser-tab" fadein="" data-url="https://github.com" style="--zen-tab-icon: url('https://github.githubassets.com/favicons/favicon-dark.png');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
@@ -364,17 +365,17 @@
<html:div class="tab-background"></html:div>
</stack>
</html:div>
<html:div class="tabbrowser-tab" fadein="" data-url="https://notion.com" style="--zen-tab-icon: url('https://www.notion.so/front-static/favicon.ico');">
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://notion.com" style="--zen-tab-icon: url('https://www.notion.so/front-static/favicon.ico');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
</html:div>
<html:div class="tabbrowser-tab" fadein="" data-url="https://instagram.com" style="--zen-tab-icon: url('https://www.instagram.com/static/images/ico/favicon-192.png/68d99ba29cc8.png');">
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://calendar.google.com" style="--zen-tab-icon: url('https://calendar.google.com/googlecalendar/images/favicons_2020q4/calendar_6.ico');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
</html:div>
<html:div class="tabbrowser-tab" fadein="" visuallyselected="" data-url="https://element.io" style="--zen-tab-icon: url('http://www.google.com/s2/favicons?domain=element.io');">
<html:div class="tabbrowser-tab" fadein="" data-url="https://youtube.com" style="--zen-tab-icon: url('https://www.youtube.com/favicon.ico');">
<stack class="tab-stack">
<html:div class="tab-background"></html:div>
</stack>
@@ -453,9 +454,7 @@
async fadeOut() {
gZenThemePicker.panel.removeAttribute('noautohide');
gZenThemePicker.panel.removeAttribute('consumeoutsideclicks');
if (AppConstants.platform != 'macosx') {
await animate(gZenThemePicker.panel, { opacity: [1, 0] });
}
await animate(gZenThemePicker.panel, { opacity: [1, 0] });
gZenThemePicker.panel.hidePopup();
gZenThemePicker.panel.removeAttribute('style');
},

View File

@@ -36,6 +36,10 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
this._resolveInitialized = resolve;
});
promiseEmptyTabInitialized = new Promise((resolve) => {
this._resolveEmptyTabInitialized = resolve;
});
workspaceIndicatorXUL = `
<hbox class="zen-current-workspace-indicator-icon"></hbox>
<hbox class="zen-current-workspace-indicator-name"></hbox>
@@ -111,12 +115,13 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
);
}
selectEmptyTab() {
selectEmptyTab(newTabTarget = null) {
if (this._emptyTab && gZenVerticalTabsManager._canReplaceNewTab) {
gBrowser.selectedTab = this._emptyTab;
return this._emptyTab;
}
let tab = gZenUIManager.openAndChangeToTab(Services.prefs.getStringPref('browser.startup.homepage'));
const newTabUrl = newTabTarget || Services.prefs.getStringPref('browser.startup.homepage');
let tab = gZenUIManager.openAndChangeToTab(newTabUrl);
if (window.uuid) {
tab.setAttribute('zen-workspace-id', this.activeWorkspace);
}
@@ -144,6 +149,9 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
_initializeEmptyTab() {
if (Services.prefs.getBoolPref('zen.workspaces.disable_empty_state_for_testing', false)) {
return;
}
this._emptyTab = gBrowser.addTrustedTab('about:blank', { inBackground: true, userContextId: 0, _forZenEmptyTab: true });
}
@@ -602,15 +610,22 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
} catch (e) {
console.error('ZenWorkspaces: Error initializing theme picker', e);
}
this._selectStartPage();
await this._selectStartPage();
this._fixTabPositions();
this._resolveInitialized();
this._clearAnyZombieTabs(); // Dont call with await
}
}
_selectStartPage() {
async _selectStartPage() {
if (Services.prefs.getBoolPref('zen.workspaces.disable_empty_state_for_testing', false)) {
return;
}
const currentTab = gBrowser.selectedTab;
let showed = false;
await this.promiseEmptyTabInitialized;
this._resolveEmptyTabInitialized = null;
this.promiseEmptyTabInitialized = null;
if (currentTab.pinned) {
this.selectEmptyTab();
try {
@@ -625,7 +640,8 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
if (
(currentTab.isEmpty &&
(currentTab.getAttribute('image') === gPageIcons[currentTabURL] || !currentTab.hasAttribute('image'))) ||
currentTab.hasAttribute('zen-empty-tab')
currentTab.hasAttribute('zen-empty-tab') ||
(currentTab._possibleEmptyTab && currentTab.isEmpty)
) {
this.selectEmptyTab();
this._removedByStartupPage = true;
@@ -652,6 +668,26 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
return !window.toolbar.visible || Services.prefs.getBoolPref('browser.tabs.closeWindowWithLastTab');
}
async _clearAnyZombieTabs() {
const tabs = this.allStoredTabs;
const workspaces = await this._workspaces();
for (let tab of tabs) {
const workspaceID = tab.getAttribute('zen-workspace-id');
if (
workspaceID &&
!tab.hasAttribute('zen-essential') &&
!workspaces.workspaces.find((workspace) => workspace.uuid === workspaceID)
) {
// Remove any tabs where their workspace doesn't exist anymore
gBrowser.removeTab(tab, {
animate: false,
skipSessionStore: true,
closeWindowWithLastTab: false,
});
}
}
}
handleTabBeforeClose(tab) {
if (!this.workspaceEnabled || this.__contextIsDelete || this._removedByStartupPage) {
return null;
@@ -1382,15 +1418,16 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
_deleteAllTabsInWorkspace(workspaceID) {
for (let tab of gBrowser.tabs) {
if (tab.getAttribute('zen-workspace-id') === workspaceID) {
gBrowser.removeTab(tab, {
animate: true,
skipSessionStore: true,
closeWindowWithLastTab: false,
});
gBrowser.removeTabs(
Array.from(gBrowser.tabs).filter(
(tab) => tab.getAttribute('zen-workspace-id') === workspaceID && !tab.hasAttribute('zen-empty-tab')
),
{
animate: false,
skipSessionStore: true,
closeWindowWithLastTab: false,
}
}
);
}
moveTabToWorkspace(tab, workspaceID) {
@@ -1508,18 +1545,19 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
const previousWorkspace = await this.getActiveWorkspace();
alwaysChange = alwaysChange || onInit;
this.activeWorkspace = window.uuid;
if (previousWorkspace && previousWorkspace.uuid === window.uuid && !alwaysChange) {
this._cancelSwipeAnimation();
return;
}
this.activeWorkspace = window.uuid;
const containerId = window.containerTabId?.toString();
const workspaces = await this._workspaces();
// Refresh tab cache
gBrowser.verticalPinnedTabsContainer = this.pinnedTabsContainer;
gBrowser.tabContainer.verticalPinnedTabsContainer = this.pinnedTabsContainer;
gBrowser.verticalPinnedTabsContainer = this.pinnedTabsContainer || gBrowser.verticalPinnedTabsContainer;
gBrowser.tabContainer.verticalPinnedTabsContainer =
this.pinnedTabsContainer || gBrowser.tabContainer.verticalPinnedTabsContainer;
// Move empty tab to the new workspace
this._moveEmptyTabToWorkspace(window.uuid);
@@ -1967,6 +2005,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
async onLocationChange(browser) {
gZenCompactModeManager.sidebar.toggleAttribute('zen-has-empty-tab', gBrowser.selectedTab.hasAttribute('zen-empty-tab'));
if (!this.workspaceEnabled || this._inChangingWorkspace || this._isClosingWindow) {
return;
}
@@ -1993,6 +2032,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
// Switch workspace if needed
if (workspaceID && workspaceID !== activeWorkspace.uuid && parent.ZenWorkspaces._hasInitializedTabsStrip) {
const workspaces = await parent.ZenWorkspaces._workspaces();
await parent.ZenWorkspaces.changeWorkspace({ uuid: workspaceID });
}
}
@@ -2231,7 +2271,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
const activeWorkspace = this.getActiveWorkspaceFromCache();
const activeWorkspaceUserContextId = activeWorkspace?.containerTabId;
if (typeof userContextId !== 'undefined' && userContextId !== activeWorkspaceUserContextId) {
if (fromExternal !== true && typeof userContextId !== 'undefined' && userContextId !== activeWorkspaceUserContextId) {
return [userContextId, false, undefined];
}
return [activeWorkspaceUserContextId, true, undefined];

View File

@@ -7,6 +7,7 @@ var ZenWorkspacesStorage = {
Weave: 'resource://services-sync/main.sys.mjs',
});
if (!window.ZenWorkspaces) return;
await this._ensureTable();
await ZenWorkspaceBookmarksStorage.init();
},

View File

@@ -12,6 +12,9 @@ export class ZenGlanceChild extends JSWindowActorChild {
case 'DOMContentLoaded':
await this.initiateGlance();
break;
case 'keydown':
this.onKeyDown(event);
break;
default:
}
}
@@ -120,4 +123,13 @@ export class ZenGlanceChild extends JSWindowActorChild {
this.openGlance(target);
}
}
onKeyDown(event) {
if (event.defaultPrevented || event.key !== 'Escape') {
return;
}
this.sendAsyncMessage('ZenGlance:CloseGlance', {
hasFocused: this.contentWindow.document.activeElement !== this.contentWindow.document.body,
});
}
}

View File

@@ -15,6 +15,16 @@ export class ZenGlanceParent extends JSWindowActorParent {
this.openGlance(this.browsingContext.topChromeWindow, message.data);
break;
}
case 'ZenGlance:CloseGlance': {
const params = {
onTabClose: true,
...message.data,
};
this.browsingContext.topChromeWindow.gZenGlanceManager.closeGlance(params);
break;
}
default:
console.warn(`[glance]: Unknown message: ${message.name}`);
}
}

View File

@@ -0,0 +1,4 @@
BROWSER_CHROME_MANIFESTS += [
"tests/browser.toml",
]