Files
desktop/src/zen/library/ZenLibrary.mjs

600 lines
16 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { html } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const { ZenLibraryWidget } = ChromeUtils.importESModule(
"moz-src:///zen/library/ZenLibraryWidget.sys.mjs"
);
let lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
ZenLibraryHistorySection:
"moz-src:///zen/library/sections/ZenLibraryHistorySection.mjs",
ZenLibraryDownloadsSection:
"moz-src:///zen/library/sections/ZenLibraryDownloadsSection.mjs",
ZenLibraryBoostsSection:
"moz-src:///zen/library/sections/ZenLibraryBoostsSection.mjs",
ZenLibraryMediaSection:
"moz-src:///zen/library/sections/ZenLibraryMediaSection.mjs",
ZenLibrarySpacesSection:
"moz-src:///zen/library/sections/ZenLibrarySpacesSection.mjs",
},
{ global: "current" }
);
const LAST_TAB_PREF = "zen.library.last-tab";
ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
return document.getElementById("zen-appcontent-wrapper");
});
export class ZenLibrary extends MozLitElement {
static instance = null;
#progress = 0;
#springControls = null;
#toolboxWidth = 0;
#originalButtonsNextSibling = null;
get #hasAdoptedButtons() {
return this.#originalButtonsNextSibling !== null;
}
#canSwipe = false;
#isOpen = false;
#isWrapperSwipeAttached = false;
#wrapperGestureControl = null;
#resizeObserver = new ResizeObserver(() => {
this.openProgress = this.#progress;
});
static queries = {
_content: "#zen-library-content",
_header: "#zen-library-header",
_footer: "#zen-library-footer",
};
static properties = {
_activeTab: { type: String },
};
constructor() {
super();
this.zenLibrarySections = {
media: lazy.ZenLibraryMediaSection,
downloads: lazy.ZenLibraryDownloadsSection,
boosts: lazy.ZenLibraryBoostsSection,
...(!window.gZenWorkspaces.privateWindowOrDisabled
? {
spaces: lazy.ZenLibrarySpacesSection,
}
: {}),
history: lazy.ZenLibraryHistorySection,
};
const lastTab = Services.prefs.getStringPref(LAST_TAB_PREF, "history");
this.activeTab = lastTab in this.zenLibrarySections ? lastTab : "history";
this.#hijackFirefoxCommands();
}
static get isLibraryOpen() {
const lib = this.getInstance();
return lib.#isOpen;
}
static get isLibrarySlightlyOpen() {
const lib = this.getInstance(/* createIfMissing = */ false);
if (!lib) {
return false;
}
// Due to calculation inaccuracies assume
// that openProgress never goes back to 0
return lib.openProgress > 0.001;
}
set activeTab(value) {
if (this._activeTab === value) {
return;
}
this._activeTab = value;
Services.prefs.setStringPref(LAST_TAB_PREF, value);
}
get activeTab() {
return this._activeTab;
}
get activeSection() {
return this.zenLibrarySections[this.activeTab];
}
set openProgress(value) {
const p = value;
const stealWindowButtonsPastPoint = 0.6;
const wasOpen = this.#progress > 0.001;
this.#progress = p;
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;
lazy.appContentWrapper?.style.setProperty(
"--library-wrapper-target-px",
`${webOffset}px`
);
[this, lazy.appContentWrapper, gNavToolbox].forEach(elem => {
elem?.style.setProperty("--library-progress", String(p));
});
if (isOpen && !wasOpen) {
this.setAttribute("open", "true");
document.documentElement.setAttribute("zen-library-open", "true");
} else if (!isOpen && wasOpen) {
this.removeAttribute("open");
document.documentElement.removeAttribute("zen-library-open");
}
if (isPastWindowButtonSwitchPoint && this.#coversWindowButtons) {
this.#adoptWindowButtons();
} else if (!isPastWindowButtonSwitchPoint) {
this.#restoreWindowButtons();
}
}
/**
* Whether the window buttons sit in the sidebar column the library covers.
*/
get #coversWindowButtons() {
if (!gZenVerticalTabsManager.isWindowsStyledButtons) {
return !this.#libraryOnRight;
}
return this.#libraryOnRight && !this.#isCompactMode;
}
get openProgress() {
return this.#progress;
}
#hijackFirefoxCommands() {
document
.getElementById("Browser:ShowAllHistory")
.addEventListener("command", event => {
event.stopPropagation();
event.stopImmediatePropagation();
ZenLibrary.toggle("history");
});
}
#adoptWindowButtons() {
if (this.#hasAdoptedButtons) {
return;
}
const realButtons = gZenVerticalTabsManager.actualWindowButtons;
if (!this.#originalButtonsNextSibling) {
this.#originalButtonsNextSibling = {
isNext: realButtons.nextSibling,
sibling: realButtons.nextSibling || realButtons.previousSibling,
clone: realButtons.cloneNode(true),
};
this.#originalButtonsNextSibling.clone.classList.add(
"zen-library-window-buttons-clone"
);
if (this.#originalButtonsNextSibling.isNext) {
this.#originalButtonsNextSibling.sibling.before(
this.#originalButtonsNextSibling.clone
);
} else {
this.#originalButtonsNextSibling.sibling.after(
this.#originalButtonsNextSibling.clone
);
}
this._header.appendChild(realButtons);
}
}
#restoreWindowButtons() {
if (!this.#hasAdoptedButtons) {
return;
}
const realButtons = gZenVerticalTabsManager.actualWindowButtons;
if (this.#originalButtonsNextSibling) {
this.#originalButtonsNextSibling.clone.remove();
if (this.#originalButtonsNextSibling.isNext) {
this.#originalButtonsNextSibling.sibling.before(realButtons);
} else {
this.#originalButtonsNextSibling.sibling.after(realButtons);
}
this.#originalButtonsNextSibling = null;
}
}
#stylesLoaded = null;
#whenStylesLoaded() {
this.#stylesLoaded ??= this.updateComplete.then(() => {
const link = this.querySelector("link[rel='stylesheet']");
if (!link || link.sheet) {
return undefined;
}
return new Promise(resolve => {
link.addEventListener("load", resolve, { once: true });
link.addEventListener("error", resolve, { once: true });
});
});
return this.#stylesLoaded;
}
#idleCleanup = null;
#scheduleIdleCleanup() {
this.#idleCleanup = window.requestIdleCallback(() => {
this.#idleCleanup = null;
this.#stylesLoaded = null;
ZenLibrary.instance = null;
this.remove();
});
}
#cancelIdleCleanup() {
if (this.#idleCleanup) {
window.cancelIdleCallback(this.#idleCleanup);
this.#idleCleanup = null;
}
}
/**
* Opens or closes the library. With a tab id, opens the library on that
* tab, switches to it if already open on another, or closes if it is
* already the open one. Without one, plainly toggles open and closed.
*
* @param {string?} [tab] - A section id to open on
*/
static toggle(tab = undefined) {
if (!Services.prefs.getBoolPref("zen.library.enabled")) {
return;
}
const lib = this.getInstance();
if (tab && tab in lib.zenLibrarySections) {
if (lib.#isOpen && lib.activeTab === tab) {
this.animateProgress(0);
} else if (lib.#isOpen) {
lib.activeTab = tab;
} else {
lib.activeTab = tab;
this.animateProgress(1);
}
return;
}
this.animateProgress(lib.#isOpen ? 0 : 1);
}
static async animateProgress(target) {
const lib = this.getInstance();
lib.#detachWrapperOfSwipe();
lib.#cancelIdleCleanup();
await lib.#whenStylesLoaded();
lib.style.visibility = "";
await window.promiseDocumentFlushed(() => {});
if (lib.#springControls) {
lib.#springControls.stop();
lib.#springControls = null;
}
if (target === 1) {
lib.#onOpenLibrary();
lib.#isOpen = true;
} else if (target === 0) {
lib.#isOpen = false;
}
lib.setAttribute("transitioning", "true");
lib.#springControls = gZenUIManager.motion.animate(
lib.openProgress,
target,
{
type: "spring",
stiffness: 720,
damping: 47,
mass: 1.2,
onUpdate: latest => {
lib.openProgress = latest;
},
onComplete: () => {
lib.openProgress = target;
lib.#springControls = null;
lib.removeAttribute("transitioning");
if (target === 0) {
lib.#scheduleIdleCleanup();
}
},
}
);
}
static async startSwipe() {
const lib = this.getInstance();
lib.#cancelIdleCleanup();
lib.#canSwipe = true;
await lib.#whenStylesLoaded();
lib.style.visibility = "";
await window.promiseDocumentFlushed(() => {});
if (!lib.#canSwipe) {
return;
}
lib.#onOpenLibrary();
if (lib.#springControls) {
lib.#springControls.stop();
lib.#springControls = null;
}
lib.style.setProperty("pointer-events", "none");
lib.#attachWrapperToSwipe();
}
static stopSwipe(direction) {
const lib = this.getInstance();
lib.style.setProperty("pointer-events", "unset");
lib.#canSwipe = false;
if (lib.#libraryOnRight) {
direction = direction * -1;
}
if (direction) {
const target = Math.max(-direction, 0);
this.animateProgress(target);
}
lib.#detachWrapperOfSwipe();
// Return library open state
return lib.#isOpen;
}
static swipeProgress(target) {
const lib = this.getInstance();
if (!lib.#canSwipe) {
return;
}
lib.openProgress = target;
}
#attachWrapperToSwipe() {
if (!this.#isWrapperSwipeAttached) {
const appWrapper = document.getElementById("zen-main-app-wrapper");
this.#wrapperGestureControl =
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(
appWrapper
);
this.#isWrapperSwipeAttached = true;
}
}
#detachWrapperOfSwipe() {
if (this.#isWrapperSwipeAttached || this.#wrapperGestureControl) {
const appWrapper = document.getElementById("zen-main-app-wrapper");
window.gZenWorkspaces._swipeManager?.detachWorkspaceSwipeGestures(
appWrapper,
this.#wrapperGestureControl
);
this.#wrapperGestureControl = null;
this.#isWrapperSwipeAttached = false;
}
}
#onOpenLibrary() {
gURLBar.view.close();
// Get the width from the css property,
// getBoundsWithoutFlushing will fail as it takes the
// toolbox transformation during the animation into account
this.#toolboxWidth = parseFloat(
gNavToolbox.style
.getPropertyValue("--actual-zen-sidebar-width")
.replace("/\D/g", "")
);
if (document.documentElement.hasAttribute("zen-sidebar-expanded")) {
this.#toolboxWidth += window.windowUtils.getBoundsWithoutFlushing(
document.getElementById("zen-sidebar-splitter")
).width;
}
}
static getInstance(createIfMissing = true) {
if (!this.instance && createIfMissing) {
this.instance = new ZenLibrary();
this.instance.style.visibility = "collapse";
const mountRoot = document.getElementById("zen-main-app-wrapper");
mountRoot.append(this.instance);
}
return this.instance;
}
get #libraryOnRight() {
return gZenVerticalTabsManager._prefsRightSide;
}
static get libraryOnRight() {
const lib = this.getInstance();
return lib.#libraryOnRight;
}
createRenderRoot() {
return this;
}
connectedCallback() {
if (super.connectedCallback) {
super.connectedCallback();
}
this.onKeyDown = this.onKeyDown.bind(this);
document.addEventListener("keydown", this.onKeyDown, true);
this.#resizeObserver.observe(this);
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(this);
this._tabOpen = this.onTabOpen.bind(this);
window.addEventListener("TabOpen", this._tabOpen);
ZenLibraryWidget.attachLibrary(this);
}
disconnectedCallback() {
this.#cancelIdleCleanup();
if (this.#springControls) {
this.#springControls.stop();
this.#springControls = null;
}
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;
}
}
get #isCompactMode() {
return (
window.gZenCompactModeManager.preference &&
Services.prefs.getBoolPref("zen.view.compact.hide-tabbar")
);
}
onTabOpen() {
if (this.#isOpen) {
ZenLibrary.animateProgress(0);
}
}
onKeyDown(e) {
if (!this.hasAttribute("open")) {
return;
}
if (e.key === "Escape") {
ZenLibrary.animateProgress(0);
}
}
firstUpdated() {
if (super.firstUpdated) {
super.firstUpdated();
}
this.#buildFooterButtons();
}
#buildFooterButtons() {
const footer = this.querySelector("#zen-library-footer");
const buttons = [
{
image: "chrome://browser/skin/zen-icons/back.svg",
l10nId: "library-footer-close-button",
command: () => ZenLibrary.animateProgress(0),
},
{
image: "chrome://browser/skin/zen-icons/heart-circle-fill.svg",
l10nId: "library-footer-donate-button",
command: () => {
window.openTrustedLinkIn("https://www.zen-browser.app/donate", "tab");
ZenLibrary.animateProgress(0);
},
},
];
for (const { image, l10nId, command } of buttons) {
const button = document.createXULElement("toolbarbutton");
button.className = "toolbarbutton-1";
button.setAttribute("image", image);
button.setAttribute("data-l10n-id", l10nId);
button.addEventListener("command", command);
footer.appendChild(button);
}
}
#animateTabIcon(tab) {
tab.removeAttribute("animate");
// Flush styles so re-adding the attribute restarts the animation.
void tab.offsetWidth;
tab.setAttribute("animate", "true");
}
render() {
return html`
<link
rel="stylesheet"
href="chrome://browser/content/zen-styles/zen-library.css"
/>
<hbox id="zen-library-panel">
<vbox id="zen-library-side">
<vbox id="zen-library-header"></vbox>
<vbox id="zen-library-sidebar-tabs">
${Object.values(this.zenLibrarySections).map(
Section => html`
<vbox
class="zen-library-tab"
?active=${this.activeTab === Section.id}
data-section=${Section.id}
@click=${event => {
if (this.activeTab !== Section.id) {
this.activeTab = Section.id;
this.#animateTabIcon(event.currentTarget);
}
}}
>
<div class="zen-library-tab-icon">
<div class="zen-library-tab-icon-image"></div>
</div>
<label data-l10n-id=${Section.label}></label>
</vbox>
`
)}
</vbox>
<toolbar
id="zen-library-footer"
class="chromeclass-location"
mode="icons"
fullscreentoolbar="true"
></toolbar>
</vbox>
<vbox id="zen-library-content">
${this.activeSection.render(this)}
</vbox>
</hbox>
`;
}
}
customElements.define("zen-library", ZenLibrary);