gh-15438: Performance optimizations and media tab for Library (gh-15483)

Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: fen4flo <75260616+FlorianButz@users.noreply.github.com>
This commit is contained in:
fen4flo
2026-09-20 22:08:51 +02:00
committed by GitHub
parent 7debd15013
commit 303b6e39ed
23 changed files with 1809 additions and 196 deletions

View File

@@ -35,6 +35,28 @@ library-history-search-placeholder =
.placeholder = Search History…
library-history-empty = No history found
library-media-empty = No media found
library-media-search-placeholder =
.placeholder = Search Media…
library-media-loading = Looking for media…
library-media-opt-in-title = Your Media in the Library
library-media-opt-in-body = Quickly grab screenshots, images and videos from your computer by showing them in the Zen Library.
library-media-opt-in-button = Show Media
library-media-opt-in-note = Media never leaves your computer and is not viewable by anyone but you.
library-media-filter-title = Filter Media…
library-media-filter-folders = Where to look
library-media-open-in = Open in { $app }
library-media-close =
.title = Close
library-media-previous =
.title = Previous
library-media-next =
.title = Next
library-media-more =
.title = More options
library-media-menu-open =
.label = Open
library-media-menu-copy =
.label = Copy
library-history-forget-button =
.title = Remove from history
library-history-reopen-button =

View File

@@ -7,3 +7,9 @@
- name: zen.library.last-tab
value: "history"
- name: zen.library.media.enabled
value: false
- name: zen.library.media.folders
value: "downloads,documents,pictures"

View File

@@ -652,7 +652,10 @@ class nsZenBoostsManager {
);
const progressListener = {
QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener"]),
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
onLocationChange: webProgress => {
if (webProgress.isTopLevel) {
editor.close();

View File

@@ -100,14 +100,14 @@ body,
display: flex;
opacity: var(--zen-grainy-background-opacity, 0);
:root:not([swipe-gesture="true"]) & {
&:not([swipe-gesture="true"]) {
/* note: Keep in sync with kGlobalAnimationDuration */
transition: opacity 0.2s ease-in-out;
}
}
}
:root[swipe-gesture] #tabbrowser-arrowscrollbox {
#tabbrowser-arrowscrollbox[swipe-gesture] {
/* See gh-13703 for rationale. */
pointer-events: none;
}

View File

@@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.zen-library-media-preview {
position: absolute;
inset: 0;
--zen-main-app-child-z-index: 3;
display: flex;
align-items: center;
justify-content: center;
}
.zen-library-media-preview-backdrop {
position: absolute;
inset: 0;
-moz-window-dragging: drag;
background: light-dark(rgba(255, 255, 255, 0.55), rgba(0, 0, 0, 0.45));
}
.zen-library-media-preview-panel {
position: relative;
-moz-window-dragging: no-drag;
display: flex;
flex-direction: column;
width: min(78vw, 1100px);
border-radius: 18px;
overflow: hidden;
background: light-dark(rgb(245, 245, 247), rgb(28, 30, 32));
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
}
.zen-library-media-preview-header {
--zen-toolbar-button-size: 15px;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
flex: none;
color: light-dark(rgb(28, 30, 32), rgb(233, 234, 237));
}
.zen-library-media-preview-name {
margin-inline-start: 6px;
font-size: 13px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.zen-library-media-preview-date {
font-size: 13px;
opacity: 0.6;
margin-inline-end: auto;
white-space: nowrap;
}
.zen-library-media-preview .toolbarbutton-1 > .toolbarbutton-icon {
box-sizing: border-box;
-moz-context-properties: fill;
fill: var(--toolbarbutton-icon-fill);
}
.zen-library-media-preview-open {
& > .toolbarbutton-text {
display: block;
margin: 0;
padding: 5px 10px;
border-radius: var(--toolbarbutton-border-radius);
background: color-mix(in srgb, currentColor 12%, transparent);
font-weight: 600;
}
&:hover > .toolbarbutton-text {
background: color-mix(in srgb, currentColor 20%, transparent);
}
}
.zen-library-media-preview-stage {
display: flex;
align-items: center;
justify-content: center;
height: min(64vh, 620px);
padding: 16px;
background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.28));
border-top: 1px solid light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.07));
}
.zen-library-media-preview-media {
max-width: 100%;
max-height: 100%;
border-radius: 8px;
object-fit: contain;
}

View File

@@ -27,6 +27,7 @@
width: var(--zen-library-sprite-size);
height: var(--zen-library-sprite-size);
background-image: url("chrome://browser/skin/zen-icons/library/library-sprite.svg");
scale: 1.08;
}
@media (prefers-reduced-motion: no-preference) {
@@ -87,7 +88,7 @@
}
&[downloading] {
transform: scale(calc((var(--zen-library-progress) * 0.2 + 0.81) * 100%));
transform: scale(calc((var(--zen-library-progress) * 0.2 + 0.8) * 100%));
}
}
}
@@ -123,7 +124,7 @@
background-image: var(--download-image);
width: 20px;
height: 20px;
border-radius: 8px;
border-radius: var(--border-radius-small);
background-size: cover;
scale: 2;
@@ -140,7 +141,7 @@
--border-color: color-mix(in srgb, var(--zen-primary-color), light-dark(rgba(0,0,0,.8), white) 70%);
background-color: color-mix(in srgb, var(--zen-primary-color), light-dark(white, rgba(0,0,0,.5)) 80%);
border: 3px solid var(--border-color);
border: 3px solid transparent;
box-shadow: 0 0 6px 2px rgba(0,0,0,.1);
color: var(--border-color);
width: 25px;
@@ -175,11 +176,14 @@
--thickness: 3px;
--progress: var(--zen-accent-button-background);
background: conic-gradient(
from -0deg,
var(--progress) calc(var(--value) * 1%),
transparent 0
);
background:
conic-gradient(
from 0deg,
var(--progress) calc(var(--value) * 1%),
transparent 0
)
border-box,
var(--border-color) border-box;
mask: radial-gradient(
circle closest-side,
@@ -188,6 +192,10 @@
);
}
#zen-sidebar-foot-buttons:is([zen-library-stack-open], [zen-library-stack-closing]) {
z-index: 3;
}
#zen-library-download-list {
width: 100%;
height: auto;

View File

@@ -812,3 +812,4 @@
}
%include zen-library-widget.css
%include zen-library-media-preview.css

View File

@@ -498,6 +498,16 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
await this.animateCollapse(group);
}
/**
* Settles a collapsed folder's own height. The library toggles folders
* through copies, so the real one needs telling once the dust settles.
*
* @param {Element} group - A folder
*/
relayoutCollapsedFolder(group) {
this.#queueCollapsedRelayout(group);
}
#queueCollapsedRelayout(group) {
if (this.#collapsedRelayoutQueue.size === 0) {
requestAnimationFrame(() => this.#flushCollapsedRelayout());

View File

@@ -69,22 +69,19 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
return this.#detached?.browser ?? null;
}
get #detachedHost() {
let host = document.getElementById("zen-glance-detached");
if (!host) {
host = window.MozXULElement.parseXULToFragment(`
<html:div id="zen-glance-detached" hidden="true">
<html:div class="zen-glance-detached-backdrop"/>
<html:div class="browserContainer">
<html:div class="browserStack"/>
</html:div>
#createDetachedHost() {
const host = window.MozXULElement.parseXULToFragment(`
<html:div id="zen-glance-detached">
<html:div class="zen-glance-detached-backdrop"/>
<html:div class="browserContainer">
<html:div class="browserStack"/>
</html:div>
`).firstElementChild;
host
.querySelector(".zen-glance-detached-backdrop")
.addEventListener("click", () => this.closeDetachedGlance());
document.getElementById("zen-main-app-wrapper").appendChild(host);
}
</html:div>
`).firstElementChild;
host
.querySelector(".zen-glance-detached-backdrop")
.addEventListener("click", () => this.closeDetachedGlance());
document.getElementById("zen-main-app-wrapper").appendChild(host);
return host;
}
@@ -101,7 +98,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
if (this.#detached || !this.#isGlanceLoadAllowed(data)) {
return null;
}
const host = this.#detachedHost;
const host = this.#createDetachedHost();
const wrapper = host.querySelector(".browserContainer");
const browser = document.createXULElement("browser");
browser.setAttribute("type", "content");
@@ -122,7 +119,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
wrapper.querySelector(selector)?.remove();
}
host.hidden = false;
const hostRect = host.getBoundingClientRect();
const startPoint = {
clientX: data.clientX - hostRect.left,
@@ -202,10 +198,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
);
} finally {
browser.destroy();
browser.remove();
wrapper.removeAttribute("animate");
host.removeAttribute("fade-out");
host.hidden = true;
host.remove();
this.#detached = null;
}
}

View File

@@ -202,14 +202,10 @@
#zen-main-app-wrapper > #zen-glance-detached {
position: fixed;
inset: 24px;
inset: calc(var(--zen-element-separation) / 2);
z-index: 100;
display: flex;
&[hidden] {
display: none;
}
& .zen-glance-detached-backdrop {
position: fixed;
inset: 0;

View File

@@ -2,7 +2,11 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import {
html,
nothing,
repeat,
} from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const { ZenLibraryWidget } = ChromeUtils.importESModule(
@@ -36,6 +40,8 @@ ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
export class ZenLibrary extends MozLitElement {
static instance = null;
#contentMounted = true;
#mounted = new Set();
#progress = 0;
#springControls = null;
@@ -51,8 +57,8 @@ export class ZenLibrary extends MozLitElement {
#canSwipe = false;
#isOpen = false;
#isWrapperSwipeAttached = false;
#wrapperGestureControl = null;
#gestureControl = null;
#resizeObserver = new ResizeObserver(() => {
this.openProgress = this.#progress;
@@ -83,6 +89,7 @@ export class ZenLibrary extends MozLitElement {
};
const lastTab = Services.prefs.getStringPref(LAST_TAB_PREF, "history");
this.activeTab = lastTab in this.zenLibrarySections ? lastTab : "history";
this.#mounted.add(this.activeTab);
this.#hijackFirefoxCommands();
}
@@ -101,11 +108,17 @@ export class ZenLibrary extends MozLitElement {
return lib.openProgress > 0.001;
}
set isHidden(value) {
this.requestUpdate();
this.hidden = value;
}
set activeTab(value) {
if (this._activeTab === value) {
return;
}
this._activeTab = value;
this.#mounted.add(value);
Services.prefs.setStringPref(LAST_TAB_PREF, value);
}
@@ -125,31 +138,64 @@ export class ZenLibrary extends MozLitElement {
const isPastWindowButtonSwitchPoint = p > stealWindowButtonsPastPoint;
const isOpen = p > 0.001;
let libraryWidth = window.windowUtils.getBoundsWithoutFlushing(this).width;
const compactModeOffsetDirection = this.#libraryOnRight
? -this.#toolboxWidth
: this.#toolboxWidth;
const compactModeOffset = this.#isCompactMode
? compactModeOffsetDirection
: 0;
let webOffset =
(this.#libraryOnRight ? -1 : 1) * (libraryWidth - this.#toolboxWidth) +
compactModeOffset;
if (this.#stylesLoaded) {
let libraryWidth =
window.windowUtils.getBoundsWithoutFlushing(this).width;
const compactModeOffsetDirection = this.#libraryOnRight
? -this.#toolboxWidth
: this.#toolboxWidth;
const compactModeOffset = this.#isCompactMode
? compactModeOffsetDirection
: 0;
lazy.appContentWrapper?.style.setProperty(
"--library-wrapper-target-px",
`${webOffset}px`
);
[this, lazy.appContentWrapper, gNavToolbox].forEach(elem => {
elem?.style.setProperty("--library-progress", String(p));
});
let webOffset =
(this.#libraryOnRight ? -1 : 1) * (libraryWidth - this.#toolboxWidth) +
compactModeOffset;
this.style.setProperty(
"transform",
`translateX(calc(-100% * (1 - ${value})))`
);
lazy.appContentWrapper?.style.setProperty(
"transform",
`translateX(${value * webOffset}px)`
);
const toolboxProgress = Math.min(1, value * 1.5);
if (this.#isCompactMode) {
if (this.#libraryOnRight) {
gNavToolbox.style.setProperty(
"transform",
`translateX(calc(100% * ${toolboxProgress}))`
);
} else {
gNavToolbox.style.setProperty(
"transform",
`translateX(calc(-100% * ${toolboxProgress}))`
);
}
} else {
const toolboxScale = 1 - toolboxProgress * 0.04;
const toolboxOpacity = 1 - toolboxProgress;
gNavToolbox?.style.setProperty("transform", `scale(${toolboxScale})`);
gNavToolbox?.style.setProperty("opacity", `${toolboxOpacity}`);
}
}
if (isOpen && !wasOpen) {
this.setAttribute("open", "true");
document.documentElement.setAttribute("zen-library-open", "true");
document
.getElementById("zen-sidebar-splitter")
.setAttribute("zen-library-open", "true");
this.#init();
} else if (!isOpen && wasOpen) {
this.removeAttribute("open");
document.documentElement.removeAttribute("zen-library-open");
this.#mounted = new Set([this.activeTab]);
this.requestUpdate();
document
.getElementById("zen-sidebar-splitter")
.removeAttribute("zen-library-open");
this.#cleanup();
}
if (isPastWindowButtonSwitchPoint && this.#coversWindowButtons) {
@@ -253,8 +299,10 @@ export class ZenLibrary extends MozLitElement {
this.#idleCleanup = window.requestIdleCallback(() => {
this.#idleCleanup = null;
this.#stylesLoaded = null;
ZenLibrary.instance = null;
this.remove();
this.#contentMounted = false;
this.#mounted = new Set([this.activeTab]);
this.requestUpdate();
});
}
@@ -294,7 +342,6 @@ export class ZenLibrary extends MozLitElement {
static async animateProgress(target) {
const lib = this.getInstance();
lib.#detachWrapperOfSwipe();
lib.#cancelIdleCleanup();
await lib.#whenStylesLoaded();
lib.style.visibility = "";
@@ -310,6 +357,7 @@ export class ZenLibrary extends MozLitElement {
lib.#isOpen = true;
} else if (target === 0) {
lib.#isOpen = false;
lib.#canSwipe = false;
}
lib.setAttribute("transitioning", "true");
@@ -329,6 +377,8 @@ export class ZenLibrary extends MozLitElement {
lib.#springControls = null;
lib.removeAttribute("transitioning");
if (target === 0) {
lib.#mounted = new Set([lib.activeTab]);
lib.requestUpdate();
lib.#scheduleIdleCleanup();
}
},
@@ -343,9 +393,6 @@ export class ZenLibrary extends MozLitElement {
await lib.#whenStylesLoaded();
lib.style.visibility = "";
await window.promiseDocumentFlushed(() => {});
if (!lib.#canSwipe) {
return;
}
lib.#onOpenLibrary();
@@ -355,7 +402,6 @@ export class ZenLibrary extends MozLitElement {
}
lib.style.setProperty("pointer-events", "none");
lib.#attachWrapperToSwipe();
}
static stopSwipe(direction) {
@@ -371,7 +417,6 @@ export class ZenLibrary extends MozLitElement {
const target = Math.max(-direction, 0);
this.animateProgress(target);
}
lib.#detachWrapperOfSwipe();
// Return library open state
return lib.#isOpen;
@@ -387,29 +432,33 @@ export class ZenLibrary extends MozLitElement {
}
#attachWrapperToSwipe() {
if (!this.#isWrapperSwipeAttached) {
if (!this.#wrapperGestureControl) {
const appWrapper = document.getElementById("zen-main-app-wrapper");
this.#wrapperGestureControl =
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(
appWrapper
);
this.#isWrapperSwipeAttached = true;
}
}
#detachWrapperOfSwipe() {
if (this.#isWrapperSwipeAttached || this.#wrapperGestureControl) {
if (this.#wrapperGestureControl) {
const appWrapper = document.getElementById("zen-main-app-wrapper");
window.gZenWorkspaces._swipeManager?.detachWorkspaceSwipeGestures(
appWrapper,
this.#wrapperGestureControl
);
this.#wrapperGestureControl = null;
this.#isWrapperSwipeAttached = false;
}
}
#onOpenLibrary() {
this.#cancelIdleCleanup();
if (!this.#contentMounted) {
this.#contentMounted = true;
this.requestUpdate();
}
gURLBar.view.close();
// Get the width from the css property,
// getBoundsWithoutFlushing will fail as it takes the
@@ -449,49 +498,60 @@ export class ZenLibrary extends MozLitElement {
return this;
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
this.onKeyDown = this.onKeyDown.bind(this);
document.addEventListener("keydown", this.onKeyDown, true);
#init() {
document.addEventListener("keydown", this, true);
window.addEventListener("TabOpen", this);
this.#attachWrapperToSwipe();
this.#gestureControl =
window.gZenWorkspaces._swipeManager.attachWorkspaceSwipeGestures(this);
this.#resizeObserver.observe(this);
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(this);
this._tabOpen = this.onTabOpen.bind(this);
window.addEventListener("TabOpen", this._tabOpen);
ZenLibraryWidget.attachLibrary(this);
this.isHidden = false;
}
disconnectedCallback() {
#cleanup() {
this.#cancelIdleCleanup();
if (this.#springControls) {
this.#springControls.stop();
this.#springControls = null;
}
this.#detachWrapperOfSwipe();
if (this.#gestureControl) {
window.gZenWorkspaces._swipeManager.detachWorkspaceSwipeGestures(
this,
this.#gestureControl
);
}
this.#restoreWindowButtons();
ZenLibraryWidget.detachLibrary(this);
super.disconnectedCallback();
document.removeEventListener("keydown", this.onKeyDown, true);
this.#resizeObserver.disconnect();
if (this._tabOpen) {
window.removeEventListener("TabOpen", this._tabOpen);
this._tabOpen = null;
}
document.removeEventListener("keydown", this, true);
window.removeEventListener("TabOpen", this);
this.isHidden = true;
}
get #isCompactMode() {
return (
window.gZenCompactModeManager.preference &&
Services.prefs.getBoolPref("zen.view.compact.hide-tabbar")
(Services.prefs.getBoolPref("zen.view.compact.hide-tabbar") ||
Services.prefs.getBoolPref("zen.view.use-single-toolbar"))
);
}
handleEvent(e) {
switch (e.type) {
case "TabOpen":
this.onTabOpen();
break;
case "keydown":
this.onKeyDown(e);
break;
}
}
onTabOpen() {
if (this.#isOpen) {
ZenLibrary.animateProgress(0);
@@ -543,11 +603,30 @@ export class ZenLibrary extends MozLitElement {
}
}
#animateTabIcon(tab) {
tab.removeAttribute("animate");
// Flush styles so re-adding the attribute restarts the animation.
void tab.offsetWidth;
tab.setAttribute("animate", "true");
updated(changedProperties) {
super.updated?.(changedProperties);
this.#updateMountedSections();
}
/**
* Shows the section being looked at and puts the others out of sight. A
* section is told which it is, so one that reaches outside itself, such as
* spaces setting the library's width, only does so while it is on show.
*/
#updateMountedSections() {
for (const section of this._content?.children ?? []) {
const id = section.dataset?.section;
if (!id) {
continue;
}
const showing = id === this.activeTab;
const wasShowing = section.hasAttribute("showing");
section.hidden = !showing;
section.toggleAttribute("showing", showing);
if (showing !== wasShowing) {
(showing ? section.onShown : section.onHidden)?.call(section);
}
}
}
render() {
@@ -569,7 +648,14 @@ export class ZenLibrary extends MozLitElement {
@click=${event => {
if (this.activeTab !== Section.id) {
this.activeTab = Section.id;
this.#animateTabIcon(event.currentTarget);
const previousTab =
event.currentTarget.parentNode.querySelector(
`.zen-library-tab[animate="true"]`
);
if (previousTab) {
previousTab.removeAttribute("animate");
}
event.currentTarget.setAttribute("animate", "true");
}
}}
>
@@ -589,7 +675,15 @@ export class ZenLibrary extends MozLitElement {
></toolbar>
</vbox>
<vbox id="zen-library-content">
${this.activeSection.render(this)}
${
this.#contentMounted
? repeat(
[...this.#mounted],
id => id,
id => this.zenLibrarySections[id].render(this)
)
: nothing
}
</vbox>
</hbox>
`;

View File

@@ -165,9 +165,7 @@ export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
gBrowser.moveTabsBefore(moving, firstExisting);
}
} else {
const pinned = gBrowser.isTab(element)
? element.pinned
: (element.tabs?.[0]?.pinned ?? element.pinned);
const pinned = this.#pinnedAt(event);
for (const tab of tabs) {
if (pinned && !tab.pinned) {
gBrowser.pinTab(tab);
@@ -185,6 +183,23 @@ export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
this.#land(tabs[0], placeBefore);
}
/**
* Whether a drop belongs to the pinned part of a card.
*
* @param {DragEvent} event - The drop
* @returns {boolean} Whether the tab should end up pinned
*/
#pinnedAt(event) {
const strip = event.currentTarget
.closest(".zen-library-space")
?.querySelector(".zen-library-space-tabs");
const pinnedRows = [...(strip?.querySelectorAll("tab[pinned]") ?? [])];
if (!pinnedRows.length) {
return false;
}
return event.clientY <= pinnedRows.at(-1).getBoundingClientRect().bottom;
}
get #landingSupported() {
return AppConstants.platform === "macosx" && !gReduceMotion;
}

View File

@@ -0,0 +1,110 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineLazyGetter(lazy, "mimeService", () =>
Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService)
);
/**
* Groups the browser cannot name on its own. Pictures, video and sound come
* from the content type instead, so they are not listed here.
*/
const GROUP_EXTENSIONS = {
documents:
"pdf doc docx xls xlsx ppt pptx txt md rtf odt ods odp csv epub pages numbers key",
archives: "zip rar 7z tar gz bz2 xz tgz zst",
apps: "dmg pkg exe msi app deb rpm appimage apk jar",
};
/**
* The few the browser has no type for, or types differently from how these
* lists read them. Matroska is unknown to it, and Ogg is reported as a
* container rather than as sound.
*/
const EXTRA_KINDS = new Map([
["mkv", "video"],
["ogg", "audio"],
]);
const GROUP_BY_EXTENSION = new Map();
for (const [group, extensions] of Object.entries(GROUP_EXTENSIONS)) {
for (const extension of extensions.split(" ")) {
GROUP_BY_EXTENSION.set(extension, group);
}
}
/** The groups a file can be filtered by, in the order they are offered. */
export const FILE_GROUPS = [
"images",
"video",
"audio",
...Object.keys(GROUP_EXTENSIONS),
];
/**
* A picture the platform renders as a document rather than a bitmap. It is
* left out of thumbnails, which are drawn inside the browser's own chrome.
*/
const NOT_DRAWN = "image/svg+xml";
/**
* @param {string} fileName - A file's name, with or without its path
* @returns {string} Its extension, lowercased, or "" when it has none
*/
export function extensionOf(fileName) {
const name = fileName?.split(/[/\\]/).pop() ?? "";
const dot = name.lastIndexOf(".");
return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
}
/**
* @param {string} fileName - A file's name
* @returns {string} What the browser believes the file holds, or "" when it
* has no idea
*/
export function contentTypeOf(fileName) {
const extension = extensionOf(fileName);
if (!extension) {
return "";
}
return lazy.mimeService.getTypeFromExtension(extension);
}
/**
* @param {string} fileName - A file's name
* @returns {string|null} "image", "video" or "audio", or null for anything
* that is none of those
*/
export function mediaKindOf(fileName) {
const type = contentTypeOf(fileName);
for (const kind of ["image", "video", "audio"]) {
if (type.startsWith(`${kind}/`)) {
return kind;
}
}
return EXTRA_KINDS.get(extensionOf(fileName)) ?? null;
}
/**
* @param {string} fileName - A file's name
* @returns {string|null} The group it belongs to, of {@link FILE_GROUPS}
*/
export function fileGroupOf(fileName) {
const kind = mediaKindOf(fileName);
if (kind) {
return kind === "image" ? "images" : kind;
}
return GROUP_BY_EXTENSION.get(extensionOf(fileName)) ?? null;
}
/**
* @param {string} fileName - A file's name
* @returns {boolean} Whether the file itself can stand in for its icon
*/
export function canDrawThumbnail(fileName) {
const type = contentTypeOf(fileName);
return type.startsWith("image/") && type !== NOT_DRAWN;
}

View File

@@ -0,0 +1,542 @@
/* 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/. */
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(
lazy,
"relativeDayFormat",
() =>
new Intl.RelativeTimeFormat(undefined, {
numeric: "auto",
style: "short",
})
);
const HTML_NS = "http://www.w3.org/1999/xhtml";
const MS_PER_DAY = 86400000;
const MORPH_MS = 250;
// How far in the picture gives way to the dialog around it.
// Matches the corner the grid's pictures have.
const MEDIA_RADIUS = 8;
const HANDOVER = 0.3;
// How soon the dialog's surfaces are gone on the way back.
const CHROME_OUT = 0.25;
// How long to hold the dialog back while the picture readies itself.
const IMAGE_WAIT_MS = 400;
const VIDEO_WAIT_MS = 900;
const MORPH_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
/**
* The media tab's full view. It lives beside the library rather than inside
* it, so it can sit in the middle of the window while the library slides.
*/
export class ZenLibraryMediaPreview {
#window;
#document;
#root = null;
#parts = {};
#menu = null;
#items = [];
#index = -1;
#origin = null;
#onKeyDown = event => this.#handleKey(event);
constructor(win) {
this.#window = win;
this.#document = win.document;
}
get isOpen() {
return !!this.#root?.isConnected;
}
get #item() {
return this.#items[this.#index] ?? null;
}
// Building
#build() {
const doc = this.#document;
const make = (tag, className, into) => {
const element = doc.createElementNS(HTML_NS, tag);
if (className) {
element.className = className;
}
into?.appendChild(element);
return element;
};
const root = make("div", "zen-library-media-preview");
const backdrop = make("div", "zen-library-media-preview-backdrop", root);
backdrop.addEventListener("click", () => this.close());
const panel = make("div", "zen-library-media-preview-panel", root);
const header = doc.createXULElement("toolbar");
header.className = "zen-library-media-preview-header";
panel.appendChild(header);
const button = (className, icon, l10nId, onClick) => {
const element = doc.createXULElement("toolbarbutton");
element.className = `toolbarbutton-1 ${className}`;
header.appendChild(element);
const image = doc.createElementNS(HTML_NS, "img");
image.className = "toolbarbutton-icon";
image.src = icon;
image.alt = "";
element.appendChild(image);
doc.l10n.setAttributes(element, l10nId);
element.addEventListener("command", onClick);
element.addEventListener("click", onClick);
return element;
};
button(
"close",
"chrome://browser/skin/zen-icons/close.svg",
"library-media-close",
() => this.close()
);
const prev = button(
"prev",
"chrome://browser/skin/zen-icons/arrow-left.svg",
"library-media-previous",
() => this.#step(-1)
);
const next = button(
"next",
"chrome://browser/skin/zen-icons/arrow-right.svg",
"library-media-next",
() => this.#step(1)
);
const nameLabel = make("span", "zen-library-media-preview-name", header);
const date = make("span", "zen-library-media-preview-date", header);
const more = button(
"more",
"chrome://global/skin/icons/more.svg",
"library-media-more",
event => this.#openMenu(event)
);
const openButton = doc.createXULElement("toolbarbutton");
openButton.className = "toolbarbutton-1 zen-library-media-preview-open";
header.appendChild(openButton);
const openText = doc.createXULElement("label");
openText.className = "toolbarbutton-text";
openButton.appendChild(openText);
openButton.addEventListener("command", () => this.#launch());
openButton.addEventListener("click", () => this.#launch());
const stage = make("div", "zen-library-media-preview-stage", panel);
this.#parts = {
panel,
backdrop,
header,
openText,
stage,
nameLabel,
date,
openButton,
prev,
next,
more,
};
this.#root = root;
this.#mountRoot.appendChild(root);
}
get #mountRoot() {
return (
this.#document.getElementById("zen-main-app-wrapper") ??
this.#document.documentElement
);
}
// Opening and closing
/**
* @param {object} options - Where to start
* @param {object[]} options.items - Everything the grid is showing
* @param {number} options.index - The item that was clicked
* @param {Element} options.from - The thumbnail it grows out of
*/
open({ items, index, from }) {
if (index < 0) {
return;
}
if (!this.#root) {
this.#build();
}
this.#items = items;
this.#index = index;
this.#origin = from;
this.#window.addEventListener("keydown", this.#onKeyDown, true);
this.#show();
this.#root.style.visibility = "hidden";
this.#whenMediaReady().then(() => {
if (!this.isOpen) {
return;
}
this.#root.style.visibility = "";
this.#morph(from, /* opening = */ true);
});
}
/**
* @returns {Promise} Settles once the picture has a size to animate from,
* or right away when it already has one.
*/
#whenMediaReady() {
const media = this.#parts.stage.querySelector(
".zen-library-media-preview-media"
);
if (!media) {
return Promise.resolve();
}
const isVideo = media.localName === "video";
const ready = () =>
isVideo ? media.readyState >= media.HAVE_CURRENT_DATA : media.complete;
if (ready()) {
return Promise.resolve();
}
return new Promise(resolve => {
const done = () => resolve();
media.addEventListener("load", done, { once: true });
media.addEventListener("loadeddata", done, { once: true });
media.addEventListener("error", done, { once: true });
this.#window.setTimeout(done, isVideo ? VIDEO_WAIT_MS : IMAGE_WAIT_MS);
});
}
close() {
if (!this.isOpen) {
return;
}
this.#window.removeEventListener("keydown", this.#onKeyDown, true);
const done = this.#morph(this.#origin, /* opening = */ false);
const finish = () => {
this.#root?.remove();
this.#root = null;
this.#parts = {};
this.#origin = null;
};
if (done) {
done.finished.then(finish, finish);
} else {
finish();
}
}
destroy() {
this.#window.removeEventListener("keydown", this.#onKeyDown, true);
this.#menu?.remove();
this.#menu = null;
this.#root?.remove();
this.#root = null;
}
/**
* Opens the dialog straight on top of the card's picture, matching its box
* exactly, and grows it into the middle of the window. Closing is the same
* run played backwards.
*
* @param {Element} thumbnail - The card's picture
* @param {boolean} opening - Which way round
* @returns {Animation} The dialog's animation
*/
#morph(thumbnail, opening) {
const { panel, backdrop, header, stage } = this.#parts;
const base = {
duration: MORPH_MS,
easing: MORPH_EASING,
fill: "both",
};
const steps = keyframes =>
opening
? keyframes
: [...keyframes]
.reverse()
.map(frame =>
frame.offset === undefined
? frame
: { ...frame, offset: 1 - frame.offset }
);
const chrome = { ...base, easing: "linear" };
const fadeOut = arriving =>
opening
? arriving
: [
{ ...arriving.at(-1), offset: 0 },
{ ...arriving[0], offset: CHROME_OUT },
{ ...arriving[0], offset: 1 },
];
backdrop.animate(fadeOut([{ opacity: 0 }, { opacity: 1 }]), chrome);
const source = thumbnail?.querySelector(".zen-library-media-thumb");
const from = source?.getBoundingClientRect();
const to = panel.getBoundingClientRect();
if (!from?.width || !to.width) {
return backdrop.animate(
fadeOut([{ opacity: 0 }, { opacity: 1 }]),
chrome
);
}
const media = stage?.querySelector(".zen-library-media-preview-media");
const shown = media?.getBoundingClientRect();
const target = shown?.width ? shown : to;
const scale = from.width / target.width;
const centre = box => ({
x: box.left + box.width / 2,
y: box.top + box.height / 2,
});
const panelCentre = centre(to);
const shownCentre = centre(target);
const cardCentre = centre(from);
const atCard =
`translate(${cardCentre.x - panelCentre.x - scale * (shownCentre.x - panelCentre.x)}px, ` +
`${cardCentre.y - panelCentre.y - scale * (shownCentre.y - panelCentre.y)}px) ` +
`scale(${scale})`;
const panelStyle = this.#window.getComputedStyle(panel);
const late = (values, property) => {
const hidden = property === "boxShadow" ? "none" : "transparent";
return opening
? [
{ [property]: hidden, offset: 0 },
{ [property]: hidden, offset: HANDOVER },
{ [property]: values, offset: 1 },
]
: [
{ [property]: values, offset: 0 },
{ [property]: hidden, offset: CHROME_OUT },
{ [property]: hidden, offset: 1 },
];
};
panel.animate(late(panelStyle.backgroundColor, "backgroundColor"), chrome);
panel.animate(late(panelStyle.boxShadow, "boxShadow"), chrome);
if (stage) {
const stageStyle = this.#window.getComputedStyle(stage);
stage.animate(
late(stageStyle.backgroundColor, "backgroundColor"),
chrome
);
stage.animate(late(stageStyle.borderTopColor, "borderTopColor"), chrome);
}
header.animate(
fadeOut([
{ opacity: 0, offset: 0 },
{ opacity: 0, offset: HANDOVER },
{ opacity: 1, offset: 1 },
]),
chrome
);
if (media && shown?.width) {
media.animate(
steps([
{ borderRadius: `${Math.round(MEDIA_RADIUS / scale)}px` },
{ borderRadius: `${MEDIA_RADIUS}px` },
]),
base
);
}
return panel.animate(
steps([{ transform: atCard }, { transform: "none" }]),
base
);
}
#step(delta) {
const next = this.#index + delta;
if (next < 0 || next >= this.#items.length) {
return;
}
this.#index = next;
this.#origin = this.#thumbFor(this.#item);
this.#show();
}
/**
* @param {object} item - The media being shown
* @returns {Element|null} Its card in the grid, so closing lands back on it
*/
#thumbFor(item) {
const section = this.#document.querySelector("zen-library-media-section");
const cards = [
...(section?.querySelectorAll(".zen-library-media-item") ?? []),
];
return (
cards
.find(card => card.title === item?.name)
?.querySelector(".zen-library-media-frame") ?? this.#origin
);
}
#handleKey(event) {
if (!this.isOpen) {
return;
}
switch (event.key) {
case "Escape":
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
this.close();
break;
case "ArrowLeft":
this.#step(-1);
break;
case "ArrowRight":
this.#step(1);
break;
}
}
// Contents
#show() {
const item = this.#item;
if (!item) {
return;
}
const { stage, nameLabel, date, openText, prev, next } = this.#parts;
const doc = this.#document;
stage.replaceChildren();
const media = doc.createElementNS(
HTML_NS,
item.kind === "video" ? "video" : "img"
);
media.className = "zen-library-media-preview-media";
media.src = item.url;
if (item.kind !== "video") {
media.draggable = true;
media.addEventListener("dragstart", event =>
this.#onDragStart(event, item)
);
}
if (item.kind === "video") {
media.controls = true;
media.autoplay = true;
media.preload = "auto";
} else {
media.alt = "";
}
stage.appendChild(media);
nameLabel.textContent = item.name;
date.textContent = this.#relativeDate(item.modified);
doc.l10n
.formatValue("library-media-open-in", { app: this.#openerName(item) })
.then(text => {
openText.textContent = text;
});
prev.disabled = this.#index <= 0;
next.disabled = this.#index >= this.#items.length - 1;
}
#relativeDate(modified) {
const days = Math.round((modified - Date.now()) / MS_PER_DAY);
if (Math.abs(days) >= 7) {
return lazy.relativeDayFormat.format(Math.round(days / 7), "week");
}
return lazy.relativeDayFormat.format(days, "day");
}
/**
* @param {object} item - The media being shown
* @returns {string} What the system opens this kind of file with
*/
#openerName(item) {
try {
const mime = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
const info = mime.getFromTypeAndExtension(null, item.extension);
return info.defaultDescription || info.description || "";
} catch (error) {
return "";
}
}
#onDragStart(event, item) {
const file = new lazy.FileUtils.File(item.path);
if (!file.exists()) {
event.preventDefault();
return;
}
event.dataTransfer.mozSetDataAt("application/x-moz-file", file, 0);
event.dataTransfer.effectAllowed = "copyMove";
event.dataTransfer.setData("text/uri-list", item.url);
}
#launch() {
const item = this.#item;
if (!item) {
return;
}
try {
new lazy.FileUtils.File(item.path).launch();
} catch (error) {
this.#window.openTrustedLinkIn(item.url, "tab");
}
}
#reveal() {
const item = this.#item;
if (item) {
new lazy.FileUtils.File(item.path).reveal();
}
}
#copy() {
const item = this.#item;
if (!item) {
return;
}
const transferable = Cc[
"@mozilla.org/widget/transferable;1"
].createInstance(Ci.nsITransferable);
transferable.init(this.#window.docShell.QueryInterface(Ci.nsILoadContext));
transferable.addDataFlavor("application/x-moz-file");
transferable.setTransferData(
"application/x-moz-file",
new lazy.FileUtils.File(item.path)
);
Services.clipboard.setData(
transferable,
null,
Services.clipboard.kGlobalClipboard
);
}
#openMenu(event) {
if (!this.#menu) {
this.#menu = this.#window.MozXULElement.parseXULToFragment(`
<menupopup class="zen-library-media-menu">
<menuitem data-action="open" data-l10n-id="library-media-menu-open"/>
<menuitem data-action="show" data-l10n-id="downloads-cmd-show-menuitem-2"/>
<menuseparator/>
<menuitem data-action="copy" data-l10n-id="library-media-menu-copy"/>
</menupopup>
`).firstElementChild;
this.#menu.addEventListener("command", menuEvent => {
switch (menuEvent.target.dataset.action) {
case "open":
this.#launch();
break;
case "show":
this.#reveal();
break;
case "copy":
this.#copy();
break;
}
});
this.#document.getElementById("mainPopupSet").appendChild(this.#menu);
}
this.#menu.openPopup(event.currentTarget, "after_end", 0, 0, false, false);
}
}

View File

@@ -4,6 +4,7 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
DownloadsCommon:
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
@@ -290,9 +291,10 @@ class ZenLibraryDownloadStack {
badge.parentElement.toggleAttribute("downloading", pending);
}
if (download) {
const preview = this.#previewUrl(download);
badge.style.setProperty(
"--download-image",
`url('${this.#iconUrl(download)}')`
`${preview ? `url('${preview}'), ` : ""}url('${this.#iconUrl(download)}')`
);
}
badge
@@ -398,6 +400,18 @@ class ZenLibraryDownloadStack {
: download.source.url;
}
/**
* @param {object} download - The download an entry stands for
* @returns {string|null} The finished file itself, when it is a picture
*/
#previewUrl(download) {
const path = download.succeeded && download.target.path;
if (!path || !lazy.canDrawThumbnail(path)) {
return null;
}
return PathUtils.toFileURI(path);
}
#isPending(download) {
return (
!!download &&

View File

@@ -12,5 +12,7 @@ MOZ_SRC_FILES += [
"ZenLibrary.mjs",
"ZenLibraryDragAndDrop.mjs",
"ZenLibraryFileTypes.sys.mjs",
"ZenLibraryMediaPreview.mjs",
"ZenLibraryWidget.sys.mjs",
]

View File

@@ -13,6 +13,9 @@ import {
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FILE_GROUPS: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
fileGroupOf: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
DownloadsCommon:
@@ -25,22 +28,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
const FILE_MIME = "application/x-moz-file";
const OPENING_FEEDBACK_MS = 1500;
const FILE_TYPES = {
images: "png jpg jpeg gif webp svg bmp tif tiff heic heif avif ico",
video: "mp4 mkv mov avi webm m4v wmv flv mpg mpeg",
audio: "mp3 wav flac aac ogg oga m4a opus wma aiff",
documents:
"pdf doc docx xls xlsx ppt pptx txt md rtf odt ods odp csv epub pages numbers key",
archives: "zip rar 7z tar gz bz2 xz tgz zst",
apps: "dmg pkg exe msi app deb rpm appimage apk jar",
};
const EXTENSION_TYPES = new Map();
for (const [type, extensions] of Object.entries(FILE_TYPES)) {
for (const extension of extensions.split(" ")) {
EXTENSION_TYPES.set(extension, type);
}
}
export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
static id = "downloads";
static label = "library-downloads-section-title";
@@ -103,7 +90,7 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
{
id: "type",
titleL10nId: "library-downloads-filter-type",
options: Object.keys(FILE_TYPES).map(id => ({
options: lazy.FILE_GROUPS.map(id => ({
id,
l10nId: `library-downloads-type-${id}`,
})),
@@ -214,14 +201,11 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
}
#fileType(download) {
const extension = this.#fileName(download).match(/\.([^.]+)$/)?.[1];
return extension ? EXTENSION_TYPES.get(extension.toLowerCase()) : undefined;
return lazy.fileGroupOf(this.#fileName(download)) ?? undefined;
}
#activeTypes() {
return Object.keys(FILE_TYPES).filter(id =>
this.isFilterActive("type", id)
);
return lazy.FILE_GROUPS.filter(id => this.isFilterActive("type", id));
}
#whenCutoff() {
@@ -260,6 +244,19 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
}`;
}
/**
* @param {object} download - The download a row stands for
* @returns {string|null} The finished file itself, when it is a picture
* worth showing in place of a file icon
*/
#previewUrl(download) {
const path = download.succeeded && download.target.path;
if (!path || !lazy.canDrawThumbnail(path)) {
return null;
}
return PathUtils.toFileURI(path);
}
#isPending(download) {
return !download.stopped || (download.canceled && download.hasPartialData);
}
@@ -597,7 +594,12 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
>
<img
class="zen-library-row-icon"
src=${this.#iconUrl(download)}
?preview=${!!this.#previewUrl(download)}
src=${this.#previewUrl(download) ?? this.#iconUrl(download)}
@error=${event => {
event.target.removeAttribute("preview");
event.target.src = this.#iconUrl(download);
}}
alt=""
/>
<div class="zen-library-row-text">

View File

@@ -3,12 +3,73 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import { ZenLibrarySearchSection } from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
import { ZenLibraryMediaPreview } from "moz-src:///zen/library/ZenLibraryMediaPreview.mjs";
export class ZenLibraryMediaSection extends MozLitElement {
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
mediaKindOf: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
});
const FILE_MIME = "application/x-moz-file";
const HTML_NS = "http://www.w3.org/1999/xhtml";
const FOLDERS_PREF = "zen.library.media.folders";
const ENABLED_PREF = "zen.library.media.enabled";
const SKELETON_CARDS = 10;
const FOLDERS = {
downloads: { key: "DfltDwnld", home: "Downloads" },
documents: { key: "Docs", home: "Documents" },
desktop: { key: "Desk", home: "Desktop" },
pictures: { home: "Pictures" },
music: { home: "Music" },
videos: { home: AppConstants.platform === "macosx" ? "Movies" : "Videos" },
};
/**
* @param {string} fileName - A file found while looking through a folder
* @returns {string|null} How the grid shows it
*/
function kindFor(fileName) {
const kind = lazy.mediaKindOf(fileName);
if (kind === "video") {
return "video";
}
if (kind === "image") {
return lazy.canDrawThumbnail(fileName) ? "image" : "file";
}
return null;
}
const MAX_DEPTH = 2;
const MAX_FOLDERS = 60;
const MAX_ITEMS = 300;
export class ZenLibraryMediaSection extends ZenLibrarySearchSection {
static id = "media";
static label = "library-media-section-title";
static properties = {
items: { state: true },
loading: { state: true },
};
#preview = null;
#scanId = 0;
#scanned = [];
#scanHandle = null;
#thumbObserver = null;
#cardItems = new WeakMap();
constructor() {
super();
this.items = [];
this.loading = true;
}
static render(library) {
return html`
<zen-library-media-section
@@ -19,9 +80,391 @@ export class ZenLibraryMediaSection extends MozLitElement {
`;
}
render() {
get searchPlaceholderL10nId() {
return "library-media-search-placeholder";
}
get filterTitleL10nId() {
return "library-media-filter-title";
}
get filterGroups() {
if (!this.#enabled || this.#scanned.length < 2) {
return [];
}
return [
{
id: "folder",
titleL10nId: "library-media-filter-folders",
options: this.#scanned.map(path => ({
id: path,
label: PathUtils.filename(path),
})),
},
];
}
onFiltersChanged() {
this.requestUpdate();
}
connectedCallback() {
super.connectedCallback();
if (!this.#enabled) {
this.loading = false;
return;
}
this.#scanHandle = window.requestIdleCallback(() => this.#scan(), {
timeout: 600,
});
}
/** @returns {boolean} Whether looking through the folders was agreed to */
get #enabled() {
return Services.prefs.getBoolPref(ENABLED_PREF, false);
}
#enable() {
Services.prefs.setBoolPref(ENABLED_PREF, true);
this.loading = true;
this.requestUpdate();
this.#scan();
}
disconnectedCallback() {
super.disconnectedCallback();
this.#scanId++;
if (this.#scanHandle) {
window.cancelIdleCallback(this.#scanHandle);
this.#scanHandle = null;
}
this.#thumbObserver?.disconnect();
this.#thumbObserver = null;
this.#preview?.destroy();
this.#preview = null;
}
/**
* @returns {string[]} Absolute paths of the folders to look through. The
* pref takes named locations such as "downloads" and absolute paths
* alike, and anything that cannot be found is passed over.
*/
#folders() {
const home = Services.dirsvc.get("Home", Ci.nsIFile).path;
const paths = [];
for (const raw of Services.prefs
.getStringPref(FOLDERS_PREF, "")
.split(",")) {
const entry = raw.trim();
if (!entry) {
continue;
}
if (PathUtils.isAbsolute(entry)) {
paths.push(entry);
continue;
}
const folder = FOLDERS[entry.toLowerCase()];
if (!folder) {
continue;
}
let path = null;
if (folder.key) {
try {
path = Services.dirsvc.get(folder.key, Ci.nsIFile).path;
} catch (error) {
path = null;
}
}
paths.push(path ?? PathUtils.join(home, folder.home));
}
return [...new Set(paths)];
}
async #scan() {
const scanId = ++this.#scanId;
this.#scanHandle = null;
this.loading = true;
const found = [];
const budget = { folders: MAX_FOLDERS };
this.#scanned = this.#folders();
for (const folder of this.#scanned) {
budget.root = folder;
await this.#walk(folder, 0, found, budget);
if (scanId !== this.#scanId) {
return;
}
// Let the window breathe between folders.
await new Promise(resolve =>
window.requestIdleCallback(resolve, { timeout: 100 })
);
}
found.sort((a, b) => b.modified - a.modified);
this.items = found.slice(0, MAX_ITEMS);
this.loading = false;
}
/**
* @param {string} path - Folder to look through
* @param {number} depth - How far below the named folder this is
* @param {object[]} found - Collects the media
* @param {object} budget - Caps how many folders one scan opens
*/
async #walk(path, depth, found, budget) {
if (depth > MAX_DEPTH || budget.folders <= 0) {
return;
}
budget.folders--;
let children = [];
try {
children = await IOUtils.getChildren(path);
} catch (error) {
return;
}
const candidates = [];
for (const child of children) {
const fileName = PathUtils.filename(child);
if (fileName.startsWith(".")) {
continue;
}
const dot = fileName.lastIndexOf(".");
const extension = dot > 0 ? fileName.slice(dot + 1).toLowerCase() : "";
const kind = kindFor(fileName);
// A name with a suffix that is not media is a file to pass over, or a
// bundle on macOS. Either way there is nothing to ask the disk about.
if (!kind && extension) {
continue;
}
candidates.push({ child, fileName, extension, kind });
}
const stats = await Promise.all(
candidates.map(candidate =>
IOUtils.stat(candidate.child).catch(() => null)
)
);
const folders = [];
for (const [index, candidate] of candidates.entries()) {
const info = stats[index];
if (!info) {
continue;
}
if (info.type === "directory") {
if (!candidate.extension) {
folders.push(candidate.child);
}
continue;
}
if (!candidate.kind) {
continue;
}
found.push({
folder: budget.root,
path: candidate.child,
name: candidate.fileName,
extension: candidate.extension,
kind: candidate.kind,
modified: info.lastModified,
size: info.size,
url: PathUtils.toFileURI(candidate.child),
});
}
for (const folder of folders) {
await this.#walk(folder, depth + 1, found, budget);
}
}
// Interaction
#onDragStart(event, item) {
const file = new lazy.FileUtils.File(item.path);
if (!file.exists()) {
event.preventDefault();
return;
}
const { dataTransfer } = event;
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
dataTransfer.effectAllowed = "copyMove";
dataTransfer.setData("text/uri-list", item.url);
dataTransfer.addElement(event.currentTarget);
}
#onItemClick(event, item) {
this.#preview ??= new ZenLibraryMediaPreview(window);
const shown = this.#shown;
this.#preview.open({
items: shown,
index: shown.indexOf(item),
from: event.currentTarget.querySelector(".zen-library-media-frame"),
});
}
// Rendering
/**
* Builds one card, empty. Its picture is fetched only once the card comes
* into view, so opening the tab does not read every file at once.
*
* @param {object} item - The media the card stands for
* @returns {Element} Its card
*/
#buildCard(item) {
const card = document.createElementNS(HTML_NS, "div");
card.className = "zen-library-media-item";
card.draggable = true;
card.title = item.name;
card.addEventListener("dragstart", event => this.#onDragStart(event, item));
card.addEventListener("click", event => this.#onItemClick(event, item));
const frame = document.createElementNS(HTML_NS, "div");
frame.className = "zen-library-media-frame";
frame.dataset.kind = item.kind;
card.appendChild(frame);
this.#cardItems.set(card, item);
return card;
}
/**
* @param {Element} card - A card that has come into view
*/
#loadThumb(card) {
const item = this.#cardItems.get(card);
const frame = card.firstElementChild;
if (!item || !frame || frame.childElementCount) {
return;
}
const make = tag => {
const element = document.createElementNS(HTML_NS, tag);
frame.appendChild(element);
return element;
};
if (item.kind === "video") {
const video = make("video");
video.className = "zen-library-media-thumb";
video.src = `${item.url}#t=0.1`;
video.preload = "metadata";
video.muted = true;
video.tabIndex = -1;
const badge = make("img");
badge.className = "zen-library-media-play no-squircles";
badge.src = "chrome://browser/skin/zen-icons/media-play.svg";
badge.alt = "";
return;
}
const image = make("img");
image.className =
item.kind === "image"
? "zen-library-media-thumb"
: "zen-library-media-thumb zen-library-media-file";
image.src =
item.kind === "image" ? item.url : `moz-icon://${item.name}?size=128`;
image.alt = "";
}
#fillGrid() {
const grid = this.querySelector(".zen-library-media-grid");
if (!grid || !this.#enabled) {
return;
}
const shown = this.#shown;
const signature = shown.map(item => item.path).join("\n");
if (grid.dataset.signature === signature) {
return;
}
grid.dataset.signature = signature;
this.#thumbObserver?.disconnect();
this.#thumbObserver = new IntersectionObserver(
entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
this.#loadThumb(entry.target);
this.#thumbObserver.unobserve(entry.target);
}
}
},
{
root: this.querySelector(".zen-library-search-results"),
rootMargin: "300px",
}
);
const fragment = document.createDocumentFragment();
const cards = shown.map(item => this.#buildCard(item));
for (const card of cards) {
fragment.appendChild(card);
}
grid.replaceChildren(fragment);
for (const card of cards) {
this.#thumbObserver.observe(card);
}
}
/** @returns {object[]} What the grid shows, once the search has its say */
get #shown() {
const folders = this.#scanned.filter(path =>
this.isFilterActive("folder", path)
);
const query = this.searchQuery.trim().toLowerCase();
return this.items.filter(
item =>
(!folders.length || folders.includes(item.folder)) &&
(!query || item.name.toLowerCase().includes(query))
);
}
renderItems() {
if (!this.#enabled) {
return this.#renderOptIn();
}
if (this.loading) {
return html`
<div
class="zen-library-empty"
data-l10n-id="library-media-loading"
></div>
`;
}
const shown = this.#shown;
if (!shown.length) {
return html`
<div class="zen-library-empty" data-l10n-id="library-media-empty"></div>
`;
}
return html`<div class="zen-library-media-grid"></div>`;
}
updated(changedProperties) {
super.updated?.(changedProperties);
this.toggleAttribute("needs-opt-in", !this.#enabled);
this.#fillGrid();
}
/**
* An empty grid behind a note saying what turning this on would do. The
* cards are only shapes: nothing has been looked at yet.
*/
#renderOptIn() {
return html`
<div class="zen-library-empty" data-l10n-id="library-media-empty"></div>
<div class="zen-library-media-grid zen-library-media-skeleton" behind>
${Array.from(
{ length: SKELETON_CARDS },
() => html`<div class="zen-library-media-item"></div>`
)}
</div>
<div class="zen-library-media-opt-in">
<div class="zen-library-media-opt-in-icon">
<div class="empty-state-icon-image"></div>
</div>
<h3 data-l10n-id="library-media-opt-in-title"></h3>
<p data-l10n-id="library-media-opt-in-body"></p>
<button
class="zen-big-accent-button primary zen-library-media-opt-in-button"
data-l10n-id="library-media-opt-in-button"
@click=${() => this.#enable()}
></button>
<div class="zen-library-media-opt-in-note">
<img src="chrome://global/skin/icons/security.svg" alt="" />
<span data-l10n-id="library-media-opt-in-note"></span>
</div>
</div>
`;
}
}

View File

@@ -11,6 +11,7 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
import { ZenLibraryDragAndDrop } from "moz-src:///zen/library/ZenLibraryDragAndDrop.mjs";
const GRADIENT_TOPIC = "zen-space-gradient-update";
const SIZING_FALLBACK_MS = 600;
const SCROLL_EDGE_PX = 48;
const SCROLL_STEP_PX = 12;
@@ -69,8 +70,6 @@ export class ZenLibrarySpacesSection extends MozLitElement {
#observer = { observe: () => this.requestUpdate() };
#onDataChanged = () => this.requestUpdate();
#onStripEvent = event => {
// The copies raise the same events as they are built; those are not
// changes to the real strips.
if (this.contains(event.target)) {
return;
}
@@ -100,15 +99,13 @@ export class ZenLibrarySpacesSection extends MozLitElement {
this.#dnd.clearDragOverVisuals();
this.#refreshStrips();
};
// A drag from a copy ran on the strip's drag and drop, whose own drag end
// only hears drags from the strip; it gets to finish this one too. The
// event comes to the copy itself, which the drop's rebuild has taken out
// of the document by then, so it never reaches the window.
#onCopyDragEnd = event => {
gBrowser.tabContainer.tabDragAndDrop.handle_dragend(event);
this.#onDragEnd();
};
#landing = false;
#sizingTimer = null;
#pendingGroups = [];
/** @type {WeakMap<Element, Element>} copied tab or group to the real one */
#realElements = new WeakMap();
#dnd = new ZenLibraryDragAndDrop(this);
@@ -162,6 +159,8 @@ export class ZenLibrarySpacesSection extends MozLitElement {
}
clearTimeout(this.#refreshTimer);
this.#refreshTimer = null;
window.clearTimeout(this.#sizingTimer);
this.library?.removeAttribute("sizing");
this.library?.style.removeProperty("--zen-library-content-width");
}
@@ -179,17 +178,55 @@ export class ZenLibrarySpacesSection extends MozLitElement {
}
}
onShown() {
this.#updateLibraryWidth();
}
onHidden() {
window.clearTimeout(this.#sizingTimer);
this.library?.removeAttribute("sizing");
this.library?.style.removeProperty("--zen-library-content-width");
}
#updateLibraryWidth() {
const side = this.library?.querySelector("#zen-library-side");
const list = this.querySelector(".zen-library-spaces");
if (!side || !list) {
if (!side || !list || this.hidden) {
return;
}
const sideWidth = window.windowUtils.getBoundsWithoutFlushing(side).width;
this.library.style.setProperty(
"--zen-library-content-width",
`${sideWidth + this.#cardsWidth(list)}px`
);
const width = `${sideWidth + this.#cardsWidth(list)}px`;
if (
this.library.style.getPropertyValue("--zen-library-content-width") ===
width
) {
return;
}
this.library.style.setProperty("--zen-library-content-width", width);
this.#markResizing();
}
/**
* Holds the cards' scrollbars back while the library grows or shrinks
* around them, where they would otherwise flash into view for the length
* of the animation.
*/
#markResizing() {
const library = this.library;
library.setAttribute("sizing", "true");
window.clearTimeout(this.#sizingTimer);
const done = () => {
window.clearTimeout(this.#sizingTimer);
library.removeAttribute("sizing");
library.removeEventListener("transitionend", onEnd);
};
const onEnd = event => {
if (event.target === library && event.propertyName === "width") {
done();
}
};
library.addEventListener("transitionend", onEnd);
this.#sizingTimer = window.setTimeout(done, SIZING_FALLBACK_MS);
}
/**
@@ -285,14 +322,20 @@ export class ZenLibrarySpacesSection extends MozLitElement {
continue;
}
const before = rebuild ? this.#rowPositions(strip) : null;
strip.textContent = "";
const space = gZenWorkspaces.workspaceElement(card.dataset.uuid);
if (!space) {
strip.textContent = "";
continue;
}
// Built away from the document, so a card's rows land in one go
// rather than one reflow at a time.
const rows = document.createDocumentFragment();
this.#pendingGroups = [];
for (const section of [space.pinnedTabsContainer, space.tabsContainer]) {
this.#appendCopy(strip, section);
this.#appendCopy(rows, section);
}
strip.replaceChildren(rows);
this.#fillGroups();
// Rows slide to their new places, unless the drag image is landing on
// the moved one, which is motion enough.
if (before && !this.#landing) {
@@ -301,6 +344,22 @@ export class ZenLibrarySpacesSection extends MozLitElement {
}
}
/**
* Fills every copied group now that the strip is in the document. A group
* nested inside another is connected as soon as its parent takes it, so
* this list drains in order.
*/
#fillGroups() {
while (this.#pendingGroups.length) {
const { copy, inner } = this.#pendingGroups.shift();
for (const child of inner.children) {
if (!child.classList.contains("zen-tab-group-start")) {
this.#appendCopy(copy, child);
}
}
}
}
/**
* The top-left of every row in a strip, keyed by the copy's stable id, so a
* row can be matched across a rebuild.
@@ -405,12 +464,12 @@ export class ZenLibrarySpacesSection extends MozLitElement {
...[...realIcon.children].map(child => child.cloneNode(true))
);
}
const inner = node.querySelector(":scope > .tab-group-container") ?? node;
for (const child of inner.children) {
if (!child.classList.contains("zen-tab-group-start")) {
this.#appendCopy(copy, child);
}
}
// A group empties itself and builds its own scaffolding the moment it
// joins the document, so its rows are put in afterwards.
this.#pendingGroups.push({
copy,
inner: node.querySelector(":scope > .tab-group-container") ?? node,
});
return;
}
if (!node.querySelector("tab, tab-group, zen-folder")) {
@@ -451,6 +510,7 @@ export class ZenLibrarySpacesSection extends MozLitElement {
} else {
gZenFolders.animateExpand(copy);
}
gZenFolders.relayoutCollapsedFolder(group);
}
return;
}

View File

@@ -4,32 +4,6 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#zen-appcontent-wrapper {
--library-progress: 0;
--library-wrapper-target-px: 0px;
transform: translateX(calc(var(--library-progress) * var(--library-wrapper-target-px)));
}
#navigator-toolbox {
--toolbox-progress: min(1, calc(var(--library-progress) * 1.5));
transform: scale(calc(1 - var(--toolbox-progress) * 0.04));
opacity: calc(1 - var(--toolbox-progress)) !important;
}
#zen-main-app-wrapper[zen-compact-mode="true"] #navigator-toolbox {
@media -moz-pref('zen.view.compact.hide-tabbar') or -moz-pref('zen.view.use-single-toolbar') {
transform: translateX(calc(var(--toolbox-progress) * -100%));
opacity: 1 !important;
}
}
:root[zen-right-side="true"] #zen-main-app-wrapper[zen-compact-mode="true"] #navigator-toolbox {
@media -moz-pref('zen.view.compact.hide-tabbar') or -moz-pref('zen.view.use-single-toolbar') {
transform: translateX(calc(var(--toolbox-progress) * 100%));
}
}
:root:not([zen-sidebar-expanded="true"]) .zen-library-window-buttons-clone {
display: none;
}
@@ -54,7 +28,6 @@ zen-library {
visibility: hidden;
pointer-events: none;
transform: translateX(calc(-100% * (1 - var(--library-progress, 0))));
& #zen-library-panel {
flex-direction: row;
@@ -106,14 +79,14 @@ zen-library[open] {
scrollbar-width: thin;
}
zen-library[transitioning] {
& #zen-library-content {
pointer-events: none;
}
zen-library[transitioning] #zen-library-content {
pointer-events: none;
}
& :is(.zen-library-search-results, .zen-library-spaces, .zen-library-space-body) {
scrollbar-width: none;
}
zen-library:is([transitioning], [sizing])
:is(.zen-library-search-results, .zen-library-spaces, .zen-library-space-body) {
scrollbar-width: none;
overflow-x: hidden;
}
#zen-library-panel {
@@ -310,6 +283,187 @@ zen-library[zen-library-downloading] .zen-library-tab[data-section="downloads"]:
background-image: url("chrome://browser/skin/zen-icons/library/library-media-sprite.svg");
}
.zen-library-media-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 12px;
padding: 6px;
align-content: start;
-moz-window-dragging: no-drag;
}
.zen-library-media-item {
border-radius: 16px;
background: var(--zen-library-pill-bg);
padding: 14px;
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 1;
transition:
background 0.12s ease,
scale 0.14s ease;
&:hover {
background: var(--zen-library-pill-hover-bg);
scale: 1.04;
}
&:hover:active {
scale: 1;
}
}
.zen-library-media-frame {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.zen-library-media-thumb {
max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
pointer-events: none;
}
.zen-library-media-file {
box-shadow: none;
}
#zen-library-panel
:is(.zen-library-section, .zen-library-search-results)
> * {
animation: zen-library-section-fade 0.22s ease;
}
@keyframes zen-library-section-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.zen-library-media-skeleton {
pointer-events: none;
& .zen-library-media-item {
animation: zen-library-media-wait 1.8s ease-in-out infinite;
}
& .zen-library-media-item:nth-child(even) {
animation-delay: 0.3s;
}
&[behind] {
opacity: 0.7;
}
}
@keyframes zen-library-media-wait {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.45;
}
}
zen-library-media-section[needs-opt-in] {
& .zen-library-search-top {
display: none;
}
& .zen-library-search-results {
overflow: hidden;
}
}
.zen-library-media-opt-in {
position: absolute;
inset-inline: 6px;
top: 50%;
translate: 0 -50%;
padding: 22px 22px 18px;
border-radius: 22px;
background: light-dark(rgb(250, 250, 252), rgb(28, 29, 33));
box-shadow:
0 18px 50px rgba(0, 0, 0, 0.45),
0 0 0 1px light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));
text-align: center;
& h3 {
margin: 0 0 8px;
font-size: 15px;
font-weight: 700;
}
& p {
margin: 0 0 16px;
font-size: 13px;
line-height: 1.45;
opacity: 0.85;
}
}
.zen-library-media-opt-in-icon {
--zen-library-sprite-size: 40px;
width: var(--zen-library-sprite-size);
height: var(--zen-library-sprite-size);
margin: 2px auto 12px;
overflow: clip;
}
.zen-library-media-opt-in-button {
width: 100%;
}
.zen-library-media-opt-in-note {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 14px;
font-size: 11px;
line-height: 1.4;
opacity: 0.6;
text-align: start;
& img {
flex: none;
width: 13px;
height: 13px;
-moz-context-properties: fill;
fill: currentColor;
}
}
.zen-library-media-play {
position: absolute;
width: 34px;
height: 34px;
padding: 9px;
box-sizing: border-box;
border-radius: 100%;
background: rgba(0, 0, 0, 0.45);
-moz-context-properties: fill;
fill: white;
pointer-events: none;
}
@keyframes zen-library-sprite-play {
from {
transform: translateX(0);
@@ -538,6 +692,17 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
width: 16px;
height: 16px;
border-radius: 4px;
&[preview] {
object-fit: cover;
}
zen-library-downloads-section &[preview] {
width: 34px;
height: 34px;
border-radius: 6px;
margin-inline: -4px;
}
margin-inline: 2px 1px;
-moz-context-properties: fill;
fill: currentColor;
@@ -662,7 +827,7 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
position: absolute;
inset: 0;
box-sizing: border-box;
border-radius: 10px;
border-radius: 12px;
border: 3px solid rgba(255, 255, 255, .6);
background: rgba(237, 237, 237, 0.6);
box-shadow: 0 0 6px 1px rgba(0, 0, 0, 0.1);
@@ -701,7 +866,7 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
left: 50%;
width: 135%;
height: 4px;
border-radius: 10px;
border-radius: 12px;
background: light-dark(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.5));
transform: translate(-50%, -50%) rotate(45deg);
pointer-events: none;

View File

@@ -43,15 +43,21 @@ export class ZenSpacesSwipe {
);
}
#readySwipeLibrary = null;
#readySwipeOpenLibrary() {
if (this.#readySwipeLibrary) {
return this.#readySwipeLibrary;
}
const spaces = gZenWorkspaces.getWorkspaces();
const current = gZenWorkspaces.getActiveWorkspaceFromCache();
const libraryEnabled = Services.prefs.getBoolPref("zen.library.enabled");
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
return (
this.#readySwipeLibrary =
spaces.indexOf(current) === (libraryOnRight ? spaces.length - 1 : 0) &&
libraryEnabled
);
libraryEnabled;
return this.#readySwipeLibrary;
}
attachWorkspaceSwipeGestures(element) {
@@ -154,6 +160,19 @@ export class ZenSpacesSwipe {
}
}
#toggleSwipeGestureAttr(enable) {
const elements = [
"zen-workspace",
"#tabbrowser-arrowscrollbox",
".zen-browser-grain",
];
elements.forEach(el =>
document
.querySelectorAll(el)
.forEach(node => node?.toggleAttribute("swipe-gesture", enable))
);
}
_handleSwipeStart(event) {
const ws = gZenWorkspaces;
@@ -163,7 +182,7 @@ export class ZenSpacesSwipe {
gZenFolders.cancelPopupTimer();
document.documentElement.setAttribute("swipe-gesture", "true");
this.#toggleSwipeGestureAttr(true);
document.addEventListener("popupshown", this._popupOpenHandler, {
once: true,
});
@@ -286,6 +305,7 @@ export class ZenSpacesSwipe {
if (this._swipeState.isSwipingLibrary) {
lazy.ZenLibrary.stopSwipe(rawDirection * direction);
this.#readySwipeLibrary = null;
return;
}
@@ -297,6 +317,7 @@ export class ZenSpacesSwipe {
if (this._swipeState.isSwipingLibrary) {
lazy.ZenLibrary.stopSwipe(null);
this.#readySwipeLibrary = null;
}
// Reset swipe state
@@ -309,7 +330,7 @@ export class ZenSpacesSwipe {
};
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);
document.documentElement.removeAttribute("swipe-gesture");
this.#toggleSwipeGestureAttr(false);
gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
[lazy.browserBackgroundElement, lazy.toolbarBackgroundElement].forEach(
element => {

View File

@@ -43,20 +43,30 @@
fill: currentColor;
scroll-margin: 0 20px;
@media (-moz-platform: macos) {
height: 32px;
max-width: 32px;
}
& .zen-workspace-icon {
pointer-events: none;
line-height: 0;
position: absolute;
font-size: 12px;
@media (-moz-platform: macos) {
font-size: 14px;
}
&:is(img) {
width: 14px;
}
@media (-moz-platform: macos) {
font-size: 15px;
&:is(img) {
width: 16px;
}
}
&[no-icon='true'] {
width: 8px;
height: 8px;
@@ -356,7 +366,7 @@ zen-workspace {
overscroll-behavior: auto;
}
:root[swipe-gesture] &::part(scrollbox) {
&[swipe-gesture]::part(scrollbox) {
scrollbar-width: none;
}
@@ -404,7 +414,7 @@ zen-workspace {
}
}
:root:not(:is([animating-background], [swipe-gesture])) #navigator-toolbox:not([movingtab]) &:not([active]) {
&:not([swipe-gesture]) :root:not(:is([animating-background])) #navigator-toolbox:not([movingtab]) &:not([active]) {
-moz-subtree-hidden-only-visually: 1;
content-visibility: hidden;
}

View File

@@ -890,7 +890,7 @@ zen-library {
display: none !important;
}
:root[zen-library-open="true"] #zen-sidebar-splitter {
#zen-sidebar-splitter[zen-library-open="true"] {
pointer-events: none !important;
}