gh-15438: Implementing Zen Library (gh-15282)

Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
This commit is contained in:
fen4flo
2026-09-17 15:03:20 +02:00
committed by GitHub
parent 45b9cfe37f
commit 2ee1275f28
50 changed files with 13534 additions and 182 deletions

View File

@@ -0,0 +1,88 @@
# 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-button =
.label = Library
.tooltiptext = Library
library-footer-close-button =
.tooltiptext = Close library
library-footer-donate-button =
.tooltiptext = Donate to Zen
library-history-section-title = History
library-downloads-section-title = Downloads
library-boosts-section-title = Boosts
library-spaces-section-title = Spaces
library-filter-button = Filter
library-filter-done = Done
library-history-filter-title = Filter History…
library-history-filter-when = When was it visited?
library-filter-today = Today
library-filter-week = This Week
library-filter-month = This Month
library-history-filter-sort = Sort by
library-history-sort-date = By Date
library-history-sort-site = By Site
library-history-sort-most-visited = By Most Visited
library-history-sort-last-visited = By Last Visited
library-history-site-other = Other
library-history-search-placeholder =
.placeholder = Search History…
library-history-empty = No history found
library-history-forget-button =
.title = Remove from history
library-history-reopen-button =
.title = Reopen page
library-downloads-empty = No downloads found
library-downloads-more-button =
.title = More options
library-downloads-menu-open =
.label = Open { $name }
library-downloads-menu-copy =
.label = Copy { $name }
library-downloads-menu-hide =
.label = Hide from Zen
library-downloads-menu-trash =
.label = Move to Trash
library-downloads-opening-in = { PLATFORM() ->
[macos] Opening in Finder…
[windows] Opening in File Explorer…
*[other] Opening in file manager…
}
library-downloads-cancel-button =
.title = Cancel download
library-downloads-filter-title = Filter Downloads…
library-downloads-filter-type = File type
library-downloads-filter-when = When was it downloaded?
library-downloads-type-images = Images
library-downloads-type-video = Video
library-downloads-type-audio = Audio
library-downloads-type-documents = Documents
library-downloads-type-archives = Archives
library-downloads-type-apps = Apps
library-downloads-retry-button =
.title = Retry download
library-spaces-theme-button =
.title = Change theme
library-spaces-move-button =
.title = Drag to reorder
library-spaces-actions-button =
.tooltiptext = More options
library-spaces-icon-button =
.title = Change icon
library-spaces-name-button =
.title = Rename space
library-boosts-search-placeholder =
.placeholder = Search Boosts…
library-boosts-empty = No boosts yet
library-boosts-toggle =
.aria-label = Enable boost
library-boosts-menu-edit =
.label = Edit Boost

9
prefs/zen/library.yaml Normal file
View File

@@ -0,0 +1,9 @@
# 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/.
- name: zen.library.enabled
value: "@IS_TWILIGHT@"
- name: zen.library.last-tab
value: "history"

View File

@@ -1,8 +1,8 @@
diff --git a/.stylelintignore b/.stylelintignore
index 185490999507b8a5032977237af50f5e61c71df1..77c1635d90c6480af4ebd3c6e3ec49780eaca571 100644
index 185490999507b8a5032977237af50f5e61c71df1..4b1b1dc76d316f38f60c90e6cbd74d9569800d04 100644
--- a/.stylelintignore
+++ b/.stylelintignore
@@ -106,3 +106,19 @@ build/pgo/blueprint/**/*.css
@@ -106,3 +106,20 @@ build/pgo/blueprint/**/*.css
# under our control or we don't want to modify at this point:
testing/web-platform/mozilla/
testing/web-platform/tests/
@@ -20,5 +20,6 @@ index 185490999507b8a5032977237af50f5e61c71df1..77c1635d90c6480af4ebd3c6e3ec4978
+zen/split-view/zen-split-view.css
+zen/spaces/zen-workspaces.css
+zen/common/styles/zen-toolbar.css
+zen/common/styles/zen-single-components.css
+
+*.inc

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index a76bb7aae7636c07989a4acb35281777d3103505..c5eafbdad5270cd9926ed4f1d850d134440d775b 100644
index a76bb7aae7636c07989a4acb35281777d3103505..d35aa0457307e95ad00464861fcee2827325193e 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -2,7 +2,7 @@
@@ -55,3 +55,11 @@ index a76bb7aae7636c07989a4acb35281777d3103505..c5eafbdad5270cd9926ed4f1d850d134
<toolbar id="nav-bar"
class="browser-toolbar"
@@ -180,7 +188,6 @@
<toolbartabstop/>
<html:moz-urlbar id="urlbar"
class="urlbar"
- popover="manual"
pageproxystate="invalid"
unifiedsearchbutton-available=""
sap-name="urlbar">

View File

@@ -22,3 +22,4 @@
#include ../../../zen/live-folders/jar.inc.mn
#include ../../../zen/space-routing/jar.inc.mn
#include ../../../zen/share/jar.inc.mn
#include ../../../zen/library/jar.inc.mn

View File

@@ -72,4 +72,6 @@
<command id="cmd_zenNewLiveFolder" />
<command id="cmd_zenDuplicateTab" />
<command id="cmd_zenToggleLibrary" />
</commandset>

View File

@@ -13,4 +13,5 @@
<link rel="localization" href="browser/zen-live-folders.ftl"/>
<link rel="localization" href="browser/zen-space-routing.ftl"/>
<link rel="localization" href="browser/zen-share.ftl"/>
<link rel="localization" href="browser/zen-library.ftl"/>
</linkset>

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/content/UrlbarInputBase.mjs b/browser/components/urlbar/content/UrlbarInputBase.mjs
index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc4292d4e6 100644
index 8be35b58c5369e123ca69426e852e3849bf75513..9357aaadee0f8d2dab170e2d5e0b57282d909bf5 100644
--- a/browser/components/urlbar/content/UrlbarInputBase.mjs
+++ b/browser/components/urlbar/content/UrlbarInputBase.mjs
@@ -83,6 +83,12 @@ if (lazy) {
@@ -126,8 +126,11 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
startLayoutExtend() {
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
// Do not expand if the Urlbar does not support being expanded or it is
@@ -3195,6 +3266,13 @@ ${
@@ -3193,8 +3264,16 @@ ${
}
this.toggleAttribute("breakout-extend", true);
+ this.#showBreakoutPopover();
this.#updateTextboxPosition();
+ this.window.gZenUIManager.onUrlbarOpen();
@@ -140,7 +143,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
// Enable the animation only after the first extend call to ensure it
// doesn't run when opening a new window.
if (!this.hasAttribute("breakout-extend-animate")) {
@@ -3218,6 +3296,29 @@ ${
@@ -3218,7 +3297,33 @@ ${
return;
}
@@ -168,9 +171,13 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
+ this.removeAttribute("zen-floating-urlbar");
+
this.toggleAttribute("breakout-extend", false);
+ if (this.#isAddressbar) {
+ this.#hideBreakoutPopover();
+ }
this.#updateTextboxPosition();
}
@@ -3256,7 +3357,7 @@ ${
@@ -3256,7 +3361,7 @@ ${
forceUnifiedSearchButtonAvailable = false
) {
let prevState = this.getAttribute("pageproxystate");
@@ -179,7 +186,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
this.setAttribute("pageproxystate", state);
this._inputContainer.setAttribute("pageproxystate", state);
this._identityBox?.setAttribute("pageproxystate", state);
@@ -3533,7 +3634,11 @@ ${
@@ -3533,7 +3638,11 @@ ${
return;
}
@@ -192,7 +199,38 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
}
#updateTextboxPositionNextFrame() {
@@ -3591,9 +3696,10 @@ ${
@@ -3553,12 +3662,29 @@ ${
this.removeAttribute("breakout");
this.parentNode.removeAttribute("breakout");
this.style.top = "";
+ this.#hideBreakoutPopover();
+ this._layoutBreakoutUpdateKey = {};
+ }
+
+ #showBreakoutPopover() {
+ if (this.#isAddressbar) {
+ this.setAttribute("popover", "manual");
+ }
+ if (!this.matches(":popover-open")) {
+ this.showPopover();
+ }
+ this.#fixAddressbarSearchbarOrder();
+ }
+
+ #hideBreakoutPopover() {
try {
this.hidePopover();
} catch (ex) {
// No big deal if not a popover already.
}
- this._layoutBreakoutUpdateKey = {};
+ if (this.#isAddressbar) {
+ this.removeAttribute("popover");
+ }
}
incrementBreakoutBlockerCount() {
@@ -3591,9 +3717,10 @@ ${
return;
}
@@ -204,7 +242,19 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
);
if (this.#breakoutBlockerCount) {
@@ -4108,6 +4214,7 @@ ${
@@ -3602,8 +3729,9 @@ ${
this.setAttribute("breakout", "true");
this.parentNode.setAttribute("breakout", "true");
- this.showPopover();
- this.#fixAddressbarSearchbarOrder();
+ if (!this.#isAddressbar || this.hasAttribute("breakout-extend")) {
+ this.#showBreakoutPopover();
+ }
this.#updateTextboxPosition();
resolve();
@@ -4108,6 +4236,7 @@ ${
}
_toggleActionOverride(event) {
@@ -212,7 +262,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
if (
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
event.keyCode == KeyEvent.DOM_VK_ALT ||
@@ -4206,9 +4313,10 @@ ${
@@ -4206,9 +4335,10 @@ ${
if (!this.#isAddressbar) {
return val;
}
@@ -226,7 +276,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
// Only trim value if the directionality doesn't change to RTL and we're not
// showing a strikeout https protocol.
return UrlbarContentUtils.isTextDirectionRTL(trimmedValue, window) ||
@@ -4410,6 +4518,11 @@ ${
@@ -4410,6 +4540,11 @@ ${
keepViewOpen = false,
browserId = null,
}) {
@@ -238,7 +288,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
let keyDownEnterDeferred;
if (
this._keyDownEnterDeferred &&
@@ -4731,6 +4844,7 @@ ${
@@ -4731,6 +4866,7 @@ ${
this.setResultForCurrentValue(null);
this.handleCommand();
this.controller.clearLastQueryContextCache();
@@ -246,7 +296,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
this._suppressStartQuery = false;
});
@@ -4742,7 +4856,6 @@ ${
@@ -4742,7 +4878,6 @@ ${
// Close the results pane, because paste and go doesn't want a result
// selection. This has to happen before the menu opens: ending
// breakout-extend once it's open keeps it from showing (bug 2037468).
@@ -254,7 +304,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
let controller =
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
@@ -5025,7 +5138,11 @@ ${
@@ -5025,7 +5160,11 @@ ${
if (!engineName && !source && !this.hasAttribute("searchmode")) {
return;
}
@@ -267,7 +317,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
if (this._searchModeIndicatorTitle) {
this._searchModeIndicatorTitle.textContent = "";
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
@@ -5335,6 +5452,7 @@ ${
@@ -5335,6 +5474,7 @@ ${
this.document.l10n.setAttributes(
this.inputField,
@@ -275,7 +325,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
l10nId,
l10nId == "urlbar-placeholder-with-name"
? { name: engineName }
@@ -5385,6 +5503,12 @@ ${
@@ -5385,6 +5525,12 @@ ${
}
logger().debug("Blur Event");
@@ -288,7 +338,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
// We cannot count every blur events after a missed engagement as abandoment
// because the user may have clicked on some view element that executes
// a command causing a focus change. For example opening preferences from
@@ -5464,6 +5588,11 @@ ${
@@ -5464,6 +5610,11 @@ ${
}
_on_click(event) {
@@ -300,7 +350,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
switch (event.target) {
case this.inputField:
case this._inputContainer:
@@ -5558,10 +5687,11 @@ ${
@@ -5558,10 +5709,11 @@ ${
}
if (untrim) {
this.setValue(this._untrimmedValue);
@@ -313,7 +363,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
this.view.autoOpen({ event });
} else {
if (this._untrimOnFocusAfterKeydown) {
@@ -5605,9 +5735,16 @@ ${
@@ -5605,9 +5757,16 @@ ${
}
_on_mousedown(event) {
@@ -331,7 +381,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
if (
event.composedTarget != this.inputField &&
event.composedTarget != this._inputContainer
@@ -5617,6 +5754,10 @@ ${
@@ -5617,6 +5776,10 @@ ${
this.focusedViaMousedown = !this.focused;
this.#preventClickSelectsAll = this.focused;
@@ -342,7 +392,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
// Keep the focus status, since the attribute may be changed
// upon calling this.focus().
@@ -5654,7 +5795,7 @@ ${
@@ -5654,7 +5817,7 @@ ${
// view open on tab switch, and the TabSelect event arrived earlier.
// Also ignore mousedown on the urlbarView context menu: opening/closing the
// view is already handled by the result opening flow.
@@ -351,7 +401,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
break;
}
@@ -5948,7 +6089,7 @@ ${
@@ -5948,7 +6111,7 @@ ${
// When we are in actions search mode we can show more results so
// increase the limit.
let maxResults =

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,294 @@
#filter dumbComments emptyLines substitution
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="1008" height="28" fill="none">
<svg x="0" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0)"/>
</svg>
<svg x="28" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.131) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45.355 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.296)"/>
</svg>
<svg x="56" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.473) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 46.299 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 1.082)"/>
</svg>
<svg x="84" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.955) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 47.669 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 2.224)"/>
</svg>
<svg x="112" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 1.517) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 49.32 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 3.6)"/>
</svg>
<svg x="140" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 2.103) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 51.12 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 5.1)"/>
</svg>
<svg x="168" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 2.656) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 52.937 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 6.614)"/>
</svg>
<svg x="196" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.119) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 54.637 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 8.031)"/>
</svg>
<svg x="224" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.425) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 56.076 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 9.23)"/>
</svg>
<svg x="252" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.496) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 57.089 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 10.075)"/>
</svg>
<svg x="280" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.233) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 57.481 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 10.4)"/>
</svg>
<svg x="308" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 2.501) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 57.001 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 10)"/>
</svg>
<svg x="336" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.423) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 56.035 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 8.325)"/>
</svg>
<svg x="364" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -2.757) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 54.37 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 5.434)"/>
</svg>
<svg x="392" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -5.733) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 51.937 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 1.211)"/>
</svg>
<svg x="420" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -8.006) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 49.263 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -3.429)"/>
</svg>
<svg x="448" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -9.597) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 47.182 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -7.043)"/>
</svg>
<svg x="476" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -10.631) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45.895 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -9.277)"/>
</svg>
<svg x="504" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.223) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45.228 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.434)"/>
</svg>
<svg x="532" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.46) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.83)"/>
</svg>
<svg x="560" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.413) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.754)"/>
</svg>
<svg x="588" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.089) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.398)"/>
</svg>
<svg x="616" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -10.494) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -9.781)"/>
</svg>
<svg x="644" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -9.636) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -8.922)"/>
</svg>
<svg x="672" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -8.527) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -7.844)"/>
</svg>
<svg x="700" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -7.186) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -6.57)"/>
</svg>
<svg x="728" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -5.639) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -5.133)"/>
</svg>
<svg x="756" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -3.928) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -3.575)"/>
</svg>
<svg x="784" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -2.132) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -1.967)"/>
</svg>
<svg x="812" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -0.407) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -0.441)"/>
</svg>
<svg x="840" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.851) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.661)"/>
</svg>
<svg x="868" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.99) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.846)"/>
</svg>
<svg x="896" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.823) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.718)"/>
</svg>
<svg x="924" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.566) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.496)"/>
</svg>
<svg x="952" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.271) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.236)"/>
</svg>
<svg x="980" width="28" height="28" viewBox="0 0 128 128">
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="context-fill" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0)"/>
</svg>
</svg>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
#filter dumbComments emptyLines substitution
# 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/.
<svg class="zen-library-icon" width="18" height="18" viewBox="4 4 18 18" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg">
<style>
@property --zen-triangle-anim { syntax: '&lt;number&gt;'; initial-value: 1; inherits: false; }
@property --zen-circle-anim { syntax: '&lt;number&gt;'; initial-value: 1; inherits: false; }
@keyframes StarBackward {
0% { transform: rotate(120deg); }
100% { transform: rotate(0deg); }
}
@keyframes TriangleBackward {
0% { --zen-triangle-anim: 1; }
100% { --zen-triangle-anim: 0; }
}
@keyframes CircleBackward {
0% { --zen-circle-anim: 1; }
100% { --zen-circle-anim: 0; }
}
@keyframes GlowBackward {
0% { opacity: 0.4; }
100% { opacity: 0; }
}
@keyframes GlowElementsBackward {
0% { opacity: 0.5; }
100% { opacity: 0; }
}
.zen-library-star { transform-origin: 10px 10.2px; animation: StarBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.zen-library-triangle-group { transform-origin: 17px 9.2px; animation: TriangleBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; transform: translate(calc(var(--zen-triangle-anim) * 2px), 0px) rotate(calc(var(--zen-triangle-anim) * 365deg)) scale(max(1 - var(--zen-triangle-anim) / 0.65, (var(--zen-triangle-anim) - 0.65) / 0.35)); }
.zen-library-circle { transform-origin: 0px 0px; animation: CircleBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; transform: translate(calc(var(--zen-circle-anim) * 1px), calc(var(--zen-circle-anim) * -1.5px)) scale(max(0, max((var(--zen-circle-anim) - 0.75) / 0.25, min(1, 1 - (var(--zen-circle-anim) - 0.3) / 0.35)))); }
.zen-library-glow { filter: blur(2px); animation: GlowBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.zen-library-glow-element { filter: blur(2px); animation: GlowElementsBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.zen-library-glow-bg, .zen-library-glow-element { fill: transparent !important; stroke: none !important; }
.zen-library-glow-rays { stroke: transparent !important; fill: none !important; }
@media (prefers-color-scheme: dark) {
.zen-library-glow-bg, .zen-library-glow-element { fill: white !important; }
.zen-library-glow-rays { stroke: white !important; }
}
</style>
<defs>
<mask id="zen-library-box-mask">
<rect x="-10" y="-10" width="40" height="40" fill="white"/>
<rect x="-10" y="11.635" width="40" height="40" fill="black"/>
</mask>
</defs>
<g fill-rule="evenodd">
<g mask="url(#zen-library-box-mask)">
<path class="zen-library-star zen-library-glow-element" d="M 7.773 11.635 C 8.136 11.863 8.24 12.075 8.218 12.294 L 8.036 14.082 C 7.984 14.598 8.584 14.917 8.982 14.585 L 10.362 13.432 C 10.53 13.291 10.764 13.258 10.966 13.347 L 12.61 14.072 C 13.085 14.281 13.574 13.808 13.38 13.327 L 12.701 11.635 C 12.632 11.437 12.675 11.216 12.816 11.058 L 14.014 9.718 C 14.36 9.331 14.061 8.72 13.544 8.756 L 11.75 8.877 C 11.531 8.892 11.322 8.781 11.211 8.591 L 10.307 7.038 C 10.046 6.59 9.373 6.685 9.247 7.188 L 8.808 8.931 C 8.754 9.144 8.585 9.309 8.37 9.355 L 6.613 9.735 C 6.106 9.845 5.988 10.514 6.427 10.79 L 7.773 11.635 Z"/>
<path class="zen-library-star" d="M 7.773 11.635 C 8.136 11.863 8.24 12.075 8.218 12.294 L 8.036 14.082 C 7.984 14.598 8.584 14.917 8.982 14.585 L 10.362 13.432 C 10.53 13.291 10.764 13.258 10.966 13.347 L 12.61 14.072 C 13.085 14.281 13.574 13.808 13.38 13.327 L 12.701 11.635 C 12.632 11.437 12.675 11.216 12.816 11.058 L 14.014 9.718 C 14.36 9.331 14.061 8.72 13.544 8.756 L 11.75 8.877 C 11.531 8.892 11.322 8.781 11.211 8.591 L 10.307 7.038 C 10.046 6.59 9.373 6.685 9.247 7.188 L 8.808 8.931 C 8.754 9.144 8.585 9.309 8.37 9.355 L 6.613 9.735 C 6.106 9.845 5.988 10.514 6.427 10.79 L 7.773 11.635 Z"/>
</g>
<g transform="translate(14, 6.2)">
<circle class="zen-library-circle zen-library-glow-element" cx="0" cy="0" r="1.4"/>
<circle class="zen-library-circle" cx="0" cy="0" r="1"/>
</g>
<g class="zen-library-triangle-group">
<g transform="translate(17, 9.2)">
<path class="zen-library-triangle zen-library-glow-element" d="M -1.1 1.8 C -1.4 2.0, -1.7 1.8, -1.7 1.5 L -1.7 -1.5 C -1.7 -1.8, -1.4 -2.0, -1.1 -1.8 L 1.4 -0.4 C 1.7 -0.2, 1.7 0.2, 1.4 0.4 Z"/>
<path class="zen-library-triangle" d="M -1.1 1.8 C -1.4 2.0, -1.7 1.8, -1.7 1.5 L -1.7 -1.5 C -1.7 -1.8, -1.4 -2.0, -1.1 -1.8 L 1.4 -0.4 C 1.7 -0.2, 1.7 0.2, 1.4 0.4 Z"/>
</g>
</g>
<g class="zen-library-glow">
<path class="zen-library-glow-bg" d="M 6.6 6.6 L 8.04 12.7 L 17.96 12.7 L 19.4 6.6 Z"/>
<path class="zen-library-glow-rays" d="M 6.1 6.6 L 8.04 12.7 L 17.96 12.7 L 19.9 6.6"/>
</g>
<path class="zen-library-box" d="M 7.218 13.287 C 7.218 12.831 7.588 12.461 8.044 12.461 H 17.957 C 18.414 12.461 18.783 12.831 18.783 13.287 V 17.418 C 18.783 19.243 17.304 20.722 15.479 20.722 H 10.523 C 8.698 20.722 7.218 19.243 7.218 17.418 V 13.287 Z M 10.609 14.526 C 10.609 14.07 10.979 13.7 11.435 13.7 H 14.739 C 15.196 13.7 15.565 14.07 15.565 14.526 C 15.565 14.982 15.196 15.352 14.739 15.352 H 11.435 C 10.979 15.352 10.609 14.982 10.609 14.526 Z"/>
</g>
</svg>

View File

@@ -543,4 +543,11 @@
* skin/classic/browser/zen-icons/selectable/warning.svg (../shared/zen-icons/common/selectable/warning.svg)
* skin/classic/browser/zen-icons/selectable/water.svg (../shared/zen-icons/common/selectable/water.svg)
* skin/classic/browser/zen-icons/selectable/weight.svg (../shared/zen-icons/common/selectable/weight.svg)
* skin/classic/browser/zen-icons/library/library-boosts-sprite.svg (../shared/zen-icons/common/library/library-boosts-sprite.svg)
* skin/classic/browser/zen-icons/library/library-downloads-sprite.svg (../shared/zen-icons/common/library/library-downloads-sprite.svg)
* skin/classic/browser/zen-icons/library/library-history-sprite.svg (../shared/zen-icons/common/library/library-history-sprite.svg)
* skin/classic/browser/zen-icons/library/library-media-sprite.svg (../shared/zen-icons/common/library/library-media-sprite.svg)
* skin/classic/browser/zen-icons/library/library-spaces-sprite.svg (../shared/zen-icons/common/library/library-spaces-sprite.svg)
* skin/classic/browser/zen-icons/library/library-sprite.svg (../shared/zen-icons/common/library/library-sprite.svg)
* skin/classic/browser/zen-icons/library/library.svg (../shared/zen-icons/common/library/library.svg)
skin/classic/browser/zen-icons/icons.css (../shared/zen-icons/icons.css)

View File

@@ -60,6 +60,13 @@ do_common_icons() {
echo "Working on $filename"
echo "* skin/classic/browser/zen-icons/selectable/$filename (../shared/zen-icons/common/selectable/$filename) " >> jar.inc.mn
done
for filename in common/library/*.svg; do
# remove the os/ prefix
add_header_to_file $filename
filename=$(basename $filename)
echo "Working on $filename"
echo "* skin/classic/browser/zen-icons/library/$filename (../shared/zen-icons/common/library/$filename) " >> jar.inc.mn
done
}
do_icons nucleo WIN # TODO: use windows icons

View File

@@ -69,7 +69,9 @@ export class nsZenBoostEditor {
* @returns {ZenBoostsParent} Boost JSActor parent
*/
get zenBoostsParent() {
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const linkedBrowser =
this.editorWindow.browser ??
this.openerWindow.gBrowser.selectedTab.linkedBrowser;
const actor =
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
return actor;

View File

@@ -606,9 +606,12 @@ class nsZenBoostsManager {
* @param {Window} parentWindow - The parent browser window
* @param {Boost} boost - The boost which will be edited
* @param {nsIURI} domainUri - The boost which will be edited
* @param {object} [options]
* @param {Element} [options.browser] - Browser showing the site when it is
* not the selected tab, e.g. a detached glance
* @returns {Window|null} The instanced editor window
*/
openBoostWindow(parentWindow, boost, domainUri) {
openBoostWindow(parentWindow, boost, domainUri, { browser = null } = {}) {
if (!this.canBoostSite(domainUri)) {
console.error(
"[ZenBoostsManager] Cannot open editor for boost with invalid domain."
@@ -648,38 +651,48 @@ class nsZenBoostsManager {
null
);
// Close the editor if the tab is switched
const onTabSelect = editor.close.bind(editor);
parentWindow.gBrowser.tabContainer.addEventListener(
"TabSelect",
onTabSelect,
{
once: true,
}
);
const progressListener = {
QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener"]),
onLocationChange: webProgress => {
if (webProgress.isTopLevel) {
editor.close();
}
},
};
const onTabSelect = editor.close.bind(editor);
if (browser) {
browser.addProgressListener(progressListener);
editor.addEventListener(
"unload",
() => {
browser.removeProgressListener(progressListener);
editor.browser = null;
},
{ once: true }
);
} else {
// Close the editor if the tab is switched or navigates.
parentWindow.gBrowser.tabContainer.addEventListener(
"TabSelect",
onTabSelect,
{ once: true }
);
parentWindow.gBrowser.addProgressListener(progressListener);
editor.addEventListener(
"unload",
() => {
parentWindow.gBrowser.tabContainer.removeEventListener(
"TabSelect",
onTabSelect
);
parentWindow.gBrowser.removeProgressListener(progressListener);
editor.browser = null;
},
{ once: true }
);
}
parentWindow.gBrowser.addProgressListener(progressListener);
editor.addEventListener(
"unload",
() => {
parentWindow.gBrowser.tabContainer.removeEventListener(
"TabSelect",
onTabSelect
);
parentWindow.gBrowser.removeProgressListener(progressListener);
},
{ once: true }
);
editor.browser = browser;
// Give the domain
editor.domain = domain;
editor.openerWindow = parentWindow;

View File

@@ -23,7 +23,7 @@
content/browser/zen-styles/zen-browser-ui.css (../../zen/common/styles/zen-browser-ui.css)
content/browser/zen-styles/zen-animations.css (../../zen/common/styles/zen-animations.css)
content/browser/zen-styles/zen-panel-ui.css (../../zen/common/styles/zen-panel-ui.css)
content/browser/zen-styles/zen-single-components.css (../../zen/common/styles/zen-single-components.css)
* content/browser/zen-styles/zen-single-components.css (../../zen/common/styles/zen-single-components.css)
content/browser/zen-styles/zen-sidebar.css (../../zen/common/styles/zen-sidebar.css)
* content/browser/zen-styles/zen-toolbar.css (../../zen/common/styles/zen-toolbar.css)
content/browser/zen-styles/zen-browser-container.css (../../zen/common/styles/zen-browser-container.css)

View File

@@ -4,6 +4,38 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@keyframes library-badge-download {
0% {
transform: translateY(0);
}
20% {
transform: translateY(12px);
}
100% {
transform: translateY(0);
}
}
@keyframes zen-library-sprite-hover {
from {
background-position-x: 0%;
}
to {
background-position-x: 100%;
}
}
@keyframes zen-library-sprite-unhover {
from {
background-position-x: 100%;
}
to {
background-position-x: 0%;
}
}
@keyframes zen-back-and-forth-text {
0%,
10% {

View File

@@ -152,7 +152,7 @@ body,
}
& > * {
z-index: 1;
z-index: var(--zen-main-app-child-z-index, 1);
}
/* stylelint-disable-next-line media-query-no-invalid */

View File

@@ -0,0 +1,297 @@
@property --zen-library-progress {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
#zen-sidebar-foot-buttons,
#tabbrowser-tabs {
--zen-library-open-duration: 0.3s;
--zen-library-open-easing: linear(0, 0.34 12%, 0.7 24%, 0.93 38%, 1.03 55%, 1);
transition: --zen-library-progress var(--zen-library-open-duration) var(--zen-library-open-easing);
&[zen-library-stack-open] {
--zen-library-progress: 1;
}
}
#zen-library-button {
position: relative;
--zen-library-sprite-size: 18px;
&::before {
content: "";
position: absolute;
width: var(--zen-library-sprite-size);
height: var(--zen-library-sprite-size);
background-image: url("chrome://browser/skin/zen-icons/library/library-sprite.svg");
}
@media (prefers-reduced-motion: no-preference) {
&::before {
animation: zen-library-sprite-unhover 0.3s steps(36, jump-none);
}
&:hover::before {
animation: zen-library-sprite-hover 0.35s steps(36, jump-none) forwards;
}
}
}
#zen-library-button > .toolbarbutton-badge-stack {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
background: none !important;
pointer-events: none;
&[downloading] {
animation: 0.4s library-badge-download ease;
}
/* The badge flies from its spot on the button to the newest entry's badge
* along an arc, the target being set as the list opens. */
& #library-button-badge {
position: absolute;
margin: 0 !important;
transform-origin: left top;
--zen-library-badge-x: 14px;
--zen-library-badge-y: -8px;
--zen-library-badge-to-x: var(--zen-library-badge-x);
--zen-library-badge-to-y: var(--zen-library-badge-y);
--zen-library-badge-mid-x: calc(
(var(--zen-library-badge-x) + var(--zen-library-badge-to-x)) / 2 +
abs(var(--zen-library-badge-to-y) - var(--zen-library-badge-y)) / 2
);
--zen-library-badge-mid-y: calc(
(var(--zen-library-badge-y) + var(--zen-library-badge-to-y)) / 2 -
abs(var(--zen-library-badge-to-x) - var(--zen-library-badge-x)) / 2
);
--zen-library-left: calc(1 - var(--zen-library-progress));
left: calc(
var(--zen-library-left) * var(--zen-library-left) * var(--zen-library-badge-x) +
2 * var(--zen-library-left) * var(--zen-library-progress) * var(--zen-library-badge-mid-x) +
var(--zen-library-progress) * var(--zen-library-progress) * var(--zen-library-badge-to-x)
);
top: calc(
var(--zen-library-left) * var(--zen-library-left) * var(--zen-library-badge-y) +
2 * var(--zen-library-left) * var(--zen-library-progress) * var(--zen-library-badge-mid-y) +
var(--zen-library-progress) * var(--zen-library-progress) * var(--zen-library-badge-to-y)
);
&:not([downloading]) {
transform: scale(calc((var(--zen-library-progress) * 0.5 + 0.5) * 100%));
}
&[downloading] {
transform: scale(calc((var(--zen-library-progress) * 0.2 + 0.8) * 100%));
}
}
}
#library-button-badge,
#zen-sidebar-foot-buttons[zen-library-badge] #zen-library-download-list > :last-child .zen-library-download-badge {
visibility: hidden;
}
#zen-sidebar-foot-buttons[zen-library-badge] {
& #library-button-badge {
visibility: visible;
}
&[zen-library-stack-open] {
& #library-button-badge {
visibility: hidden;
transition: visibility 0s var(--zen-library-open-duration);
}
& #zen-library-download-list > :last-child .zen-library-download-badge {
visibility: visible;
transition: visibility 0s var(--zen-library-open-duration);
}
}
}
.zen-library-download-badge {
pointer-events: none;
}
.zen-library-download-badge:not([downloading]) {
background-image: var(--download-image);
width: 20px;
height: 20px;
border-radius: 8px;
background-size: cover;
scale: 2;
& .zen-library-download-progress {
display: none !important;
}
}
.zen-library-download-badge[downloading] {
will-change: transform;
align-self: start;
transform: translateX(-2px);
--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);
box-shadow: 0 0 6px 2px rgba(0,0,0,.1);
color: var(--border-color);
width: 25px;
height: 25px;
border-radius: 100%;
position: relative;
}
.zen-library-download-badge[downloading]::after {
background-image: url("chrome://browser/skin/zen-icons/forward.svg");
rotate: 90deg;
position: absolute;
content: '';
background-size: contain;
inset: 2px;
fill: currentColor;
-moz-context-properties: fill;
}
.zen-library-download-progress {
position: absolute;
inset: 1px;
border-radius: 50%;
--badge-size: 25px;
--border-width: 4px;
--size: calc(var(--badge-size) + 2 * var(--border-width));
margin: calc(-1 * var(--border-width));
--thickness: 3px;
--progress: var(--zen-accent-button-background);
background: conic-gradient(
from -0deg,
var(--progress) calc(var(--value) * 1%),
transparent 0
);
mask: radial-gradient(
circle closest-side,
transparent calc(100% - var(--thickness)),
black calc(100% - var(--thickness))
);
}
#zen-library-download-list {
width: 100%;
height: auto;
position: absolute;
bottom: 100%;
visibility: hidden;
transition: visibility 0s var(--zen-library-open-duration);
#zen-sidebar-foot-buttons[zen-library-stack-open] & {
visibility: visible;
transition: none;
}
display: flex;
flex-direction: column;
padding: 0;
padding-top: 10px;
gap: 4px;
.zen-library-download-list-download {
container-type: inline-size;
-moz-window-dragging: no-drag;
height: 52px;
border-radius: 16px;
display: flex;
position: relative;
padding: 0 4px;
--zen-library-progress: 0;
--zen-library-entry-index: 0;
transition: --zen-library-progress 0.22s var(--zen-library-open-easing)
calc(var(--zen-library-entry-index) * 12ms);
&:nth-last-child(2) {
--zen-library-entry-index: 1;
}
&:nth-last-child(3) {
--zen-library-entry-index: 2;
}
&:nth-last-child(4) {
--zen-library-entry-index: 3;
}
#zen-sidebar-foot-buttons[zen-library-stack-open] & {
--zen-library-progress: 1;
}
transform: translateY(calc((1 - var(--zen-library-progress)) * 25px));
opacity: var(--zen-library-progress);
&:hover {
background: var(--zen-toolbar-element-bg);
}
& .zen-library-download-list-title-container {
@container (max-width: 50px) {
display: none !important;
}
margin: auto 0;
min-width: 0;
flex: 1;
& .zen-library-download-list-title {
font-size: 11pt;
opacity: 0.9;
text-overflow: ellipsis;
display: block;
overflow: hidden;
white-space: nowrap;
}
& .zen-library-download-list-subtitle {
font-size: 9pt;
opacity: 0.6;
text-overflow: ellipsis;
display: block;
overflow: hidden;
white-space: nowrap;
}
}
& .zen-library-download-badge {
margin: auto 20px auto 8px;
@container (max-width: 50px) {
margin: auto;
}
}
& .zen-library-download-action {
display: none;
margin: auto 4px auto 0;
@container (max-width: 50px) {
display: none !important;
}
}
&[downloading] .zen-library-download-action {
display: flex;
list-style-image: url("chrome://browser/skin/zen-icons/close.svg");
}
}
}

View File

@@ -800,3 +800,15 @@
display: none;
}
}
#tabbrowser-tabs:is([zen-library-stack-open], [zen-library-stack-closing]) {
--gradient-pos: calc(100% - var(--zen-library-stack-height, 0px));
mask-image: linear-gradient(
to bottom,
black
calc(var(--gradient-pos) - 60px),
color-mix(transparent calc(var(--zen-library-progress) * 100%), black)
var(--gradient-pos));
}
%include zen-library-widget.css

View File

@@ -4,17 +4,25 @@
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenLibraryWidget: "moz-src:///zen/library/ZenLibraryWidget.sys.mjs",
});
export const ZenCustomizableUI = new (class {
constructor() {}
TYPE_TOOLBAR = "toolbar";
defaultSidebarIcons = [
"downloads-button",
Services.prefs.getBoolPref("zen.library.enabled")
? "zen-library-button"
: "downloads-button",
"zen-workspaces-button",
"zen-create-new-button",
];
startup(CustomizableUIInternal) {
CustomizableUIInternal.createBuiltinWidget(lazy.ZenLibraryWidget);
CustomizableUIInternal.registerArea(
"zen-sidebar-top-buttons",
{

View File

@@ -166,6 +166,14 @@ document.addEventListener(
}
break;
}
case "cmd_zenToggleLibrary": {
const { ZenLibrary } = ChromeUtils.importESModule(
"moz-src:///zen/library/ZenLibrary.mjs",
{ global: "current" }
);
ZenLibrary.toggle();
break;
}
default:
gZenGlanceManager.handleMainCommandSet(event);
if (event.target.id.startsWith("cmd_zenWorkspaceSwitch")) {

View File

@@ -61,6 +61,12 @@ ChromeUtils.defineLazyGetter(lazy, "mainAppWrapper", () =>
document.getElementById("zen-main-app-wrapper")
);
ChromeUtils.defineESModuleGetters(
lazy,
{ ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs" },
{ global: "current" }
);
window.gZenCompactModeManager = {
_flashTimeouts: {},
_eventListeners: [],
@@ -169,6 +175,9 @@ window.gZenCompactModeManager = {
},
set preference(value) {
if (lazy.ZenLibrary.isLibrarySlightlyOpen) {
return;
}
if (!this.shouldBeCompact) {
value = false;
}

View File

@@ -148,9 +148,11 @@ class nsZenDownloadAnimationElement extends HTMLElement {
}
#determineEndPosition() {
const downloadsButton = document.getElementById("downloads-button");
const isDownloadButtonVisible =
downloadsButton && this.#isElementVisible(downloadsButton);
const downloadsButton = [
document.getElementById("zen-library-button"),
document.getElementById("downloads-button"),
].find(button => button && this.#isElementVisible(button));
const isDownloadButtonVisible = !!downloadsButton;
let endPosition = { clientX: 0, clientY: 0 };

View File

@@ -21,7 +21,7 @@
width: 50%;
height: 50%;
background-color: var(--zen-primary-color);
mask: url("chrome://browser/content/zen-images/downloads/archive.svg") no-repeat center;
mask: url("chrome://browser/skin/zen-icons/library/library.svg") no-repeat center;
mask-size: contain;
display: block;
}

View File

@@ -137,6 +137,7 @@
}
startTabDrag(event, tab, ...args) {
const options = args[0] ?? {};
this.ZenDragAndDropService.onDragStart(1);
this.#isOutOfWindow = false;
gZenCompactModeManager._isTabBeingDragged = true;
@@ -145,13 +146,19 @@
if (isTabGroupLabel(tab)) {
tab = tab.group;
}
const draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
const shown = options.dragImageSource ?? tab;
let draggingTabs;
if (options.dragImageSource) {
draggingTabs = [shown];
} else {
draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
}
const { offsetX, offsetY } = this.#getDragImageOffset(
event,
tab,
shown,
draggingTabs
);
const dragImage = this.#createDragImageForTabs(draggingTabs);
const dragImage = this._createDragImageForTabs(draggingTabs);
this.originalDragImageArgs = [dragImage, offsetX, offsetY];
this.#dragImageEssential = tab.hasAttribute("zen-essential");
dt.setDragImage(...this.originalDragImageArgs);
@@ -159,11 +166,13 @@
tab.style.visibility = "hidden";
}
this._dropLandingArmed =
!args[0]?.fromTabList && this.getDropEffectForTabDrag(event) == "move";
options.armLanding ??
(!args[0]?.fromTabList &&
this.getDropEffectForTabDrag(event) == "move");
this.ZenDragAndDropService.armDropLanding(this._dropLandingArmed);
}
#createDragImageForTabs(movingTabs) {
_createDragImageForTabs(movingTabs) {
const periphery = gZenWorkspaces.activeWorkspaceElement.querySelector(
"#tabbrowser-arrowscrollbox-periphery"
);
@@ -675,7 +684,7 @@
return;
}
this.#handle_sidebarDragOver(event);
this.#handle_tabDragOverToSplit(event);
this._handle_tabDragOverToSplit(event);
}
#shouldSwitchSpace(event) {
@@ -721,16 +730,30 @@
#onSpaceChanged(spaceChanged, dt) {
if (AppConstants.platform !== "macosx") {
// See the hack in #createDragImageForTabs for more details which
// See the hack in _createDragImageForTabs for more details which
// explains why we need to do this on non-macOS platforms.
return;
}
let tabs = this.originalDragImageArgs[0].children;
this._recolorDragImage(spaceChanged);
this._refreshDragImage(dt);
}
/**
* Recolours the current drag image's tab clones for a workspace's theme,
* so the image reads as the space it is over.
*
* @param {object} workspace
*/
_recolorDragImage(workspace) {
const wrapper = this.originalDragImageArgs?.[0];
if (!wrapper || !workspace) {
return;
}
const { isDarkMode, isExplicitMode } =
gZenThemePicker.getGradientForWorkspace(spaceChanged, {
gZenThemePicker.getGradientForWorkspace(workspace, {
getGradient: false,
});
for (let tab of tabs) {
for (const tab of wrapper.querySelectorAll("tab")) {
if (isExplicitMode) {
tab.style.colorScheme = isDarkMode ? "dark" : "light";
tab.style.setProperty(
@@ -742,9 +765,21 @@
tab.style.removeProperty("--tab-selected-textcolor");
}
}
}
/**
* Re-hands the drag image to the drag session once its new styles have
* been laid out.
*
* @param {DataTransfer} dt
*/
_refreshDragImage(dt) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
dt.updateDragImage(...this.originalDragImageArgs);
// The drag may have ended over the two frames, clearing the image.
if (this.originalDragImageArgs.length) {
dt.updateDragImage(...this.originalDragImageArgs);
}
});
});
}
@@ -802,7 +837,60 @@
});
}
#handle_tabDragOverToSplit(event) {
/**
* The element a drop on `element` acts on.
*
* @param {Element} element
* @returns {Element}
*/
_targetForDrop(element) {
return element;
}
/**
* Whether a drop lands before (vs after) an element, from how far down its
* box the pointer sits and the move-over threshold pref.
*
* @param {DragEvent} event
* @param {DOMRect} rect - The element's bounds
* @returns {boolean}
*/
_dropsBefore(event, rect) {
const threshold =
Services.prefs.getIntPref(
"browser.tabs.dragDrop.moveOverThresholdPercent"
) / 100;
return (event.clientY - rect.top) / rect.height <= threshold;
}
/**
* Lays the shared drop indicator across an element, horizontally.
*
* @param {object} options
* @param {Element} [options.parent] - Reparent the indicator here first
* @param {number} options.left - Inset from the left, in px
* @param {number} options.width - Indicator width, in px
* @param {number} options.top - Top offset, in px
* @returns {Element} The indicator
*/
_placeDropIndicator({ parent = null, left, width, top }) {
const indicator = gZenPinnedTabManager.dragIndicator;
if (parent && indicator.parentNode !== parent) {
parent.appendChild(indicator);
}
indicator.setAttribute("orientation", "horizontal");
indicator.style.setProperty("--indicator-left", `${left}px`);
indicator.style.setProperty("--indicator-width", `${width}px`);
indicator.style.top = `${Math.round(top)}px`;
indicator.style.removeProperty("left");
return indicator;
}
get _splitDropReady() {
return !!this.#dragOverSplit.canDrop;
}
_handle_tabDragOverToSplit(event) {
if (!this._dndSplitEnabled) {
return;
}
@@ -831,7 +919,7 @@
}
if (
movingTabsSet.has(dropElement) ||
movingTabsSet.has(this._targetForDrop(dropElement)) ||
!isTab(draggedTab) ||
draggedTab?.group?.hasAttribute("split-view-group") ||
draggedTab.hasAttribute("zen-live-folder-item-id") ||
@@ -1049,13 +1137,13 @@
const placesBefore = new Map(
this.#draggedElements(event).map(element => [
element,
this.#placeOf(element),
this._placeOf(element),
])
);
super.handle_drop(event);
this.#maybeClearVerticalPinnedGridDragOver();
this.#handle_dropSwitchSpace(event);
this.#handle_dropCreateSplit(event);
this._handle_dropCreateSplit(event);
this._clearDragOverSplit();
if (!toSplit) {
this.#landDragImage(event, placesBefore);
@@ -1075,7 +1163,7 @@
return (this._dragImageTabs ?? [draggedTab]).map(elementToMove);
}
#placeOf(element) {
_placeOf(element) {
return `${element.screenX},${element.screenY}`;
}
@@ -1094,8 +1182,20 @@
if (gReduceMotion) {
return;
}
this._landDragImageOnElements(this.#draggedElements(event), placesBefore);
}
/**
* Marks where the drag image lands, one item per element in order, and
* keeps those elements out of sight until the drag ends.
*
* @param {Element[]} elements - Where the dropped items land
* @param {Map<Element, string>} [placesBefore] - Where each element was
* before the drop; one still there stays in sight under the image
*/
_landDragImageOnElements(elements, placesBefore = null) {
const landing = [];
for (const element of this.#draggedElements(event)) {
for (const element of elements) {
const { width, height } = element.getBoundingClientRect();
this.ZenDragAndDropService.addDropLandingRect(
Math.round(element.screenX),
@@ -1103,7 +1203,7 @@
Math.round(width),
Math.round(height)
);
if (placesBefore.get(element) !== this.#placeOf(element)) {
if (placesBefore?.get(element) !== this._placeOf(element)) {
element.style.visibility = "hidden";
landing.push(element);
}
@@ -1137,9 +1237,16 @@
gZenWorkspaces.updateTabsContainers();
}
#handle_dropCreateSplit(event) {
/**
* @param {DragEvent} event
* @param {object} [options]
* @param {boolean} [options.activate] - Whether to select the created split
* @returns {boolean} Whether the drop split the dragged tab with the tab
* it was held over
*/
_handle_dropCreateSplit(event, { activate = true } = {}) {
if (!this.#dragOverSplit.canDrop) {
return;
return false;
}
const dragData = this.#dragOverSplit.data;
@@ -1147,11 +1254,11 @@
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
if (!dragData || !draggedTab) {
return;
return false;
}
this._dontAnimateTabMove = true;
const droppedOnTab = dragData.dropElement;
const droppedOnTab = this._targetForDrop(dragData.dropElement);
const dropSide = dragData.dropSide;
// Clear any visuals and timer
@@ -1161,8 +1268,10 @@
gZenViewSplitter.splitTabs(
isLeft ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab],
"vsep",
isLeft ? 0 : 1
isLeft ? 0 : 1,
{ activate }
);
return true;
}
// eslint-disable-next-line complexity
@@ -1550,33 +1659,17 @@
this.#dragShiftableItems.at(-1)
);
}
const indicator = gZenPinnedTabManager.dragIndicator;
let top = 0;
threshold =
Services.prefs.getIntPref(
"browser.tabs.dragDrop.moveOverThresholdPercent"
) / 100;
if (overlapPercent > threshold || showIndicatorUnderNewTabButton) {
top = Math.round(rect.top + rect.height) + "px";
dropBefore = false;
} else {
top = Math.round(rect.top) + "px";
dropBefore = true;
}
if (indicator.style.top !== top) {
dropBefore =
!showIndicatorUnderNewTabButton && this._dropsBefore(event, rect);
const top = Math.round(dropBefore ? rect.top : rect.top + rect.height);
if (gZenPinnedTabManager.dragIndicator.style.top !== `${top}px`) {
shouldPlayHapticFeedback = true;
}
indicator.setAttribute("orientation", "horizontal");
indicator.style.setProperty(
"--indicator-left",
rect.left + separation / 2 + "px"
);
indicator.style.setProperty(
"--indicator-width",
rect.width - separation + "px"
);
indicator.style.top = top;
indicator.style.removeProperty("left");
this._placeDropIndicator({
left: rect.left + separation / 2,
width: rect.width - separation,
top,
});
this.#removeDragOverBackground();
if (!isTab(dropElement) && dropElement?.parentElement?.isZenFolder) {
dropElement = dropElement.parentElement;

View File

@@ -63,6 +63,153 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
.addEventListener("click", this.onOverlayClick.bind(this));
}
#detached = null;
get detachedBrowser() {
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>
</html:div>
`).firstElementChild;
host
.querySelector(".zen-glance-detached-backdrop")
.addEventListener("click", () => this.closeDetachedGlance());
document.getElementById("zen-main-app-wrapper").appendChild(host);
}
return host;
}
/**
* Opens a detached glance.
*
* @param {object} data - The URL, its triggering principal, the point the
* glance animates out of as clientX/clientY in window coordinates, and
* optionally the userContextId to load in
* @returns {Promise<{browser: Element, closed: Promise<string>}|null>}
* Once open, the glance's browser and a promise for its closing, which
*/
async openDetachedGlance(data) {
if (this.#detached || !this.#isGlanceLoadAllowed(data)) {
return null;
}
const host = this.#detachedHost;
const wrapper = host.querySelector(".browserContainer");
const browser = document.createXULElement("browser");
browser.setAttribute("type", "content");
browser.setAttribute("remote", "true");
browser.setAttribute("maychangeremoteness", "true");
browser.setAttribute("disableglobalhistory", "true");
browser.setAttribute("messagemanagergroup", "browsers");
browser.setAttribute("nodefaultsrc", "true");
if (data.userContextId) {
browser.setAttribute("usercontextid", data.userContextId);
}
host.querySelector(".browserStack").appendChild(browser);
wrapper.appendChild(this.#createNewOverlayButtons());
for (const selector of [
".zen-glance-sidebar-open",
".zen-glance-sidebar-split",
]) {
wrapper.querySelector(selector)?.remove();
}
host.hidden = false;
const hostRect = host.getBoundingClientRect();
const startPoint = {
clientX: data.clientX - hostRect.left,
clientY: data.clientY - hostRect.top,
width: 0,
height: 0,
};
let resolveClosed;
const whenClosed = new Promise(resolve => {
resolveClosed = resolve;
});
this.#detached = {
host,
wrapper,
browser,
startPoint,
closing: false,
resolveClosed,
};
browser.loadURI(Services.io.newURI(data.url), {
triggeringPrincipal: data.triggeringPrincipal,
});
wrapper.setAttribute("animate", true);
await gZenUIManager.elementAnimate(
wrapper,
this.#createGlanceArcSequence(startPoint, "opening", null, hostRect),
{
duration: gZenUIManager.testingEnabled
? 0
: this.#GLANCE_ANIMATION_DURATION,
easing: "ease-in-out",
}
);
wrapper.removeAttribute("animate");
wrapper.setAttribute("has-finished-animation", true);
return { browser, closed: whenClosed };
}
/**
* @param {object} [options]
* @param {boolean} [options.skipPermitUnload] - Close even if the page
* would rather not unload
*/
async closeDetachedGlance({ skipPermitUnload = false } = {}) {
const detached = this.#detached;
if (!detached || detached.closing) {
return;
}
if (!skipPermitUnload && !detached.browser.permitUnload().permitUnload) {
return;
}
detached.closing = true;
const { host, wrapper, browser } = detached;
detached.resolveClosed();
this.#animateSidebarButtons(
wrapper.querySelector(".zen-glance-sidebar-container")
);
host.setAttribute("fade-out", true);
wrapper.removeAttribute("has-finished-animation");
wrapper.setAttribute("animate", true);
try {
await gZenUIManager.elementAnimate(
wrapper,
this.#createGlanceArcSequence(
detached.startPoint,
"closing",
null,
host.getBoundingClientRect()
),
{
duration: gZenUIManager.testingEnabled
? 0
: this.#GLANCE_ANIMATION_DURATION,
easing: "ease-out",
}
);
} finally {
browser.destroy();
browser.remove();
wrapper.removeAttribute("animate");
host.removeAttribute("fade-out");
host.hidden = true;
this.#detached = null;
}
}
#setupPreferences() {
XPCOMUtils.defineLazyPreferenceGetter(
this._lazyPref,
@@ -102,7 +249,10 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
handleMainCommandSet(event) {
const command = event.target;
const commandHandlers = {
cmd_zenGlanceClose: () => this.closeGlance({ onTabClose: true }),
cmd_zenGlanceClose: () =>
this.#detached
? this.closeDetachedGlance()
: this.closeGlance({ onTabClose: true }),
cmd_zenGlanceExpand: () => this.fullyOpenGlance(),
cmd_zenGlanceSplit: () => this.splitGlance(),
};
@@ -617,9 +767,16 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
* @param {object} data - Glance data with position and dimensions
* @param {string} direction - 'opening' or 'closing'
* @param {Element|null} imageDataElement - The image data element for preview (optional)
* @param {DOMRect|null} referenceRect - Bounds the glance opens within;
* defaults to the tab panels
* @returns {object} Animation sequence object
*/
#createGlanceArcSequence(data, direction, imageDataElement = null) {
#createGlanceArcSequence(
data,
direction,
imageDataElement = null,
referenceRect = null
) {
let { clientX, clientY, width, height } = data;
if (imageDataElement?.parentElement) {
// Since we are animating scale transforms on the wrapper, we need to
@@ -649,9 +806,9 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
// Calculate start and end positions based on direction
let startPosition, endPosition;
const tabPanelsRect = window.windowUtils.getBoundsWithoutFlushing(
gBrowser.tabpanels
);
const tabPanelsRect =
referenceRect ??
window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
const widthPercent = 0.8;
if (direction === "opening") {

View File

@@ -103,7 +103,8 @@
}
.browserSidebarContainer.zen-glance-background,
.browserSidebarContainer.zen-glance-overlay .browserContainer:not([fade-out="true"]) {
:is(.browserSidebarContainer.zen-glance-overlay, #zen-glance-detached)
.browserContainer:not([fade-out="true"]) {
border-radius: var(--zen-native-inner-radius);
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
/* stylelint-disable-next-line property-no-unknown */
@@ -112,7 +113,7 @@
}
}
.browserSidebarContainer.zen-glance-overlay {
:is(.browserSidebarContainer.zen-glance-overlay, #zen-glance-detached) {
box-shadow: none !important;
visibility: inherit;
z-index: 1;
@@ -198,3 +199,19 @@
max-height: 100%;
}
}
#zen-main-app-wrapper > #zen-glance-detached {
position: fixed;
inset: 24px;
z-index: 100;
display: flex;
&[hidden] {
display: none;
}
& .zen-glance-detached-backdrop {
position: fixed;
inset: 0;
}
}

View File

@@ -0,0 +1,592 @@
/* 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",
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 = {
history: lazy.ZenLibraryHistorySection,
downloads: lazy.ZenLibraryDownloadsSection,
boosts: lazy.ZenLibraryBoostsSection,
spaces: lazy.ZenLibrarySpacesSection,
};
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);

View File

@@ -0,0 +1,347 @@
/* 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/. */
export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
#section;
/** @type {{element: Element, dropBefore: boolean, intoFolder: boolean}|null} */
#target = null;
#highlightedLabel = null;
// The space whose theme the drag image currently wears, so it is only
// restyled when the pointer crosses into a different card.
#dragImageSpace = null;
constructor(section) {
super(section);
this.#section = section;
}
_targetForDrop(element) {
return this.#section.realElementFor(element) ?? element;
}
/**
* @param {DragEvent} event
* @returns {Element|null} The dragged tab, or the label of the dragged
* split view, when it comes from this window
*/
#draggedItem(event) {
if (this.getDropEffectForTabDrag(event) !== "move") {
return null;
}
const item = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
if (!item || item.documentGlobal !== window) {
return null;
}
return gBrowser.isTab(item) || this.#isSplitViewLabel(item) ? item : null;
}
#isSplitViewLabel(item) {
return (
gBrowser.isTabGroupLabel(item) &&
!!item.group?.hasAttribute("split-view-group")
);
}
/**
* The tabs and split views a drag moves, from the drag data set on the
* dragged item.
*
* @param {Element} item - The dragged tab or split view label
* @returns {Element[]} Tabs and split view groups
*/
#movingElements(item) {
const moving = item._dragData?.movingTabs ?? [item];
return moving.map(element =>
gBrowser.isTabGroupLabel(element) ? element.group : element
);
}
/**
* @param {DragEvent} event
* @param {Element} copy - The copied tab being dragged
*/
startTabDrag(event, copy) {
let item = this._targetForDrop(copy);
if (!gBrowser.isTab(item)) {
return;
}
let shown = copy;
if (item.group?.hasAttribute("split-view-group")) {
item = item.group.labelElement;
shown = copy.group;
}
const strip = gBrowser.tabContainer.tabDragAndDrop;
strip.startTabDrag(event, item, {
fromTabList: true,
dragImageSource: shown,
armLanding: this.#landingSupported,
});
this.#dragImageSpace = copy.closest(".zen-library-space")?.dataset.uuid;
this.#styleDragImage(strip, this.#dragImageSpace);
}
#styleDragImage(strip, uuid) {
const workspace = uuid && gZenWorkspaces.getWorkspaceFromId(uuid);
if (!workspace) {
return;
}
for (const clone of strip.originalDragImageArgs?.[0]?.querySelectorAll(
"tab"
) ?? []) {
clone.toggleAttribute("visuallyselected", true);
clone.toggleAttribute("selected", true);
}
strip._recolorDragImage(workspace);
}
handle_dragover(event) {
const item = this.#draggedItem(event);
if (!item) {
return;
}
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "move";
this.#updateDragImageSpace(event);
this._handle_tabDragOverToSplit(event);
this.#updateDropTarget(event, item);
}
/**
* Recolours the drag image to match the card under the pointer, so it reads
* as the space it would drop into.
*
* @param {DragEvent} event
*/
#updateDragImageSpace(event) {
const uuid = event.target.closest(".zen-library-space")?.dataset.uuid;
if (!uuid || uuid === this.#dragImageSpace) {
return;
}
this.#dragImageSpace = uuid;
const strip = gBrowser.tabContainer.tabDragAndDrop;
this.#styleDragImage(strip, uuid);
strip._refreshDragImage(event.dataTransfer);
}
handle_dragleave(event) {
if (!event.currentTarget.contains(event.relatedTarget)) {
this.clearDragOverVisuals();
}
}
handle_drop(event) {
const item = this.#draggedItem(event);
if (!item) {
return;
}
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "move";
const target = this.#target;
const uuid = event.currentTarget.closest(".zen-library-space").dataset.uuid;
const moving = this.#movingElements(item);
const tabs = moving.flatMap(element =>
gBrowser.isTab(element) ? [element] : element.tabs
);
const fromElsewhere = tabs.filter(
tab => tab.getAttribute("zen-workspace-id") !== uuid
);
const strip = gBrowser.tabContainer.tabDragAndDrop;
const copyBefore = this.#section.copyForTab(tabs[0]);
const placeBefore = copyBefore && strip._placeOf(copyBefore);
if (fromElsewhere.length) {
gZenWorkspaces.moveTabsToWorkspace(fromElsewhere, uuid);
}
const split = this._handle_dropCreateSplit(event, { activate: false });
this.clearDragOverVisuals();
if (!split && target) {
const { element, dropBefore, intoFolder } = target;
if (intoFolder) {
element.addTabs(moving);
const firstExisting = element.tabs.find(tab => !tabs.includes(tab));
if (firstExisting) {
gBrowser.moveTabsBefore(moving, firstExisting);
}
} else {
const pinned = gBrowser.isTab(element)
? element.pinned
: (element.tabs?.[0]?.pinned ?? element.pinned);
for (const tab of tabs) {
if (pinned && !tab.pinned) {
gBrowser.pinTab(tab);
} else if (!pinned && tab.pinned) {
gBrowser.unpinTab(tab);
}
}
if (dropBefore) {
gBrowser.moveTabsBefore(moving, element);
} else {
gBrowser.moveTabsAfter(moving, element);
}
}
}
this.#land(tabs[0], placeBefore);
}
get #landingSupported() {
return AppConstants.platform === "macosx" && !gReduceMotion;
}
/**
* @param {Element} tab - The dropped tab
* @param {string|null} placeBefore - Where its copy was before the drop;
* a copy still there stays in sight under the image
*/
#land(tab, placeBefore) {
if (!this.#landingSupported) {
return;
}
const copy = this.#section.beginTabLanding(tab);
if (copy) {
gBrowser.tabContainer.tabDragAndDrop._landDragImageOnElements(
[copy],
new Map([[copy, placeBefore]])
);
}
}
clearDragOverVisuals(options) {
super.clearDragOverVisuals(options);
gZenFolders.highlightGroupOnDragOver(null);
this.#highlightedLabel?.removeAttribute("dragover");
this.#highlightedLabel = null;
this.#target = null;
}
/**
* @param {Element|null} strip - A card's tab strip
* @returns {Element|null} The last tab or folder label shown in it
*/
#lastRow(strip) {
if (!strip) {
return null;
}
const rows = strip.querySelectorAll(
"tab:not([zen-empty-tab]):not([zen-glance-tab]), .tab-group-label-container"
);
for (let i = rows.length - 1; i >= 0; i--) {
if (rows[i].getBoundingClientRect().height > 0) {
return rows[i];
}
}
return null;
}
/**
* Works out what a drop at the pointer would do and shows it on the copies.
*
* @param {DragEvent} event
* @param {Element} item - The dragged tab or split view label
*/
#updateDropTarget(event, item) {
if (this._splitDropReady) {
return;
}
const movingTabs = item._dragData?.movingTabs ?? [item];
const moving = this.#movingElements(item);
let copy = event.target.closest("tab, .tab-group-label-container");
if (!copy) {
copy = this.#lastRow(event.target.closest(".zen-library-space-tabs"));
}
if (copy?.hasAttribute("zen-glance-tab")) {
copy = copy.parentElement.closest("tab");
}
if (copy?.classList.contains("tab-group-label-container")) {
copy = copy.parentElement;
} else if (copy?.group?.hasAttribute("split-view-group")) {
copy = copy.group;
}
const element = copy && this._targetForDrop(copy);
if (!element) {
// A card with nothing in it takes the drop at its start.
const strip = event.target.closest(".zen-library-space-tabs");
if (strip && !this.#lastRow(strip)) {
this.#target = null;
gZenFolders.highlightGroupOnDragOver(null);
this.#placeIndicatorIn(strip, 0);
return;
}
this.clearDragOverVisuals();
return;
}
if (moving.some(moved => moved === element || moved.contains(element))) {
this.clearDragOverVisuals();
return;
}
const isFolder = !!element.isZenFolder;
const box = isFolder ? copy.labelContainerElement : copy;
const rect = box.getBoundingClientRect();
const overlap = (event.clientY - rect.top) / rect.height;
let dropBefore;
let intoFolder = false;
if (isFolder) {
const threshold =
Services.prefs.getIntPref(
"zen.tabs.folder-dragover-threshold-percent"
) / 100;
if (overlap < threshold) {
dropBefore = true;
} else if (
overlap > 1 - threshold &&
(copy.collapsed || element.childGroupsAndTabs.length < 2)
) {
dropBefore = false;
} else {
intoFolder = true;
}
} else {
dropBefore = this._dropsBefore(event, rect);
}
const previous = this.#target;
this.#target = { element, dropBefore, intoFolder };
const changed =
previous?.element !== element ||
previous.dropBefore !== dropBefore ||
previous.intoFolder !== intoFolder;
if (!changed) {
return;
}
if (previous) {
// eslint-disable-next-line mozilla/valid-services
Services.zen.playHapticFeedback();
}
this.#highlightedLabel?.removeAttribute("dragover");
this.#highlightedLabel = null;
if (intoFolder) {
gZenPinnedTabManager.removeTabContainersDragoverClass();
gZenFolders.highlightGroupOnDragOver(copy, movingTabs);
this.#highlightedLabel = box;
box.setAttribute("dragover", "true");
return;
}
gZenFolders.highlightGroupOnDragOver(null);
const strip = copy.closest(".zen-library-space-tabs");
this.#placeIndicatorIn(
strip,
(dropBefore ? rect.top : rect.bottom) - strip.getBoundingClientRect().top
);
}
/**
* @param {Element} strip - A card's tab strip
* @param {number} offset - Where in the strip's view the drop would go
*/
#placeIndicatorIn(strip, offset) {
const inset = 14;
this._placeDropIndicator({
parent: strip,
left: inset,
width: strip.getBoundingClientRect().width - 2 * inset,
top: offset + strip.scrollTop,
});
}
}

View File

@@ -0,0 +1,611 @@
/* 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.defineESModuleGetters(lazy, {
DownloadsCommon:
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
DownloadsViewUI:
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
const ENTRIES = 4;
const CLOSE_DELAY_MS = 200;
const FILE_MIME = "application/x-moz-file";
const BADGE_MARKUP = `
<span class="zen-library-download-badge no-squircles">
<span class="zen-library-download-progress no-squircles"></span>
</span>
`;
class ZenLibraryDownloadStack {
#button;
#badge;
#list;
#library = null;
#entries = [];
#data;
#downloads = [];
#inBatch = false;
#loaded = false;
#recentNewDownload = false;
#closeTimer = null;
#contextMenuOpen = false;
#secondsLeft = new WeakMap();
/**
* @param {Element} button - The library toolbar button
*/
constructor(button) {
this.#button = button;
button.setAttribute("command", "cmd_zenToggleLibrary");
button.classList.add("toolbarbutton-badge-stack-host");
button.appendChild(
this.#parse(
`<box class="toolbarbutton-badge-stack">${BADGE_MARKUP}</box>`
)
);
this.#badge = button.querySelector(".zen-library-download-badge");
this.#badge.id = "library-button-badge";
this.#list = this.#buildList();
button.addEventListener("mouseenter", this);
button.addEventListener("mouseleave", this);
this.#list.addEventListener("mouseenter", this);
this.#list.addEventListener("mouseleave", this);
this.#data = lazy.DownloadsCommon.getData(this.#window, true);
this.#data.addView(this);
}
destroy() {
this.#data.removeView(this);
this.#window.clearTimeout(this.#closeTimer);
this.#list.remove();
}
get #window() {
return this.#button.documentGlobal;
}
get #footButtons() {
return this.#list.parentElement;
}
/** The tab strip, once the window has it. */
get #tabs() {
return this.#window.gBrowser?.tabContainer ?? null;
}
#parse(markup) {
return this.#window.MozXULElement.parseXULToFragment(markup);
}
handleEvent(event) {
switch (event.type) {
case "mouseenter":
this.#window.clearTimeout(this.#closeTimer);
if (event.currentTarget === this.#button) {
this.#open();
}
break;
case "mouseleave":
this.#scheduleClose();
break;
}
}
#scheduleClose() {
this.#window.clearTimeout(this.#closeTimer);
this.#closeTimer = this.#window.setTimeout(() => {
if (!this.#contextMenuOpen) {
this.#close();
}
}, CLOSE_DELAY_MS);
}
#open() {
if (this.#footButtons.hasAttribute("zen-library-stack-open")) {
return;
}
this.#aimBadge();
this.#tabs?.removeAttribute("zen-library-stack-closing");
for (const host of [this.#footButtons, this.#tabs]) {
host?.setAttribute("zen-library-stack-open", "true");
}
}
#close() {
if (!this.#footButtons.hasAttribute("zen-library-stack-open")) {
return;
}
this.#recentNewDownload = false;
this.#updateBadgeShowing();
for (const host of [this.#footButtons, this.#tabs]) {
host?.removeAttribute("zen-library-stack-open");
}
// The strip's fade stays until its progress is back at zero.
const tabs = this.#tabs;
if (!tabs) {
return;
}
tabs.setAttribute("zen-library-stack-closing", "true");
tabs.addEventListener("transitionend", function onEnd(event) {
if (event.propertyName === "--zen-library-progress") {
tabs.removeEventListener("transitionend", onEnd);
tabs.removeAttribute("zen-library-stack-closing");
}
});
}
/**
* Points the button's badge at the newest entry's badge, where it flies
* to as the list opens, from wherever it is right now.
*/
#aimBadge() {
const entry = this.#entries.at(-1);
if (entry.hidden) {
return;
}
const target = entry.querySelector(".zen-library-download-badge");
const from = this.#badge.getBoundingClientRect();
const to = target.getBoundingClientRect();
const style = this.#window.getComputedStyle(this.#badge);
// The entry is still translated down while closed; land where it ends.
const entryTransform = this.#window.getComputedStyle(entry).transform;
const rise =
entryTransform === "none"
? 0
: new this.#window.DOMMatrixReadOnly(entryTransform).f;
this.#badge.style.setProperty(
"--zen-library-badge-to-x",
`${parseFloat(style.left) + to.left - from.left}px`
);
this.#badge.style.setProperty(
"--zen-library-badge-to-y",
`${parseFloat(style.top) + to.top - from.top - rise}px`
);
}
#buildList() {
const document = this.#window.document;
const footButtons = document.getElementById("zen-sidebar-foot-buttons");
// Not a widget of the toolbar, so its customization leaves it alone.
footButtons.appendChild(
this.#parse(
`<box id="zen-library-download-list" skipintoolbarset="true"></box>`
)
);
const list = document.getElementById("zen-library-download-list");
for (let i = 0; i < ENTRIES; i++) {
list.appendChild(
this.#parse(`
<div class="zen-library-download-list-download">
${BADGE_MARKUP}
<vbox class="zen-library-download-list-title-container">
<span class="zen-library-download-list-title"></span>
<span class="zen-library-download-list-subtitle"></span>
</vbox>
<toolbarbutton class="toolbarbutton-1 zen-library-download-action no-squircles" />
</div>
`)
);
}
this.#entries = [...list.children];
for (const entry of this.#entries) {
entry.setAttribute("draggable", "true");
entry.addEventListener("dragstart", event =>
this.#onDragStart(event, entry.download)
);
entry.addEventListener("click", event => {
if (event.button === 0) {
this.#openDownload(entry.download);
}
});
entry.addEventListener("contextmenu", event =>
this.#showContextMenu(event, entry.download)
);
const action = entry.querySelector(".zen-library-download-action");
action.addEventListener("click", event => event.stopPropagation());
action.addEventListener("command", () =>
this.#cancelDownload(entry.download)
);
}
return list;
}
/**
* Cancels a download under way, partial file and all.
*
* @param {Download} download
*/
#cancelDownload(download) {
if (!download || download.stopped) {
return;
}
download.cancel().catch(() => {});
download
.removePartialData()
.catch(console.error)
.finally(() => download.target.refresh());
}
#openDownload(download) {
if (download.succeeded) {
lazy.DownloadsCommon.openDownload(download).catch(console.error);
} else if (download.source?.url) {
this.#window.openTrustedLinkIn(download.source.url, "tab");
}
}
#onDragStart(event, download) {
if (!download?.succeeded || download.deleted || !download.target?.exists) {
event.preventDefault();
return;
}
const file = new lazy.FileUtils.File(download.target.path);
const { dataTransfer } = event;
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
dataTransfer.effectAllowed = "copyMove";
dataTransfer.setData("text/uri-list", Services.io.newFileURI(file).spec);
dataTransfer.addElement(event.currentTarget);
// eslint-disable-next-line mozilla/valid-services
Services.zen.playHapticFeedback();
}
#updateList() {
const shown = this.#downloads.slice(-ENTRIES);
const unused = ENTRIES - shown.length;
this.#entries.forEach((entry, i) => {
const download = shown[i - unused];
entry.hidden = !download;
entry.download = download ?? null;
if (!download) {
return;
}
this.#updateBadge(
entry.querySelector(".zen-library-download-badge"),
download
);
entry.querySelector(".zen-library-download-list-title").textContent =
this.#fileName(download);
entry.querySelector(".zen-library-download-list-subtitle").textContent =
this.#statusText(download);
entry.toggleAttribute("downloading", !download.stopped);
});
this.#tabs?.style.setProperty(
"--zen-library-stack-height",
`${this.#list.getBoundingClientRect().height}px`
);
}
#updateBadge(badge, download) {
const pending = this.#isPending(download);
badge.toggleAttribute("downloading", pending);
if (badge === this.#badge) {
badge.parentElement.toggleAttribute("downloading", pending);
}
if (download) {
badge.style.setProperty(
"--download-image",
`url('${this.#iconUrl(download)}')`
);
}
badge
.querySelector(".zen-library-download-progress")
.style.setProperty(
"--value",
download?.hasProgress ? download.progress : 0
);
}
#updateBadgeShowing() {
this.#footButtons.toggleAttribute(
"zen-library-badge",
this.#isPending(this.#downloads.at(-1)) || this.#recentNewDownload
);
}
// DownloadList view
onDownloadBatchStarting() {
this.#inBatch = true;
}
onDownloadBatchEnded() {
this.#inBatch = false;
this.#loaded = true;
this.#update();
}
onDownloadAdded(download, { insertBefore } = {}) {
const index = insertBefore ? this.#downloads.indexOf(insertBefore) : -1;
if (index === -1) {
this.#downloads.push(download);
} else {
this.#downloads.splice(index, 0, download);
}
if (this.#loaded) {
this.#recentNewDownload = true;
}
this.#update();
}
onDownloadChanged() {
this.#update();
}
onDownloadRemoved(download) {
const index = this.#downloads.indexOf(download);
if (index !== -1) {
this.#downloads.splice(index, 1);
}
this.#update();
}
#update() {
if (this.#inBatch) {
return;
}
const newest = this.#downloads.at(-1);
this.#updateList();
this.#updateBadge(this.#badge, newest);
this.#updateBadgeShowing();
this.#applyDownloadState();
}
/**
* Keeps the newest download's state on the library while it is open, for
* its downloads tab to show.
*
* @param {Element} library
*/
attachLibrary(library) {
this.#library = library;
this.#applyDownloadState();
}
detachLibrary(library) {
if (this.#library === library) {
this.#library = null;
}
}
#applyDownloadState() {
if (!this.#library) {
return;
}
const newest = this.#downloads.at(-1);
this.#library.toggleAttribute(
"zen-library-downloading",
this.#isPending(newest)
);
this.#library.style.setProperty(
"--zen-library-download-progress",
`${newest?.hasProgress ? Math.round(newest.progress) : 0}%`
);
}
// Helpers
#fileName(download) {
return download.target.path
? PathUtils.filename(download.target.path)
: download.source.url;
}
#isPending(download) {
return (
!!download &&
(!download.stopped || (download.canceled && download.hasPartialData))
);
}
#joinStatus(...parts) {
return parts
.filter(Boolean)
.reduce((a, b) => lazy.DownloadsCommon.strings.statusSeparator(a, b));
}
#iconUrl(download) {
if (!download.target.path) {
return "moz-icon://.unknown?size=32";
}
return `moz-icon://${download.target.path}?size=32${
download.succeeded ? "&state=normal" : ""
}`;
}
#statusText(download) {
const strings = lazy.DownloadsCommon.strings;
const totalBytes = download.hasProgress ? download.totalBytes : -1;
if (!download.stopped) {
const [statusText, secondsLeft] = lazy.DownloadUtils.getDownloadStatus(
download.currentBytes,
totalBytes,
download.speed,
this.#secondsLeft.get(download) ?? Infinity
);
this.#secondsLeft.set(download, secondsLeft);
return statusText;
}
this.#secondsLeft.delete(download);
if (download.deleted) {
return strings.fileDeleted;
}
if (download.succeeded) {
if (!download.target.exists) {
return strings.fileMovedOrMissing;
}
const parsed = URL.parse(download.source.url);
const uri = parsed && Services.io.newURI(parsed.href);
const host = uri
? lazy.BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true })
: "";
const [date] = lazy.DownloadUtils.getReadableDates(
new Date(download.endTime)
);
return this.#joinStatus(
lazy.DownloadsViewUI.getSizeWithUnits(download),
host,
date
);
}
if (download.canceled && download.hasPartialData) {
return this.#joinStatus(
strings.statePaused,
lazy.DownloadUtils.getTransferTotal(download.currentBytes, totalBytes)
);
}
if (download.error?.becauseBlockedByParentalControls) {
return strings.stateBlockedParentalControls;
}
if (download.error?.becauseBlockedByReputationCheck) {
return strings.blockedMalware;
}
return download.canceled ? strings.stateCanceled : strings.stateFailed;
}
#contextMenuItems(download) {
const C = lazy.DownloadsCommon;
const state = C.stateOfDownload(download);
const isActive =
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_PAUSED;
const fileExists =
state === C.DOWNLOAD_FINISHED &&
download.target?.exists !== false &&
!download.deleted;
const sourceUrl = download.source?.originalUrl || download.source?.url;
const items = [];
if (state === C.DOWNLOAD_DOWNLOADING) {
items.push({
l10nId: "downloads-cmd-pause",
onClick: () => download.cancel().catch(() => {}),
});
} else if (state === C.DOWNLOAD_PAUSED) {
items.push({
l10nId: "downloads-cmd-resume",
onClick: () => download.start?.().catch(() => {}),
});
}
if (fileExists) {
items.push({
l10nId: "downloads-cmd-show-menuitem-2",
onClick: () =>
C.showDownloadedFile(new lazy.FileUtils.File(download.target.path)),
});
}
if (sourceUrl) {
items.push({
l10nId: "downloads-cmd-go-to-download-page",
onClick: () => this.#window.openTrustedLinkIn(sourceUrl, "tab"),
});
items.push({
l10nId: "downloads-cmd-copy-download-link",
onClick: () =>
Cc["@mozilla.org/widget/clipboardhelper;1"]
.getService(Ci.nsIClipboardHelper)
.copyString(sourceUrl),
});
}
items.push({ separator: true });
if (fileExists) {
items.push({
l10nId: "downloads-cmd-delete-file",
onClick: () =>
C.deleteDownloadFiles(
download,
lazy.DownloadsViewUI.clearHistoryOnDelete
).catch(console.error),
});
}
if (!isActive) {
items.push({
l10nId: "downloads-cmd-remove-from-history",
onClick: () => C.deleteDownload(download).catch(console.error),
});
}
return items;
}
#showContextMenu(event, download) {
if (!download) {
return;
}
event.preventDefault();
event.stopPropagation();
const document = this.#window.document;
const popup = document.createXULElement("menupopup");
for (const item of this.#contextMenuItems(download)) {
if (item.separator) {
if (popup.lastChild && popup.lastChild.tagName !== "menuseparator") {
popup.appendChild(document.createXULElement("menuseparator"));
}
continue;
}
const menuitem = document.createXULElement("menuitem");
menuitem.setAttribute("data-l10n-id", item.l10nId);
menuitem.addEventListener(
"command",
() => {
try {
item.onClick();
} catch (ex) {
console.error(ex);
}
},
{ once: true }
);
popup.appendChild(menuitem);
}
if (popup.lastChild?.tagName === "menuseparator") {
popup.lastChild.remove();
}
if (!popup.childElementCount) {
return;
}
this.#contextMenuOpen = true;
popup.addEventListener(
"popuphidden",
() => {
this.#contextMenuOpen = false;
popup.remove();
this.#scheduleClose();
},
{ once: true }
);
document.getElementById("mainPopupSet").appendChild(popup);
popup.openPopupAtScreen(event.screenX, event.screenY, true);
}
}
const stacks = new WeakMap();
export const ZenLibraryWidget = {
id: "zen-library-button",
l10nId: "zen-library-button",
_introducedByPref: "zen.library.enabled",
onCreated(node) {
stacks.set(node.ownerDocument, new ZenLibraryDownloadStack(node));
},
onDestroyed(document) {
stacks.get(document)?.destroy();
stacks.delete(document);
},
attachLibrary(library) {
stacks.get(library.ownerDocument)?.attachLibrary(library);
},
detachLibrary(library) {
stacks.get(library.ownerDocument)?.detachLibrary(library);
},
};

View File

@@ -0,0 +1,5 @@
# 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/.
content/browser/zen-styles/zen-library.css (../../zen/library/zen-library.css)

15
src/zen/library/moz.build Normal file
View File

@@ -0,0 +1,15 @@
# 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/.
MOZ_SRC_FILES += [
"sections/ZenLibraryBoostsSection.mjs",
"sections/ZenLibraryDownloadsSection.mjs",
"sections/ZenLibraryHistorySection.mjs",
"sections/ZenLibrarySearchSection.mjs",
"sections/ZenLibrarySpacesSection.mjs",
"ZenLibrary.mjs",
"ZenLibraryDragAndDrop.mjs",
"ZenLibraryWidget.sys.mjs",
]

View File

@@ -0,0 +1,278 @@
/* 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, repeat } from "chrome://global/content/vendor/lit.all.mjs";
import { ZenLibrarySearchSection } from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
});
const BOOST_TOPICS = ["zen-boosts-update", "zen-boosts-active-change"];
const boostKey = boost => `${boost.domain}/${boost.id}`;
export class ZenLibraryBoostsSection extends ZenLibrarySearchSection {
static id = "boosts";
static label = "library-boosts-section-title";
static render(library) {
return html`
<zen-library-boosts-section
class="zen-library-section"
data-section="boosts"
.library=${library}
></zen-library-boosts-section>
`;
}
#observer = { observe: () => this.requestUpdate() };
#glanceBrowser = null;
#editor = null;
#menu = null;
#menuBoost = null;
#menuRow = null;
connectedCallback() {
super.connectedCallback();
for (const topic of BOOST_TOPICS) {
Services.obs.addObserver(this.#observer, topic);
}
this.#menu = this.#buildMenu();
}
disconnectedCallback() {
super.disconnectedCallback();
for (const topic of BOOST_TOPICS) {
Services.obs.removeObserver(this.#observer, topic);
}
this.#menu?.hidePopup();
this.#menu?.remove();
this.#menu = null;
this.#glanceBrowser = null;
}
get searchPlaceholderL10nId() {
return "library-boosts-search-placeholder";
}
onSearchChanged() {
this.requestUpdate();
}
#boosts() {
const boosts = [];
const query = this.searchQuery.toLowerCase();
for (const [domain, entry] of lazy.gZenBoostsManager.registeredDomains) {
for (const [id, boostEntry] of entry.boostEntries) {
const { boostData } = boostEntry;
if (!boostData.changeWasMade) {
continue;
}
if (
query &&
!boostData.boostName.toLowerCase().includes(query) &&
!domain.toLowerCase().includes(query)
) {
continue;
}
boosts.push({
id,
domain,
name: boostData.boostName,
enabled: entry.activeBoostId === id,
});
}
}
return boosts.sort(
(a, b) => a.name.localeCompare(b.name) || a.domain.localeCompare(b.domain)
);
}
// Actions
#toggle(boost) {
lazy.gZenBoostsManager.toggleBoostActiveForDomain(boost.domain, boost.id);
}
#onRowClick(boost, row) {
if (boost.enabled) {
this.#edit(boost, row);
} else {
this.#toggle(boost);
}
}
async #edit(boost, row) {
if (this.#glanceBrowser) {
return;
}
const url = `https://${boost.domain}/`;
const uri = Services.io.newURI(url);
if (!lazy.gZenBoostsManager.canBoostSite(uri)) {
return;
}
const rowRect = row.getBoundingClientRect();
const glance = await gZenGlanceManager.openDetachedGlance({
url,
clientX: rowRect.left + rowRect.width / 2,
clientY: rowRect.top + rowRect.height / 2,
userContextId: gZenWorkspaces.getActiveWorkspace()?.containerTabId,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
if (!glance) {
return;
}
const { browser } = glance;
this.#glanceBrowser = browser;
glance.closed.then(() => {
this.#glanceBrowser = null;
this.#editor?.close();
this.#editor = null;
});
await this.#whenNavigated(browser);
if (this.#glanceBrowser !== browser) {
return;
}
const stored = lazy.gZenBoostsManager.loadBoostFromStore(
boost.domain,
boost.id
);
this.#editor = lazy.gZenBoostsManager.openBoostWindow(window, stored, uri, {
browser,
});
}
#whenNavigated(browser) {
if (browser.currentURI?.spec !== "about:blank") {
return Promise.resolve();
}
return new Promise(resolve => {
const listener = {
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
onLocationChange(webProgress) {
if (webProgress.isTopLevel) {
browser.removeProgressListener(listener);
resolve();
}
},
};
browser.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_LOCATION);
});
}
async #export(boost) {
const { boostEntry } = lazy.gZenBoostsManager.loadBoostFromStore(
boost.domain,
boost.id
);
await lazy.gZenBoostsManager.exportBoost(window, boostEntry.boostData);
}
#delete(boost) {
lazy.gZenBoostsManager.deleteBoost({ domain: boost.domain, id: boost.id });
}
// Context menu
#buildMenu() {
const menu = window.MozXULElement.parseXULToFragment(`
<menupopup class="zen-library-boosts-menu">
<menuitem data-action="edit" data-l10n-id="library-boosts-menu-edit"/>
<menuitem data-action="export" data-l10n-id="zen-boost-save"/>
<menuseparator/>
<menuitem data-action="delete" data-l10n-id="zen-boost-edit-delete"/>
</menupopup>
`).firstElementChild;
menu.addEventListener("command", event => {
const boost = this.#menuBoost;
const row = this.#menuRow;
if (!boost) {
return;
}
switch (event.target.dataset.action) {
case "edit":
this.#edit(boost, row);
break;
case "export":
this.#export(boost);
break;
case "delete":
this.#delete(boost);
break;
}
});
menu.addEventListener("popuphidden", () => {
this.#menuRow?.removeAttribute("menu-open");
this.#menuRow = null;
this.#menuBoost = null;
});
document.getElementById("mainPopupSet").appendChild(menu);
return menu;
}
#openMenu(boost, row, event) {
this.#menuRow?.removeAttribute("menu-open");
this.#menuBoost = boost;
this.#menuRow = row;
row.setAttribute("menu-open", "true");
this.#menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
}
// Rendering
#renderBoost(boost) {
return html`
<div
class="zen-library-row zen-library-boost-row"
?disabled=${!boost.enabled}
@click=${event => this.#onRowClick(boost, event.currentTarget)}
@contextmenu=${event => {
event.preventDefault();
this.#openMenu(boost, event.currentTarget, event);
}}
>
<div class="zen-library-boost-icon">
<img src="page-icon:https://${boost.domain}/" alt="" />
</div>
<div class="zen-library-row-text">
<span class="zen-library-row-title">${boost.name}</span>
<span class="zen-library-row-subtitle">${boost.domain}</span>
</div>
<div class="zen-library-row-actions">
<moz-toggle
?pressed=${boost.enabled}
data-l10n-id="library-boosts-toggle"
@click=${event => event.stopPropagation()}
@toggle=${() => this.#toggle(boost)}
></moz-toggle>
</div>
</div>
`;
}
renderItems() {
const boosts = this.#boosts();
if (!boosts.length) {
return html`
<div
class="zen-library-empty"
data-l10n-id="library-boosts-empty"
></div>
`;
}
return html`
<div class="zen-library-group">
${repeat(boosts, boostKey, boost => this.#renderBoost(boost))}
</div>
`;
}
}
customElements.define("zen-library-boosts-section", ZenLibraryBoostsSection);

View File

@@ -0,0 +1,664 @@
/* 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, repeat, when } from "chrome://global/content/vendor/lit.all.mjs";
import {
MS_PER_DAY,
PAGE_SIZE,
ZenLibrarySearchSection,
whenFilterGroup,
} from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
DownloadsCommon:
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
DownloadsViewUI:
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
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";
static render(library) {
return html`
<zen-library-downloads-section
class="zen-library-section"
data-section="downloads"
.library=${library}
></zen-library-downloads-section>
`;
}
// Oldest to newest, mirroring the order of the underlying download list.
#downloads = [];
#data = null;
#loaded = false;
#inBatch = false;
#limit = PAGE_SIZE;
#visible = [];
#refreshed = new WeakSet();
#secondsLeft = new WeakMap();
#menu = null;
#menuDownload = null;
#menuRow = null;
#openingDownload = null;
#openingTimer = null;
connectedCallback() {
super.connectedCallback();
this.#data = lazy.DownloadsCommon.getData(window, true);
this.#data.addView(this);
this.#menu = this.#buildMenu();
}
disconnectedCallback() {
super.disconnectedCallback();
this.#data?.removeView(this);
this.#data = null;
this.#menu?.hidePopup();
this.#menu?.remove();
this.#menu = null;
clearTimeout(this.#openingTimer);
this.#openingTimer = null;
this.#openingDownload = null;
}
get searchPlaceholderL10nId() {
return "places-search-downloads";
}
get filterTitleL10nId() {
return "library-downloads-filter-title";
}
get filterGroups() {
return [
{
id: "type",
titleL10nId: "library-downloads-filter-type",
options: Object.keys(FILE_TYPES).map(id => ({
id,
l10nId: `library-downloads-type-${id}`,
})),
},
whenFilterGroup("library-downloads-filter-when"),
];
}
onSearchChanged() {
this.#limit = PAGE_SIZE;
this.requestUpdate();
}
onFiltersChanged() {
this.#limit = PAGE_SIZE;
this.requestUpdate();
}
onListScrolledToEnd() {
if (this.#visible.length < this.#limit) {
return;
}
this.#limit += PAGE_SIZE;
this.requestUpdate();
}
// DownloadList view
onDownloadBatchStarting() {
this.#inBatch = true;
}
onDownloadBatchEnded() {
this.#inBatch = false;
this.#loaded = true;
this.requestUpdate();
}
onDownloadAdded(download, { insertBefore } = {}) {
const index = insertBefore ? this.#downloads.indexOf(insertBefore) : -1;
if (index === -1) {
this.#downloads.push(download);
} else {
this.#downloads.splice(index, 0, download);
}
this.#scheduleUpdate();
}
onDownloadChanged() {
this.#scheduleUpdate();
}
onDownloadRemoved(download) {
const index = this.#downloads.indexOf(download);
if (index !== -1) {
this.#downloads.splice(index, 1);
}
if (this.#menuDownload === download) {
this.#menu?.hidePopup();
}
this.#scheduleUpdate();
}
#scheduleUpdate() {
if (!this.#inBatch) {
this.requestUpdate();
}
}
updated(changedProperties) {
super.updated(changedProperties);
// Check that the files of rendered downloads still exist, like the
// downloads panel does when it opens.
for (const download of this.#visible) {
if (download.succeeded && !this.#refreshed.has(download)) {
this.#refreshed.add(download);
download.refresh().catch(console.error);
}
}
}
// Helpers
#fileName(download) {
return download.target.path
? PathUtils.filename(download.target.path)
: download.source.url;
}
#hasFile(download) {
return download.succeeded && !download.deleted && download.target.exists;
}
#file(download) {
return new lazy.FileUtils.File(download.target.path);
}
#formatUrl(url) {
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
}
#matchesQuery(download) {
const query = this.searchQuery.toLowerCase();
return (
this.#fileName(download).toLowerCase().includes(query) ||
download.source.url.toLowerCase().includes(query)
);
}
#fileType(download) {
const extension = this.#fileName(download).match(/\.([^.]+)$/)?.[1];
return extension ? EXTENSION_TYPES.get(extension.toLowerCase()) : undefined;
}
#activeTypes() {
return Object.keys(FILE_TYPES).filter(id =>
this.isFilterActive("type", id)
);
}
#whenCutoff() {
const days = this.activeWhenDays;
return days ? Date.now() - days * MS_PER_DAY : 0;
}
#computeVisible() {
const visible = [];
const types = this.#activeTypes();
const cutoff = this.#whenCutoff();
for (let i = this.#downloads.length - 1; i >= 0; i--) {
const download = this.#downloads[i];
if (types.length && !types.includes(this.#fileType(download))) {
continue;
}
if (cutoff && !(download.endTime >= cutoff)) {
continue;
}
if (!this.searchQuery || this.#matchesQuery(download)) {
visible.push(download);
if (visible.length >= this.#limit) {
break;
}
}
}
return visible;
}
#iconUrl(download) {
if (!download.target.path) {
return "moz-icon://.unknown?size=32";
}
return `moz-icon://${download.target.path}?size=32${
download.succeeded ? "&state=normal" : ""
}`;
}
#isPending(download) {
return !download.stopped || (download.canceled && download.hasPartialData);
}
#joinStatus(...parts) {
return parts
.filter(Boolean)
.reduce((a, b) => lazy.DownloadsCommon.strings.statusSeparator(a, b));
}
#statusText(download) {
const strings = lazy.DownloadsCommon.strings;
const totalBytes = download.hasProgress ? download.totalBytes : -1;
if (!download.stopped) {
const [statusText, secondsLeft] = lazy.DownloadUtils.getDownloadStatus(
download.currentBytes,
totalBytes,
download.speed,
this.#secondsLeft.get(download) ?? Infinity
);
this.#secondsLeft.set(download, secondsLeft);
return statusText;
}
this.#secondsLeft.delete(download);
if (download.deleted) {
return strings.fileDeleted;
}
if (download.succeeded) {
if (!download.target.exists) {
return strings.fileMovedOrMissing;
}
const parsed = URL.parse(download.source.url);
const uri = parsed && Services.io.newURI(parsed.href);
const host = uri
? lazy.BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true })
: "";
const [date] = lazy.DownloadUtils.getReadableDates(
new Date(download.endTime)
);
return this.#joinStatus(
lazy.DownloadsViewUI.getSizeWithUnits(download),
host,
date
);
}
if (download.canceled && download.hasPartialData) {
return this.#joinStatus(
strings.statePaused,
lazy.DownloadUtils.getTransferTotal(download.currentBytes, totalBytes)
);
}
if (download.error?.becauseBlockedByParentalControls) {
return strings.stateBlockedParentalControls;
}
if (download.error?.becauseBlockedByReputationCheck) {
return strings.blockedMalware;
}
return download.canceled ? strings.stateCanceled : strings.stateFailed;
}
// Actions
#showInFolder(download) {
if (!this.#hasFile(download)) {
return;
}
lazy.DownloadsCommon.showDownloadedFile(this.#file(download));
}
#onRowClick(download) {
if (!this.#hasFile(download)) {
return;
}
this.#showInFolder(download);
clearTimeout(this.#openingTimer);
this.#openingDownload = download;
this.#openingTimer = setTimeout(() => {
this.#openingTimer = null;
this.#openingDownload = null;
this.requestUpdate();
}, OPENING_FEEDBACK_MS);
this.requestUpdate();
}
#canRetry(download) {
return (
download.stopped &&
!download.succeeded &&
(download.canceled || !!download.error)
);
}
#retryDownload(download) {
if (download.start) {
// Errors when retrying are already reported as download failures.
download.start().catch(() => {});
return;
}
// History-only entries have no session object, so download the URL again.
const targetName = download.target.path
? PathUtils.filename(download.target.path)
: null;
window.DownloadURL(download.source.url, targetName, document);
}
#cancelDownload(download) {
download.cancel().catch(() => {});
download
.removePartialData()
.catch(console.error)
.finally(() => download.target.refresh());
}
#openDownload(download) {
if (!this.#hasFile(download)) {
return;
}
lazy.DownloadsCommon.openDownload(download, { openWhere: "tab" });
}
#copyFile(download) {
if (!this.#hasFile(download)) {
return;
}
const transferable = Cc[
"@mozilla.org/widget/transferable;1"
].createInstance(Ci.nsITransferable);
transferable.init(window.docShell.QueryInterface(Ci.nsILoadContext));
transferable.addDataFlavor(FILE_MIME);
transferable.setTransferData(FILE_MIME, this.#file(download));
Services.clipboard.setData(
transferable,
null,
Services.clipboard.kGlobalClipboard
);
}
#hideDownload(download) {
lazy.DownloadsCommon.deleteDownload(download).catch(console.error);
}
#trashDownload(download) {
lazy.DownloadsCommon.deleteDownloadFiles(
download,
lazy.DownloadsViewUI.clearHistoryOnDelete
).catch(console.error);
}
#isActionEnabled(action, download) {
switch (action) {
case "open":
case "copy":
case "show":
return this.#hasFile(download);
case "copy-link":
return !download.source.isDataURICleared;
case "trash":
return (
this.#hasFile(download) ||
!download.stopped ||
download.hasPartialData
);
default:
return true;
}
}
#doAction(action, download) {
switch (action) {
case "open":
this.#openDownload(download);
break;
case "copy":
this.#copyFile(download);
break;
case "copy-link":
lazy.DownloadsCommon.copyDownloadLink(download);
break;
case "show":
this.#showInFolder(download);
break;
case "hide":
this.#hideDownload(download);
break;
case "trash":
this.#trashDownload(download);
break;
}
}
#onDragStart(event, download) {
if (!this.#hasFile(download) || !this.#file(download).exists()) {
event.preventDefault();
return;
}
const file = this.#file(download);
const { dataTransfer } = event;
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
dataTransfer.effectAllowed = "copyMove";
dataTransfer.setData("text/uri-list", Services.io.newFileURI(file).spec);
dataTransfer.addElement(event.currentTarget);
// eslint-disable-next-line mozilla/valid-services
Services.zen.playHapticFeedback();
}
// Context menu
#buildMenu() {
const menu = window.MozXULElement.parseXULToFragment(`
<menupopup class="zen-library-downloads-menu">
<menuitem data-action="open" data-l10n-id="library-downloads-menu-open"/>
<menuitem data-action="show" data-l10n-id="downloads-cmd-show-menuitem-2"/>
<menuseparator/>
<menuitem data-action="copy" data-l10n-id="library-downloads-menu-copy"/>
<menuitem data-action="copy-link" data-l10n-id="downloads-cmd-copy-download-link"/>
<menuseparator/>
<menuitem data-action="hide" data-l10n-id="library-downloads-menu-hide"/>
<menuitem data-action="trash" data-l10n-id="library-downloads-menu-trash"/>
</menupopup>
`).firstElementChild;
menu.addEventListener("command", event => {
if (this.#menuDownload) {
this.#doAction(event.target.dataset.action, this.#menuDownload);
}
});
menu.addEventListener("popuphidden", () => {
this.#menuRow?.removeAttribute("menu-open");
this.#menuRow = null;
this.#menuDownload = null;
});
document.getElementById("mainPopupSet").appendChild(menu);
return menu;
}
#openMenu(download, row, anchor, event) {
const fileName = this.#fileName(download);
for (const item of this.#menu.querySelectorAll("menuitem")) {
const { action } = item.dataset;
if (action === "open" || action === "copy") {
document.l10n.setAttributes(item, item.dataset.l10nId, {
name: fileName,
});
}
item.disabled = !this.#isActionEnabled(action, download);
}
this.#menuRow?.removeAttribute("menu-open");
this.#menuRow = row;
this.#menuDownload = download;
row.setAttribute("menu-open", "true");
if (anchor) {
this.#menu.openPopup(anchor, "after_end", 0, 4, false, false, event);
} else {
this.#menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
}
}
// Rendering
#renderSubtitle(download) {
const statusNode =
download === this.#openingDownload
? html`<span
class="zen-library-download-status"
data-l10n-id="library-downloads-opening-in"
></span>`
: html`<span class="zen-library-download-status"
>${this.#statusText(download)}</span
>`;
return html`
<span class="zen-library-row-subtitle">
${statusNode}
<span class="zen-library-download-url"
>${this.#formatUrl(download.source.url)}</span
>
</span>
`;
}
#renderCancelButton(download) {
return html`
<toolbarbutton
class="toolbarbutton-1"
data-l10n-id="library-downloads-cancel-button"
@click=${event => {
event.stopPropagation();
this.#cancelDownload(download);
}}
>
<img
class="toolbarbutton-icon"
src="chrome://browser/skin/zen-icons/close.svg"
alt=""
/>
</toolbarbutton>
`;
}
#renderRetryButton(download) {
return html`
<toolbarbutton
class="toolbarbutton-1"
data-l10n-id="library-downloads-retry-button"
@click=${event => {
event.stopPropagation();
this.#retryDownload(download);
}}
>
<img
class="toolbarbutton-icon"
src="chrome://browser/skin/zen-icons/reload.svg"
alt=""
/>
</toolbarbutton>
`;
}
#renderDownload(download) {
const pending = this.#isPending(download);
const progress = download.hasProgress ? download.progress : 0;
return html`
<div
class="zen-library-row"
draggable="true"
style="--zen-library-download-progress: ${progress}%"
?pending=${pending}
?indeterminate=${pending && !download.hasProgress}
?paused=${pending && download.stopped}
?opening=${download === this.#openingDownload}
@click=${() => this.#onRowClick(download)}
@contextmenu=${event => {
event.preventDefault();
this.#openMenu(download, event.currentTarget, null, event);
}}
@dragstart=${event => this.#onDragStart(event, download)}
>
<img
class="zen-library-row-icon"
src=${this.#iconUrl(download)}
alt=""
/>
<div class="zen-library-row-text">
<span class="zen-library-row-title">${this.#fileName(download)}</span>
${this.#renderSubtitle(download)}
</div>
<div class="zen-library-row-actions">
${when(!download.stopped, () => this.#renderCancelButton(download))}
${when(this.#canRetry(download), () =>
this.#renderRetryButton(download)
)}
<toolbarbutton
class="toolbarbutton-1"
data-l10n-id="library-downloads-more-button"
@click=${event => {
event.stopPropagation();
this.#openMenu(
download,
event.currentTarget.closest(".zen-library-row"),
event.currentTarget,
event
);
}}
>
<img
class="toolbarbutton-icon"
src="chrome://global/skin/icons/more.svg"
alt=""
/>
</toolbarbutton>
</div>
</div>
`;
}
renderItems() {
if (!this.#loaded) {
return null;
}
this.#visible = this.#computeVisible();
if (!this.#visible.length) {
return html`
<div
class="zen-library-empty"
data-l10n-id="library-downloads-empty"
></div>
`;
}
return html`
<div class="zen-library-group">
${repeat(
this.#visible,
download => download,
download => this.#renderDownload(download)
)}
</div>
`;
}
}
customElements.define(
"zen-library-downloads-section",
ZenLibraryDownloadsSection
);

View File

@@ -0,0 +1,361 @@
/* 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, repeat } from "chrome://global/content/vendor/lit.all.mjs";
import {
MS_PER_DAY,
PAGE_SIZE,
ZenLibrarySearchSection,
whenFilterGroup,
} from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
let lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PlacesQuery: "resource://gre/modules/PlacesQuery.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(
lazy,
"relativeDayFormat",
() => new Intl.RelativeTimeFormat(undefined, { numeric: "auto" })
);
ChromeUtils.defineLazyGetter(
lazy,
"dateFormat",
() => new Intl.DateTimeFormat(undefined, { dateStyle: "medium" })
);
const HISTORY_DAYS_OLD = 120;
const SORT_OPTIONS = ["date", "site", "mostvisited", "lastvisited"];
const visitKey = visit => `${visit.guid}-${visit.date.getTime()}`;
export class ZenLibraryHistorySection extends ZenLibrarySearchSection {
static id = "history";
static label = "library-history-section-title";
static render(library) {
return html`
<zen-library-history-section
class="zen-library-section"
data-section="history"
.library=${library}
></zen-library-history-section>
`;
}
static properties = {
visits: { state: true },
};
#placesQuery = null;
#limit = PAGE_SIZE;
#fetchGeneration = 0;
#exhausted = false;
constructor() {
super();
this.visits = null;
this.activeFilters.add("sort:date");
}
connectedCallback() {
super.connectedCallback();
this.#placesQuery = new lazy.PlacesQuery();
this.#placesQuery.observeHistory(() => this.#fetch());
this.#fetch();
}
disconnectedCallback() {
super.disconnectedCallback();
this.#placesQuery?.close();
this.#placesQuery = null;
}
get searchPlaceholderL10nId() {
return "library-history-search-placeholder";
}
get filterTitleL10nId() {
return "library-history-filter-title";
}
get filterGroups() {
return [
whenFilterGroup("library-history-filter-when"),
{
id: "sort",
titleL10nId: "library-history-filter-sort",
exclusive: true,
options: [
{ id: "date", l10nId: "library-history-sort-date" },
{ id: "site", l10nId: "library-history-sort-site" },
{ id: "mostvisited", l10nId: "library-history-sort-most-visited" },
{ id: "lastvisited", l10nId: "library-history-sort-last-visited" },
],
},
];
}
get #activeDaysOld() {
return this.activeWhenDays ?? HISTORY_DAYS_OLD;
}
get #activeSort() {
return SORT_OPTIONS.find(id => this.isFilterActive("sort", id)) ?? "date";
}
onSearchChanged() {
this.#resetAndFetch();
}
onFiltersChanged() {
if (!SORT_OPTIONS.some(id => this.isFilterActive("sort", id))) {
this.activeFilters.add("sort:date");
this.requestUpdate();
}
this.#resetAndFetch();
}
onListScrolledToEnd() {
if (this.#exhausted || !this.#placesQuery) {
return;
}
this.#limit += PAGE_SIZE;
this.#fetch();
}
#resetAndFetch() {
this.#limit = PAGE_SIZE;
this.#exhausted = false;
this.#fetch();
}
async #fetch() {
if (!this.#placesQuery) {
return;
}
const generation = ++this.#fetchGeneration;
const daysOld = this.#activeDaysOld;
let visits;
if (this.searchQuery) {
visits =
(await this.#placesQuery.searchHistory(
this.searchQuery,
this.#limit
)) ?? [];
} else if (this.#activeSort === "mostvisited") {
visits = this.#fetchMostVisited(daysOld, this.#limit);
} else {
visits = await this.#placesQuery.getHistory({
daysOld,
limit: this.#limit,
sortBy: this.#activeSort,
});
}
if (generation !== this.#fetchGeneration || !this.#placesQuery) {
return;
}
this.#exhausted = this.#countVisits(visits) < this.#limit;
if (this.searchQuery && daysOld !== HISTORY_DAYS_OLD) {
const cutoff = Date.now() - daysOld * MS_PER_DAY;
visits = visits.filter(visit => visit.date.getTime() >= cutoff);
}
this.visits = visits;
// The cached Map is mutated in place, so its identity may not change.
this.requestUpdate();
}
#countVisits(visits) {
if (Array.isArray(visits)) {
return visits.length;
}
let count = 0;
for (const groupVisits of visits.values()) {
count += groupVisits.length;
}
return count;
}
#fetchMostVisited(daysOld, limit) {
const query = lazy.PlacesUtils.history.getNewQuery();
query.beginTime = lazy.PlacesUtils.toPRTime(
Date.now() - daysOld * MS_PER_DAY
);
const options = lazy.PlacesUtils.history.getNewQueryOptions();
options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
options.maxResults = limit;
const root = lazy.PlacesUtils.history.executeQuery(query, options).root;
root.containerOpen = true;
const visits = [];
for (let i = 0; i < root.childCount; i++) {
const node = root.getChild(i);
visits.push({
url: node.uri,
title: node.title,
date: lazy.PlacesUtils.toDate(node.time),
guid: node.pageGuid,
});
}
root.containerOpen = false;
return visits;
}
#openVisit(visit) {
window.openTrustedLinkIn(visit.url, "tab");
this.library?.constructor.toggle();
}
#forgetVisit(visit) {
lazy.PlacesUtils.history.remove(visit.url);
this.visits = this.#withoutUrl(this.visits, visit.url);
}
#withoutUrl(container, url) {
if (Array.isArray(container)) {
return container.filter(visit => visit.url !== url);
}
const remaining = new Map();
for (const [key, groupVisits] of container) {
const filtered = groupVisits.filter(visit => visit.url !== url);
if (filtered.length) {
remaining.set(key, filtered);
}
}
return remaining;
}
#formatDay(day) {
const daysAgo = Math.round(
(this.#placesQuery.getStartOfDayTimestamp(new Date()) - day) / MS_PER_DAY
);
const formatted =
daysAgo <= 6
? lazy.relativeDayFormat.format(-daysAgo, "day")
: lazy.dateFormat.format(day);
return formatted.charAt(0).toLocaleUpperCase() + formatted.slice(1);
}
#formatUrl(url) {
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
}
/**
* A visit drags as a link, so it can be dropped onto content, the tab
* strip or another app.
*
* @param {DragEvent} event
* @param {object} visit
*/
#onDragStart(event, visit) {
const { dataTransfer } = event;
const title = visit.title || visit.url;
dataTransfer.setData("text/x-moz-url", `${visit.url}\n${title}`);
dataTransfer.setData("text/uri-list", visit.url);
dataTransfer.setData("text/plain", visit.url);
dataTransfer.effectAllowed = "copyLink";
dataTransfer.addElement(event.currentTarget);
// eslint-disable-next-line mozilla/valid-services
Services.zen.playHapticFeedback();
}
#renderVisit(visit) {
return html`
<div
class="zen-library-row"
draggable="true"
@click=${() => this.#openVisit(visit)}
@dragstart=${event => this.#onDragStart(event, visit)}
>
<img class="zen-library-row-icon" src="page-icon:${visit.url}" alt="" />
<div class="zen-library-row-text">
<span class="zen-library-row-title">${visit.title || visit.url}</span>
<span class="zen-library-row-subtitle"
>${this.#formatUrl(visit.url)}</span
>
</div>
<div class="zen-library-row-actions">
<toolbarbutton
class="toolbarbutton-1"
data-l10n-id="library-history-forget-button"
@click=${event => {
event.stopPropagation();
this.#forgetVisit(visit);
}}
>
<img
class="toolbarbutton-icon"
src="chrome://browser/skin/zen-icons/trash.svg"
alt=""
/>
</toolbarbutton>
<toolbarbutton
class="toolbarbutton-1"
data-l10n-id="library-history-reopen-button"
@click=${event => {
event.stopPropagation();
this.#openVisit(visit);
}}
>
<img
class="toolbarbutton-icon"
src="chrome://browser/skin/zen-icons/arrow-rotate-anticlockwise.svg"
alt=""
/>
</toolbarbutton>
</div>
</div>
`;
}
#renderVisits(visits) {
return repeat(visits, visitKey, visit => this.#renderVisit(visit));
}
#renderGroupHeader(key) {
if (typeof key === "number") {
return html`<h3>${this.#formatDay(key)}</h3>`;
}
return key
? html`<h3>${key}</h3>`
: html`<h3 data-l10n-id="library-history-site-other"></h3>`;
}
#renderEmpty() {
return html`
<div class="zen-library-empty" data-l10n-id="library-history-empty"></div>
`;
}
renderItems() {
if (!this.visits) {
return null;
}
if (Array.isArray(this.visits)) {
if (!this.visits.length) {
return this.#renderEmpty();
}
return html`
<div class="zen-library-group">${this.#renderVisits(this.visits)}</div>
`;
}
if (!this.visits.size) {
return this.#renderEmpty();
}
return repeat(
this.visits.entries(),
([key]) => key,
([key, groupVisits]) => html`
<div class="zen-library-group">
${this.#renderGroupHeader(key)} ${this.#renderVisits(groupVisits)}
</div>
`
);
}
}
customElements.define("zen-library-history-section", ZenLibraryHistorySection);

View File

@@ -0,0 +1,265 @@
/* 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, when } from "chrome://global/content/vendor/lit.all.mjs";
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const SEARCH_DEBOUNCE_MS = 250;
export const PAGE_SIZE = 100;
export const MS_PER_DAY = 86400000;
const WHEN_DAYS = { today: 1, week: 7, month: 30 };
/**
* Builds the exclusive "when" filter group shared by time-based sections.
*
* @param {string} titleL10nId - Fluent id of the group title
*/
export function whenFilterGroup(titleL10nId) {
return {
id: "when",
titleL10nId,
exclusive: true,
options: Object.keys(WHEN_DAYS).map(id => ({
id,
l10nId: `library-filter-${id}`,
})),
};
}
export class ZenLibrarySearchSection extends MozLitElement {
static properties = {
searchQuery: { type: String, state: true },
filtersOpen: { type: Boolean, state: true },
};
#searchDebounce = null;
#sentinelObserver = null;
constructor() {
super();
this.searchQuery = "";
this.filtersOpen = false;
this.activeFilters = new Set();
}
createRenderRoot() {
return this;
}
get searchPlaceholderL10nId() {
return "";
}
get filterTitleL10nId() {
return "";
}
/**
* Filter groups shown in the filter panel. Sections without filters keep
* this empty, which also hides the filter button. An `exclusive` group
* behaves like a radio group where the active option can also be toggled
* back off.
*
* @returns {{id: string, titleL10nId: string, exclusive?: boolean,
* options: {id: string, l10nId?: string, label?: string,
* icon?: string}[]}[]}
*/
get filterGroups() {
return [];
}
renderItems() {
return null;
}
onSearchChanged() {}
onFiltersChanged() {}
onListScrolledToEnd() {}
isFilterActive(groupId, optionId) {
return this.activeFilters.has(`${groupId}:${optionId}`);
}
/**
* Number of days selected in the "when" filter group, or null when no
* option is active.
*/
get activeWhenDays() {
for (const [id, days] of Object.entries(WHEN_DAYS)) {
if (this.isFilterActive("when", id)) {
return days;
}
}
return null;
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.#searchDebounce) {
clearTimeout(this.#searchDebounce);
this.#searchDebounce = null;
}
this.#sentinelObserver?.disconnect();
this.#sentinelObserver = null;
}
firstUpdated() {
if (super.firstUpdated) {
super.firstUpdated();
}
const results = this.querySelector(".zen-library-search-results");
this.#sentinelObserver = new IntersectionObserver(
entries => {
if (entries.some(entry => entry.isIntersecting)) {
this.onListScrolledToEnd();
}
},
{ root: results, rootMargin: "200px" }
);
this.#sentinelObserver.observe(
this.querySelector(".zen-library-search-sentinel")
);
results.addEventListener(
"scroll",
() => results.toggleAttribute("scrolled", results.scrollTop > 0),
{ passive: true }
);
}
updated(changedProperties) {
if (super.updated) {
super.updated(changedProperties);
}
if (changedProperties.has("filtersOpen") && this.filtersOpen) {
const inner = this.querySelector(".zen-library-filter-panel-inner");
this.style.setProperty(
"--zen-library-filter-height",
`${inner.scrollHeight + 8}px`
);
}
}
#onSearchInput(event) {
const { value } = event.target;
if (this.#searchDebounce) {
clearTimeout(this.#searchDebounce);
}
this.#searchDebounce = setTimeout(() => {
this.#searchDebounce = null;
const query = value.trim();
if (query !== this.searchQuery) {
this.searchQuery = query;
this.onSearchChanged();
}
}, SEARCH_DEBOUNCE_MS);
}
#toggleFilter(group, optionId) {
const key = `${group.id}:${optionId}`;
const wasActive = this.activeFilters.has(key);
if (group.exclusive) {
for (const option of group.options) {
this.activeFilters.delete(`${group.id}:${option.id}`);
}
}
if (wasActive) {
this.activeFilters.delete(key);
} else {
this.activeFilters.add(key);
}
this.requestUpdate();
this.onFiltersChanged();
}
#renderFilterOption(group, option) {
return html`
<button
class="zen-library-filter-chip"
?active=${this.isFilterActive(group.id, option.id)}
@click=${() => this.#toggleFilter(group, option.id)}
>
${when(option.icon, () => html`<img src=${option.icon} alt="" />`)}
${
option.l10nId
? html`<span data-l10n-id=${option.l10nId}></span>`
: html`<span>${option.label}</span>`
}
</button>
`;
}
#renderFilterPanel() {
return html`
<div class="zen-library-filter-header" ?inert=${!this.filtersOpen}>
<h2 data-l10n-id=${this.filterTitleL10nId}></h2>
<button
class="zen-library-filter-done"
data-l10n-id="library-filter-done"
@click=${() => {
this.filtersOpen = false;
}}
></button>
</div>
<div class="zen-library-filter-panel">
<div class="zen-library-filter-panel-inner" ?inert=${!this.filtersOpen}>
${this.filterGroups.map(
group => html`
<div class="zen-library-filter-group">
<h3 data-l10n-id=${group.titleL10nId}></h3>
<div class="zen-library-filter-options">
${group.options.map(option =>
this.#renderFilterOption(group, option)
)}
</div>
</div>
`
)}
</div>
</div>
`;
}
render() {
const hasFilters = !!this.filterGroups.length;
return html`
<div class="zen-library-search-top" ?open=${this.filtersOpen}>
<div class="zen-library-search-header" ?inert=${this.filtersOpen}>
<div class="zen-library-search-box">
<img
src="chrome://browser/skin/zen-icons/search-glass.svg"
alt=""
/>
<input
type="search"
data-l10n-id=${this.searchPlaceholderL10nId}
@input=${this.#onSearchInput}
/>
</div>
${when(
hasFilters,
() => html`
<button
class="zen-library-filter-button"
@click=${() => {
this.filtersOpen = true;
}}
>
<img src="chrome://browser/skin/zen-icons/sliders.svg" alt="" />
<span data-l10n-id="library-filter-button"></span>
</button>
`
)}
</div>
${when(hasFilters, () => this.#renderFilterPanel())}
</div>
<div class="zen-library-search-results">
${this.renderItems()}
<div class="zen-library-search-sentinel"></div>
</div>
`;
}
}

View File

@@ -0,0 +1,820 @@
/* 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,
repeat,
styleMap,
} from "chrome://global/content/vendor/lit.all.mjs";
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 SCROLL_EDGE_PX = 48;
const SCROLL_STEP_PX = 12;
// Events the copies fire while being built that the rest of the browser must
// not mistake for real tab strip changes.
// Changes to the real strips that the copies must follow. Attribute changes
// count only for what a copy shows.
const STRIP_EVENTS = [
"TabOpen",
"TabClose",
"TabMove",
"TabPinned",
"TabUnpinned",
"TabHide",
"TabShow",
"TabAttrModified",
"TabGrouped",
"TabUngrouped",
"TabGroupCreate",
"TabGroupRemoved",
"TabGroupMoved",
"TabGroupCollapse",
"TabGroupExpand",
];
const SHOWN_ATTRIBUTES = ["label", "image", "pending", "muted", "soundplaying"];
const CONTAINED_EVENTS = [
"TabGrouped",
"TabUngrouped",
"TabGroupCreate",
"TabGroupRemovedFromDOM",
"FolderGrouped",
"FolderUngrouped",
"TabGroupCollapse",
"TabGroupExpand",
];
const GROUP_TAGS = ["tab-group", "zen-folder", "tab-split-view-wrapper"];
export class ZenLibrarySpacesSection extends MozLitElement {
static id = "spaces";
static label = "library-spaces-section-title";
static render(library) {
return html`
<zen-library-spaces-section
class="zen-library-section"
data-section="spaces"
.library=${library}
></zen-library-spaces-section>
`;
}
static properties = {
renaming: { state: true },
};
#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;
}
if (
event.type === "TabAttrModified" &&
!event.detail?.changed?.some(attr => SHOWN_ATTRIBUTES.includes(attr))
) {
return;
}
this.#scheduleStripsRefresh(
event.type === "TabAttrModified"
? event.target.getAttribute("zen-workspace-id")
: null
);
};
#refreshTimer = null;
/** @type {Set<string>|null} The spaces to rebuild, or null for every card */
#pendingSpaces = new Set();
#resizeObserver = new ResizeObserver(() => this.#updateLibraryWidth());
#onScroll = event => {
if (event.target.classList.contains("zen-library-space-body")) {
this.#updateScrollBorders(event.target);
}
};
#onDragEnd = () => {
this.#landing = false;
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;
/** @type {WeakMap<Element, Element>} copied tab or group to the real one */
#realElements = new WeakMap();
#dnd = new ZenLibraryDragAndDrop(this);
#dragIndex = -1;
#dropIndex = -1;
#dragStartX = 0;
#dragCenterX = 0;
#dragStartScroll = 0;
#lastPointerX = 0;
#scrollFrame = null;
#slotCenters = [];
constructor() {
super();
this.renaming = null;
}
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("ZenWorkspaceDataChanged", this.#onDataChanged);
window.addEventListener("ZenWorkspacesUIUpdate", this.#onDataChanged);
Services.obs.addObserver(this.#observer, GRADIENT_TOPIC);
this.#resizeObserver.observe(this);
window.addEventListener("dragend", this.#onDragEnd);
this.addEventListener("scroll", this.#onScroll, true);
for (const type of CONTAINED_EVENTS) {
this.addEventListener(type, this.#containEvent, true);
}
for (const type of STRIP_EVENTS) {
window.addEventListener(type, this.#onStripEvent);
}
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("ZenWorkspaceDataChanged", this.#onDataChanged);
window.removeEventListener("ZenWorkspacesUIUpdate", this.#onDataChanged);
Services.obs.removeObserver(this.#observer, GRADIENT_TOPIC);
this.#resizeObserver.disconnect();
window.removeEventListener("dragend", this.#onDragEnd);
this.removeEventListener("scroll", this.#onScroll, true);
for (const type of CONTAINED_EVENTS) {
this.removeEventListener(type, this.#containEvent, true);
}
for (const type of STRIP_EVENTS) {
window.removeEventListener(type, this.#onStripEvent);
}
clearTimeout(this.#refreshTimer);
this.#refreshTimer = null;
this.library?.style.removeProperty("--zen-library-content-width");
}
updated(changedProperties) {
super.updated(changedProperties);
this.#fillStrips();
this.#updateLibraryWidth();
for (const card of this.#cards) {
this.#updateScrollBorders(card.querySelector(".zen-library-space-body"));
}
if (changedProperties.has("renaming") && this.renaming) {
const input = this.querySelector(".zen-library-space-name-input");
input?.focus();
input?.select();
}
}
#updateLibraryWidth() {
const side = this.library?.querySelector("#zen-library-side");
const list = this.querySelector(".zen-library-spaces");
if (!side || !list) {
return;
}
const sideWidth = window.windowUtils.getBoundsWithoutFlushing(side).width;
this.library.style.setProperty(
"--zen-library-content-width",
`${sideWidth + list.scrollWidth}px`
);
}
get #cards() {
return [...this.querySelectorAll(".zen-library-space")];
}
/**
* Marks a strip as scrolled past its top or bottom edge so the matching
* edge border shows.
*
* @param {Element|null} body - A card's scrolling body
*/
#updateScrollBorders(body) {
if (!body) {
return;
}
const max = body.scrollHeight - body.clientHeight;
body.toggleAttribute("scrolled-top", body.scrollTop > 1);
body.toggleAttribute(
"scrolled-bottom",
max > 1 && body.scrollTop < max - 1
);
}
/**
* Rebuilds copies shortly, once a burst of changes has settled.
*
* @param {string|null} uuid - The space that changed, or null for all
*/
#scheduleStripsRefresh(uuid) {
if (this.#pendingSpaces) {
if (uuid) {
this.#pendingSpaces.add(uuid);
} else {
this.#pendingSpaces = null;
}
}
if (this.#refreshTimer) {
return;
}
this.#refreshTimer = setTimeout(() => {
this.#refreshTimer = null;
const uuids = this.#pendingSpaces;
this.#pendingSpaces = new Set();
this.#refreshStrips(uuids);
}, 100);
}
/**
* Rebuilds copies right away, unless a dropped tab is mid-landing.
*
* @param {Set<string>|null} uuids - The spaces to rebuild, or null for every
* card
*/
#refreshStrips(uuids = null) {
if (this.#landing) {
return;
}
this.#fillStrips(true, uuids);
}
/**
* @param {boolean} rebuild - Redo copies that already exist, not only
* fill in cards that have none yet
* @param {Set<string>|null} uuids - When rebuilding, limit to these spaces.
*/
#fillStrips(rebuild = false, uuids = null) {
for (const card of this.#cards) {
if (rebuild && uuids && !uuids.has(card.dataset.uuid)) {
continue;
}
const strip = card.querySelector(".zen-library-space-tabs");
if (!rebuild && strip.childElementCount) {
continue;
}
const before = rebuild ? this.#rowPositions(strip) : null;
strip.textContent = "";
const space = gZenWorkspaces.workspaceElement(card.dataset.uuid);
if (!space) {
continue;
}
for (const section of [space.pinnedTabsContainer, space.tabsContainer]) {
this.#appendCopy(strip, section);
}
// Rows slide to their new places, unless the drag image is landing on
// the moved one, which is motion enough.
if (before && !this.#landing) {
this.#animateRows(strip, before);
}
}
}
/**
* 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.
*
* @param {Element} strip
* @returns {Map<string, {left: number, top: number}>}
*/
#rowPositions(strip) {
const positions = new Map();
for (const row of strip.querySelectorAll(
"tab, .tab-group-label-container"
)) {
const id = row.id || row.closest("[id]")?.id;
if (id) {
const rect = row.getBoundingClientRect();
positions.set(id, { left: rect.left, top: rect.top });
}
}
return positions;
}
/**
* Slides each rebuilt row from where its old copy was to where it is now.
*
* @param {Element} strip
* @param {Map<string, {left: number, top: number}>} before - Old positions
*/
#animateRows(strip, before) {
if (gReduceMotion) {
return;
}
for (const row of strip.querySelectorAll(
"tab, .tab-group-label-container"
)) {
const id = row.id || row.closest("[id]")?.id;
const old = id && before.get(id);
if (!old) {
continue;
}
const rect = row.getBoundingClientRect();
const dx = old.left - rect.left;
const dy = old.top - rect.top;
if (dx || dy) {
gZenUIManager.elementAnimate(
row,
{ x: [dx, 0], y: [dy, 0] },
{ duration: 180, easing: "ease-out" }
);
}
}
}
/**
* Copies must never be mistaken for the originals by id lookups, so every
* id in a copy gets a suffix.
*
* @param {Element} copy - The copied element, before it is connected
*/
#renameIds(copy) {
for (const element of [copy, ...copy.querySelectorAll("[id]")]) {
if (element.id) {
element.id = `${element.id}-copy`;
}
}
}
#appendCopy(container, node) {
// A closing tab stays in the strip through its animation, but is gone.
if (
node.nodeType !== Node.ELEMENT_NODE ||
node.hasAttribute("hidden") ||
node.closing
) {
return;
}
if (gBrowser.isTab(node)) {
const copy = node.cloneNode(false);
this.#renameIds(copy);
for (const attribute of [
"selected",
"visuallyselected",
"multiselected",
]) {
copy.removeAttribute(attribute);
}
container.appendChild(copy);
this.#realElements.set(copy, node);
if (node.glanceTab) {
this.#appendCopy(copy.querySelector(".tab-content"), node.glanceTab);
}
return;
}
if (GROUP_TAGS.includes(node.localName)) {
const copy = node.cloneNode(false);
this.#renameIds(copy);
container.appendChild(copy);
this.#realElements.set(copy, node);
const icon = copy.querySelector(".tab-group-folder-icon");
const realIcon = node.querySelector(".tab-group-folder-icon");
if (icon && realIcon) {
icon.replaceChildren(
...[...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);
}
}
return;
}
if (!node.querySelector("tab, tab-group, zen-folder")) {
const copy = node.cloneNode(true);
this.#renameIds(copy);
container.appendChild(copy);
return;
}
const copy = node.cloneNode(false);
this.#renameIds(copy);
container.appendChild(copy);
for (const child of node.children) {
this.#appendCopy(copy, child);
}
}
#containEvent = event => {
event.stopPropagation();
};
#containHover = {
handleEvent: event => event.stopPropagation(),
capture: true,
};
#onStripClick = event => {
const label = event.target.closest(".tab-group-label-container");
if (label) {
const copy = label.closest(GROUP_TAGS.join());
const group = this.#realElements.get(copy);
if (group) {
event.stopPropagation();
const collapsed = !group.collapsed;
group.collapsed = collapsed;
copy.collapsed = collapsed;
if (collapsed) {
gZenFolders.animateCollapse(copy);
} else {
gZenFolders.animateExpand(copy);
}
}
return;
}
const tab = this.#realElements.get(event.target.closest("tab"));
if (!tab) {
return;
}
event.stopPropagation();
if (event.target.closest(".tab-close-button")) {
gBrowser.removeTab(tab, { animate: true });
return;
}
gBrowser.selectedTab = tab;
this.library?.constructor.toggle();
};
/**
* @param {Element} copy - A copied tab, group or folder
* @returns {Element|undefined} The real element it stands for
*/
realElementFor(copy) {
return this.#realElements.get(copy);
}
/**
* Rebuilds every card's copies immediately, so a just-moved tab's new copy
* exists to read or animate.
*/
refreshStripsNow() {
this.#fillStrips(true);
}
beginTabLanding(tab) {
this.#landing = true;
this.refreshStripsNow();
const copy = this.copyForTab(tab);
if (!copy?.isConnected) {
return null;
}
const rect = copy.getBoundingClientRect();
return rect.width && rect.height ? copy : null;
}
copyForTab(tab) {
for (const card of this.#cards) {
for (const copy of card.querySelectorAll(".zen-library-space-tabs tab")) {
if (this.#realElements.get(copy) === tab) {
return copy;
}
}
}
return null;
}
#onStripDragStart = event => {
const target =
event.target.nodeType === Node.TEXT_NODE
? event.target.parentElement
: event.target;
const copy = target.closest("tab");
if (copy) {
this.#dnd.startTabDrag(event, copy);
copy.addEventListener("dragend", this.#onCopyDragEnd, { once: true });
}
};
#onStripDrop = event => {
this.#dnd.handle_drop(event);
this.#refreshStrips();
};
#renderStrip() {
return html`
<div class="zen-library-space-body" zen-sidebar-expanded="true">
<div
class="zen-library-space-tabs"
id="tabbrowser-tabs"
orient="vertical"
expanded="true"
@click=${this.#onStripClick}
@mouseover=${this.#containHover}
@mouseout=${this.#containHover}
@dragstart=${this.#onStripDragStart}
@dragover=${event => this.#dnd.handle_dragover(event)}
@dragleave=${event => this.#dnd.handle_dragleave(event)}
@drop=${this.#onStripDrop}
></div>
</div>
`;
}
// Theme
#themeStyles(workspace) {
const { gradient, isDarkMode, isExplicitMode, toolbarColor, primaryColor } =
gZenThemePicker.getGradientForWorkspace(workspace);
let colorScheme = "";
if (isExplicitMode) {
colorScheme = isDarkMode ? "dark" : "light";
}
return {
"--zen-library-space-gradient": gradient,
"--zen-primary-color": primaryColor,
"--toolbox-textcolor": `rgba(${toolbarColor.join(",")})`,
colorScheme,
};
}
#openThemePicker(workspace, event) {
gZenThemePicker.openThemePickerForWorkspace(
workspace,
event.currentTarget,
event
);
}
#openActions(event) {
document
.getElementById("zenWorkspaceMoreActions")
.openPopup(event.currentTarget, "after_end");
}
// Icon and name
#changeIcon(workspace, event) {
gZenEmojiPicker.open(event.currentTarget, {
closeOnSelect: false,
allowNone: gZenWorkspaces.workspaceHasIcon(workspace),
onSelect: async icon => {
workspace.icon = icon;
await gZenWorkspaces.saveWorkspace(workspace);
},
});
}
#commitRename(workspace, event) {
if (this.renaming !== workspace.uuid) {
return;
}
const newName = event.target.value.trim();
this.renaming = null;
if (newName && newName !== workspace.name) {
workspace.name = newName;
gZenWorkspaces.saveWorkspace(workspace);
}
}
#onRenameKeyDown(workspace, event) {
if (event.key === "Enter") {
this.#commitRename(workspace, event);
} else if (event.key === "Escape") {
this.renaming = null;
}
}
#onPointerDown(event) {
if (event.button !== 0) {
return;
}
const cards = this.#cards;
const card = event.currentTarget.closest(".zen-library-space");
this.#dragIndex = cards.indexOf(card);
this.#dropIndex = this.#dragIndex;
this.#dragStartX = event.clientX;
this.#lastPointerX = event.clientX;
this.#dragStartScroll = this.#list.scrollLeft;
const cardRect = window.windowUtils.getBoundsWithoutFlushing(card);
this.#dragCenterX = cardRect.left + cardRect.width / 2;
this.#slotCenters = cards
.filter((other, i) => i !== this.#dragIndex)
.map(other => {
const rect = window.windowUtils.getBoundsWithoutFlushing(other);
return rect.left + rect.width / 2;
});
card.setAttribute("dragging", "true");
event.currentTarget.setPointerCapture(event.pointerId);
event.preventDefault();
this.#autoScroll();
}
get #list() {
return this.querySelector(".zen-library-spaces");
}
#onPointerMove(event) {
if (this.#dragIndex === -1) {
return;
}
this.#lastPointerX = event.clientX;
this.#updateDrag();
}
#updateDrag() {
const cards = this.#cards;
const scrolled = this.#list.scrollLeft - this.#dragStartScroll;
const travel = this.#lastPointerX - this.#dragStartX + scrolled;
cards[this.#dragIndex].style.translate = `${travel}px 0`;
const centerX = this.#dragCenterX + travel;
const index = this.#slotCenters.filter(center => centerX > center).length;
if (index !== this.#dropIndex) {
this.#dropIndex = index;
this.#shiftCards(cards);
}
}
#autoScroll() {
if (this.#dragIndex === -1) {
return;
}
const list = this.#list;
const rect = window.windowUtils.getBoundsWithoutFlushing(list);
let step = 0;
if (this.#lastPointerX < rect.left + SCROLL_EDGE_PX) {
step = -SCROLL_STEP_PX;
} else if (this.#lastPointerX > rect.right - SCROLL_EDGE_PX) {
step = SCROLL_STEP_PX;
}
if (step) {
const before = list.scrollLeft;
list.scrollLeft += step;
if (list.scrollLeft !== before) {
this.#updateDrag();
}
}
this.#scrollFrame = requestAnimationFrame(() => this.#autoScroll());
}
#shiftCards(cards) {
cards.forEach((card, i) => {
let shift = "";
if (this.#dragIndex < i && i <= this.#dropIndex) {
shift = "left";
} else if (this.#dropIndex <= i && i < this.#dragIndex) {
shift = "right";
}
if (shift) {
card.setAttribute("shift", shift);
} else {
card.removeAttribute("shift");
}
});
}
async #onPointerUp(event, workspace) {
if (this.#dragIndex === -1) {
return;
}
event.currentTarget.releasePointerCapture(event.pointerId);
cancelAnimationFrame(this.#scrollFrame);
this.#scrollFrame = null;
const moved =
this.#dropIndex !== this.#dragIndex && event.type === "pointerup";
const dropIndex = this.#dropIndex;
this.#dragIndex = -1;
this.#dropIndex = -1;
const list = this.#list;
list.setAttribute("no-transition", "true");
if (moved) {
gZenWorkspaces.reorderWorkspace(workspace.uuid, dropIndex);
await this.updateComplete;
}
for (const card of this.#cards) {
card.removeAttribute("dragging");
card.removeAttribute("shift");
card.style.translate = "";
}
await new Promise(resolve => requestAnimationFrame(resolve));
list.removeAttribute("no-transition");
}
#renderIcon(workspace) {
const hasIcon = gZenWorkspaces.workspaceHasIcon(workspace);
let content = "";
if (hasIcon) {
const icon = gZenWorkspaces.getWorkspaceIcon(workspace);
content = icon.endsWith(".svg") ? html`<img src=${icon} alt="" />` : icon;
}
return html`
<button
class="zen-library-space-icon"
?no-icon=${!hasIcon}
data-l10n-id="library-spaces-icon-button"
@click=${event => this.#changeIcon(workspace, event)}
>
${content}
</button>
`;
}
#renderName(workspace) {
if (this.renaming === workspace.uuid) {
return html`
<input
class="zen-library-space-name-input"
.value=${workspace.name}
@keydown=${event => this.#onRenameKeyDown(workspace, event)}
@blur=${event => this.#commitRename(workspace, event)}
/>
`;
}
return html`
<button
class="zen-library-space-name"
data-l10n-id="library-spaces-name-button"
@click=${() => {
this.renaming = workspace.uuid;
}}
>
${workspace.name}
</button>
`;
}
#renderSpace(workspace) {
return html`
<div
class="zen-library-space zen-squircle-before"
data-uuid=${workspace.uuid}
style=${styleMap(this.#themeStyles(workspace))}
?active=${workspace.uuid === gZenWorkspaces.activeWorkspace}
>
<div class="zen-library-space-header">
${this.#renderIcon(workspace)} ${this.#renderName(workspace)}
<toolbarbutton
class="toolbarbutton-1"
data-l10n-id="library-spaces-theme-button"
@click=${event => this.#openThemePicker(workspace, event)}
>
<img
class="toolbarbutton-icon"
src="chrome://browser/skin/zen-icons/paintbrush-fill.svg"
alt=""
/>
</toolbarbutton>
</div>
${this.#renderStrip()}
<div class="zen-library-space-footer">
<toolbarbutton
class="toolbarbutton-1 zen-library-space-handle"
data-l10n-id="library-spaces-move-button"
@pointerdown=${this.#onPointerDown}
@pointermove=${this.#onPointerMove}
@pointerup=${event => this.#onPointerUp(event, workspace)}
@pointercancel=${event => this.#onPointerUp(event, workspace)}
>
<img
class="toolbarbutton-icon"
src="chrome://browser/skin/zen-icons/drag-indicator.svg"
draggable="false"
alt=""
/>
</toolbarbutton>
<toolbarbutton
class="toolbarbutton-1 zen-library-space-actions"
zen-workspace-id=${workspace.uuid}
data-l10n-id="library-spaces-actions-button"
@click=${this.#openActions}
>
<img
class="toolbarbutton-icon"
src="chrome://global/skin/icons/more.svg"
alt=""
/>
</toolbarbutton>
</div>
</div>
`;
}
render() {
return html`
<div class="zen-library-spaces">
${repeat(
gZenWorkspaces.getWorkspaces(),
workspace => workspace.uuid,
workspace => this.#renderSpace(workspace)
)}
</div>
`;
}
}
customElements.define("zen-library-spaces-section", ZenLibrarySpacesSection);

View File

@@ -0,0 +1,984 @@
/*
* 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-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));
}
#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;
}
}
: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;
}
zen-library {
--zen-library-pill-bg: color-mix(in srgb, currentColor 7%, transparent);
--zen-library-pill-hover-bg: color-mix(in srgb, currentColor 12%, transparent);
--zen-library-radius: 12px;
contain: content;
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: clamp(418px, var(--zen-library-content-width, 0px), 95vw);
display: flex;
z-index: 9;
color: var(--toolbar-color);
color-scheme: var(--toolbar-color-scheme);
visibility: hidden;
pointer-events: none;
transform: translateX(calc(-100% * (1 - var(--library-progress, 0))));
& #zen-library-panel {
flex-direction: row;
}
& #zen-library-side {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 110px;
height: 100%;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.08));
box-shadow: 9px 0 17px rgba(0, 0, 0, 0.04);
}
}
:root[zen-right-side="true"] zen-library {
transform: translateX(calc(100% * (1 - var(--library-progress, 0))));
left: auto;
right: 0;
& #zen-library-panel {
flex-direction: row-reverse;
}
& #zen-library-side {
background: linear-gradient(to left, transparent, rgba(255, 255, 255, 0.08));
box-shadow: -9px 0 17px rgba(0, 0, 0, 0.04);
}
& #zen-library-footer {
flex-direction: row-reverse;
& toolbarbutton:first-child {
rotate:180deg;
}
}
}
zen-library[open] {
--zen-main-app-child-z-index: 2;
visibility: visible;
pointer-events: auto;
-moz-window-dragging: drag;
transition: width 0.3s cubic-bezier(.36,.06,0,.94);
}
zen-library[transitioning] #zen-library-content {
pointer-events: none;
}
#zen-library-panel {
position: absolute;
inset: 0;
height: 100%;
display: flex;
flex-direction: column;
}
#zen-library-content {
flex-grow: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
contain: content;
}
#zen-library-footer {
display: flex;
padding: 8px;
}
@media (-moz-platform: macos) {
#zen-library-header {
padding: 12px 0 0 9px;
& .titlebar-buttonbox-container {
margin-left: 0 !important;
}
}
}
#zen-library-footer {
justify-content: space-between;
& .toolbarbutton-1 {
appearance: none;
& .toolbarbutton-text {
display: none;
}
}
}
#zen-library-body {
padding: 16px;
}
#zen-library-sidebar-tabs {
display: flex;
flex-direction: column;
gap: 8px;
justify-content: space-between;
height: min-content;
}
.zen-library-tab {
--zen-library-sprite-size: 28px;
-moz-window-dragging: no-drag;
margin: 8px;
display: flex;
position: relative;
justify-content: center;
align-items: center;
padding: 16px 0;
font-weight: 600;
flex-direction: column;
gap: 4px;
& label {
text-align: center;
}
isolation: isolate;
&::before {
content: "";
position: absolute;
inset: 0;
z-index: -2;
border-radius: 8px;
transition: transform 0.1s ease-in-out;
}
&:hover::before {
background: color-mix(in srgb, currentColor 5%, transparent);
}
&[active]::before {
background: color-mix(in srgb, currentColor 10%, transparent);
}
&:hover:active::before {
transform: scale(0.96);
}
& .zen-library-tab-icon {
width: var(--zen-library-sprite-size);
height: var(--zen-library-sprite-size);
overflow: clip;
}
@media (prefers-reduced-motion: no-preference) {
&[animate] .zen-library-tab-icon-image {
animation: zen-library-sprite-play 0.583s steps(36, jump-none);
}
}
}
@media (max-height: 275px) {
.zen-library-tab {
& label {
display: none;
}
}
}
.zen-library-tab-icon-image,
.empty-state-icon-image {
width: calc(36 * var(--zen-library-sprite-size));
height: var(--zen-library-sprite-size);
background-size: 100% 100%;
--stroke: light-dark(var(--zen-colors-primary), var(--zen-accent-button-color));
fill: color-mix(in srgb, var(--stroke), light-dark(white, black) 70%);
stroke: var(--stroke);
-moz-context-properties: fill, stroke;
transform: translateX(0);
}
.zen-library-tab:not([active]) .zen-library-tab-icon-image {
fill: transparent;
}
[data-section="history"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
background-image: url("chrome://browser/skin/zen-icons/library/library-history-sprite.svg");
}
[data-section="downloads"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
background-image: url("chrome://browser/skin/zen-icons/library/library-downloads-sprite.svg");
}
zen-library[zen-library-downloading] .zen-library-tab[data-section="downloads"]::after {
--border-color: color-mix(in srgb, var(--zen-primary-color), light-dark(rgba(0,0,0,.8), white) 70%);
content: "";
position: absolute;
z-index: -1;
top: 16px;
left: 50%;
translate: -50% 0;
width: var(--zen-library-sprite-size);
height: var(--zen-library-sprite-size);
box-sizing: border-box;
border: 3px solid transparent;
border-radius: 100%;
background:
conic-gradient(
var(--zen-accent-button-background) var(--zen-library-download-progress, 0%),
var(--border-color) 0
)
border-box,
color-mix(in srgb, var(--zen-primary-color), light-dark(white, rgba(0,0,0,.5)) 80%)
padding-box;
box-shadow: 0 0 6px 2px rgba(0,0,0,.1);
pointer-events: none;
}
[data-section="boosts"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
background-image: url("chrome://browser/skin/zen-icons/library/library-boosts-sprite.svg");
}
[data-section="spaces"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
background-image: url("chrome://browser/skin/zen-icons/library/library-spaces-sprite.svg");
}
[data-section="media"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
background-image: url("chrome://browser/skin/zen-icons/library/library-media-sprite.svg");
}
@keyframes zen-library-sprite-play {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(35 * -1 * var(--zen-library-sprite-size)));
}
}
.zen-library-section {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
font-size: 14px;
&:not([data-section="spaces"]) {
max-width: calc(418px - 110px);
}
}
zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-header, .zen-library-filter-panel-inner, .zen-library-search-results) {
transition: none;
}
.zen-library-search-top {
display: grid;
grid-template-columns: 1fr;
padding: 12px 14px 8px;
-moz-window-dragging: no-drag;
}
.zen-library-search-header,
.zen-library-filter-header {
grid-area: 1 / 1;
display: flex;
align-items: center;
gap: 8px;
min-width: 1px;
transition: opacity 0.28s ease;
}
.zen-library-search-top[open] .zen-library-search-header,
.zen-library-search-top:not([open]) .zen-library-filter-header {
opacity: 0;
}
.zen-library-search-box,
.zen-library-filter-button {
display: flex;
align-items: center;
gap: 8px;
box-sizing: border-box;
height: 36px;
padding-inline: 12px;
border-radius: var(--zen-library-radius);
background: var(--zen-library-pill-bg);
}
.zen-library-filter-button,
.zen-library-filter-done,
.zen-library-filter-chip {
border: none;
color: inherit;
font: inherit;
&:hover {
background: var(--zen-library-pill-hover-bg);
}
}
.zen-library-search-box img,
.zen-library-filter-button img,
.zen-library-filter-chip img {
width: 16px;
height: 16px;
-moz-context-properties: fill;
fill: currentColor;
opacity: 0.8;
}
.zen-library-search-box {
flex: 1;
min-width: 0;
& img {
opacity: 0.5;
}
& input {
flex: 1;
min-width: 0;
height: 100%;
padding: 0;
background: none;
border: none;
outline: none;
color: inherit;
font: inherit;
&::placeholder {
color: inherit;
opacity: 0.5;
}
}
}
.zen-library-filter-button {
font-weight: 600;
}
.zen-library-filter-header {
justify-content: space-between;
& h2 {
margin: 0;
font-size: 1em;
font-weight: 700;
}
}
.zen-library-filter-done {
padding: 8px 14px;
border-radius: var(--zen-library-radius);
background: var(--zen-library-pill-bg);
font-weight: 600;
}
.zen-library-filter-panel {
grid-area: 2 / 1;
height: 0;
overflow: visible;
}
.zen-library-filter-panel-inner {
opacity: 0;
transition: opacity 0.3s ease;
.zen-library-search-top[open] & {
opacity: 1;
}
}
.zen-library-filter-group h3 {
margin: 16px 0 10px;
font-size: 0.93em;
font-weight: 600;
opacity: 0.85;
}
.zen-library-filter-options {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding-bottom: 6px;
}
.zen-library-filter-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 9px 14px;
border-radius: var(--zen-library-radius);
background: var(--zen-library-pill-bg);
&[active] {
background: color-mix(in srgb, currentColor 20%, transparent);
}
}
.zen-library-search-results {
flex: 1;
min-height: 0;
position: relative;
z-index: 1;
overflow-y: auto;
padding: 0 10px 10px;
border-top: 1px solid transparent;
transition:
transform 0.3s ease,
border-color 0.2s ease;
-moz-window-dragging: no-drag;
&[scrolled] {
border-top-color: color-mix(in srgb, currentColor 15%, transparent);
}
.zen-library-search-top[open] + & {
transform: translateY(var(--zen-library-filter-height, 0));
}
}
.zen-library-search-sentinel {
height: 1px;
}
.zen-library-empty {
padding: 24px 0;
text-align: center;
opacity: 0.6;
}
.zen-library-group h3 {
margin: 10px 8px 5px;
font-size: 12px;
font-weight: 700;
opacity: 0.6;
}
.zen-library-row {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 12px;
border-radius: 16px;
/* The open library window is draggable; let rows be dragged as items. */
-moz-window-dragging: no-drag;
&:hover {
background: color-mix(in srgb, currentColor 10%, transparent);
}
}
.zen-library-row-icon {
width: 16px;
height: 16px;
border-radius: 4px;
margin-inline: 2px 1px;
-moz-context-properties: fill;
fill: currentColor;
zen-library-downloads-section & {
width: 22px;
height: 22px;
border-radius: 5px;
}
}
.zen-library-row-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
& span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.zen-library-row-title {
font-size: 14px;
font-weight: 500;
}
.zen-library-row-subtitle {
font-size: 12px;
opacity: 0.6;
}
.zen-library-row-actions {
display: flex;
gap: 4px;
/* Shown while the row is hovered; outranks the library's button rule. */
#zen-library-panel & toolbarbutton {
display: none;
.zen-library-row:is(:hover, [menu-open]) & {
display: inline-flex;
}
}
}
#zen-library-panel .toolbarbutton-1 {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
padding: 0 var(--toolbarbutton-padding-outer);
border: none;
background: none;
& > .toolbarbutton-icon {
display: block;
box-sizing: content-box;
width: 16px;
height: 16px;
padding: var(--toolbarbutton-padding-inner);
border-radius: var(--toolbarbutton-border-radius);
-moz-context-properties: fill;
fill: var(--toolbarbutton-icon-fill);
}
&:not([disabled]):hover > .toolbarbutton-icon {
background-color: var(--toolbarbutton-background-color-hover);
}
&:not([disabled]):is(:hover:active, [open]) > .toolbarbutton-icon {
background-color: var(--toolbarbutton-background-color-active);
}
.zen-library-row-actions & > .toolbarbutton-icon {
width: 18px;
height: 18px;
padding: 8px;
border-radius: var(--zen-library-radius);
}
}
/* Boosts */
.zen-library-row-actions moz-toggle {
--toggle-width: 44px;
--toggle-height: 24px;
--toggle-dot-margin: 2px;
--toggle-border-width: 0px;
--toggle-background-color: color-mix(in srgb, var(--zen-primary-color) 15%, transparent);
--toggle-background-color-hover: color-mix(in srgb, var(--zen-primary-color) 25%, transparent);
--toggle-background-color-active: var(--toggle-background-color-hover);
--toggle-dot-background-color: light-dark(white, rgb(235, 235, 235));
display: none;
.zen-library-row:is(:hover, [menu-open]) & {
display: block;
}
}
.zen-library-boost-row {
padding: 6px 12px 6px 4px;
border-radius: 18px;
}
.zen-library-boost-icon {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 46px;
height: 46px;
margin-inline: 2px 1px;
&::before {
content: "";
position: absolute;
inset: 0;
box-sizing: border-box;
border-radius: 10px;
border: 3px solid white;
background: #edededaa;
box-shadow: 0 0 6px 1px rgba(0, 0, 0, 0.1);
}
& img {
position: relative;
width: 24px;
height: 24px;
object-fit: contain;
-moz-context-properties: fill;
fill: currentColor;
}
}
.zen-library-boost-row[disabled] {
& .zen-library-row-text,
& .zen-library-boost-icon img {
opacity: 0.5;
}
& .zen-library-boost-icon {
&::before,
& img {
mask-image: linear-gradient(45deg, black calc(50% - 5px), transparent calc(50% - 5px), transparent calc(50% + 5px), black calc(50% + 5px));
mask-size: 200% 200%;
mask-position: center;
mask-repeat: no-repeat;
mask-clip: no-clip;
}
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 135%;
height: 4px;
border-radius: 10px;
background: light-dark(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.5));
transform: translate(-50%, -50%) rotate(45deg);
pointer-events: none;
}
}
}
.zen-library-download-url {
display: none;
}
.zen-library-row:hover:not([opening], [pending]) {
& .zen-library-download-status {
display: none;
}
& .zen-library-download-url {
display: inline;
}
}
.zen-library-row[pending]:not(:hover) .zen-library-download-status {
display: none;
}
.zen-library-row[pending]:not(:hover) .zen-library-row-subtitle::before {
content: "";
display: block;
height: 4px;
margin-block: calc((1lh - 4px) / 2);
border-radius: 2px;
background:
linear-gradient(var(--color-accent-primary), var(--color-accent-primary)) 0 0 / var(--zen-library-download-progress, 0%) 100% no-repeat,
color-mix(in srgb, currentColor 12%, transparent);
transition: background-size 0.2s ease;
}
.zen-library-row[paused] .zen-library-row-subtitle::before {
background-image: linear-gradient(
color-mix(in srgb, var(--color-accent-primary) 50%, transparent),
color-mix(in srgb, var(--color-accent-primary) 50%, transparent)
);
}
.zen-library-row[indeterminate] .zen-library-row-subtitle::before {
background-size: 40% 100%;
transition: none;
animation: zen-library-progress-indeterminate 1.2s ease-in-out infinite;
}
@keyframes zen-library-progress-indeterminate {
from {
background-position-x: -70%;
}
to {
background-position-x: 170%;
}
}
.zen-library-spaces {
--zen-library-space-width: 242px;
--zen-library-space-gap: 24px;
flex: 1;
min-height: 0;
display: flex;
gap: var(--zen-library-space-gap);
padding: 45px 25px;
overflow-x: auto;
-moz-window-dragging: no-drag;
user-select: none;
}
.zen-library-space {
position: relative;
isolation: isolate;
display: flex;
flex-direction: column;
flex: 0 0 var(--zen-library-space-width);
/* The shift transforms assume the card is exactly this wide. */
box-sizing: border-box;
min-height: 0;
color: var(--toolbox-textcolor);
transition:
translate 0.2s ease,
opacity 0.2s ease;
background: var(--zen-branding-bg);
border-radius: 14px;
--toolbar-color: var(--toolbox-textcolor);
&::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: 14px;
background: var(--zen-library-space-gradient);
background-blend-mode: screen;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
&[dragging] {
z-index: 1;
&::before {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
}
}
&[dragging],
.zen-library-spaces[no-transition] & {
transition: none;
}
&[shift="left"] {
translate: calc(-1 * (var(--zen-library-space-width) + var(--zen-library-space-gap))) 0;
}
&[shift="right"] {
translate: calc(var(--zen-library-space-width) + var(--zen-library-space-gap)) 0;
}
}
.zen-library-space-header,
.zen-library-space-footer {
display: flex;
align-items: center;
gap: 8px;
}
.zen-library-space-header {
position: relative;
margin: 18px 14px 6px;
font-size: 13px;
font-weight: 500;
opacity: 0.85;
height: 28px;
&::before {
content: "";
position: absolute;
inset: -4px -6px;
z-index: -1;
border-radius: var(--border-radius-medium);
pointer-events: none;
}
&:hover::before {
background: var(--tab-background-color-hover);
}
}
.zen-library-space-body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-width: none;
--zen-library-scroll-border: color-mix(
in srgb,
var(--toolbox-textcolor) 15%,
transparent
);
border-block: 1px solid transparent;
transition:
border-top-color 0.15s ease,
border-bottom-color 0.15s ease;
&[scrolled-top] {
border-top-color: var(--zen-library-scroll-border);
}
&[scrolled-bottom] {
border-bottom-color: var(--zen-library-scroll-border);
}
#zen-library-panel &[zen-sidebar-expanded] {
min-width: 0;
padding: 0;
}
}
.zen-library-space-tabs {
display: flex;
flex-direction: column;
flex: 0 0 auto !important;
&::after,
& #tabbrowser-arrowscrollbox-periphery-copy {
display: none !important;
}
& * {
animation: none !important;
}
/* The strip's drop indicator is placed in here so it scrolls along. */
position: relative;
& > #zen-drag-indicator {
position: absolute;
}
& .tab-group-label-container[dragover] {
background: color-mix(in srgb, var(--zen-primary-color) 25%, transparent);
}
& .zen-current-workspace-indicator {
min-height: var(--tab-min-height);
}
}
.zen-library-space-footer {
justify-content: space-between;
padding: 8px 14px 14px;
opacity: 0.7;
}
.zen-library-space-icon,
.zen-library-space-name,
.zen-library-space-name-input {
margin: 0;
padding: 0;
border: none;
background: transparent;
color: inherit;
font: inherit;
appearance: none;
}
.zen-library-space-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 6px;
font-size: 15px;
-moz-context-properties: fill;
fill: currentColor;
&:hover {
background: color-mix(in srgb, currentColor 14%, transparent);
}
& img {
width: 16px;
height: 16px;
}
&[no-icon] {
border: 1px dashed light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.5));
box-sizing: border-box;
position: absolute;
inset-inline-start: 0;
opacity: 0;
transition: opacity 0.15s ease;
.zen-library-space-header:hover & {
opacity: 1;
}
}
}
.zen-library-space-name,
.zen-library-space-name-input {
flex: 1;
min-width: 0;
text-align: start;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.zen-library-space-icon[no-icon] ~ :is(.zen-library-space-name, .zen-library-space-name-input) {
transition:
translate 0.15s ease,
margin 0.15s ease;
.zen-library-space-header:hover & {
translate: 28px 0;
margin-inline-end: 28px;
}
}
.zen-library-space-name-input {
width: 100%;
height: auto;
line-height: inherit;
outline: none;
box-shadow: none;
border-radius: 0;
-moz-appearance: none;
}
.zen-library-space-handle {
cursor: grab;
touch-action: none;
.zen-library-space[dragging] & {
cursor: grabbing;
}
}

View File

@@ -12,6 +12,7 @@ DIRS += [
"compact-mode",
"drag-and-drop",
"glance",
"library",
"live-folders",
"mods",
"tests",

View File

@@ -383,6 +383,12 @@ class nsZenWindowSync {
handleEvent(aEvent) {
const window = aEvent.currentTarget.documentGlobal ?? aEvent.currentTarget;
// The library builds copies of tabs and folders that fire these same
// events while being built. Their ids are suffixed with "-copy" (see
// ZenLibrarySpacesSection#renameIds), so never sync anything from one.
if (aEvent.target?.id?.endsWith("-copy")) {
return;
}
if (
!window.gZenStartup.isReady ||
!window.gZenWorkspaces?.shouldHaveWorkspaces ||

View File

@@ -1542,14 +1542,21 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
browser.gZenThemePicker.invalidateGradientCache(uuid);
}
// Do not rebuild if the workspace is not the same as the current one
// Only rebuild for the workspace the window shows, or the one its
// picker is editing without showing it.
const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace();
if (windowWorkspace.uuid !== uuid) {
const appliesToWindow = windowWorkspace.uuid === uuid;
if (
!appliesToWindow &&
browser.gZenThemePicker.editingWorkspaceId !== uuid
) {
return;
}
// get the theme from the window
workspaceTheme = this.fixTheme(theme || windowWorkspace.theme);
workspaceTheme = this.fixTheme(
theme || (appliesToWindow ? windowWorkspace : workspace).theme
);
const docElement = browser.document.documentElement;
if (!skipUpdate) {
@@ -1560,7 +1567,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
}
}
if (theme) {
if (theme && appliesToWindow) {
const workspaceElement = browser.gZenWorkspaces.workspaceElement(
windowWorkspace.uuid
);
@@ -1569,7 +1576,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
}
}
if (!skipUpdate) {
if (!skipUpdate && appliesToWindow) {
let backgroundElement =
browser.gZenThemePicker.browserBackgroundElement;
let toolbarElement = browser.gZenThemePicker.toolbarBackgroundElement;
@@ -1724,7 +1731,9 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
workspaceTheme.gradientColors,
true
);
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
if (appliesToWindow) {
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
}
browser.gZenThemePicker.customColorList.innerHTML = "";
for (const dot of workspaceTheme.gradientColors) {
@@ -1733,59 +1742,66 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
}
}
browser.gZenThemePicker.toolbarBackgroundElement.style.setProperty(
"--zen-main-browser-background-toolbar",
gradientToolbar
);
browser.gZenThemePicker.browserBackgroundElement.style.setProperty(
"--zen-main-browser-background",
gradient
);
const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
if (isDefaultTheme) {
docElement.setAttribute("zen-default-theme", "true");
} else {
docElement.removeAttribute("zen-default-theme");
}
if (dominantColor) {
// Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
// so instead get the pref value directly.
browser.gZenThemePicker.isLegacyVersion =
Services.prefs.getIntPref("zen.theme.gradient-legacy-version", 1) ===
0;
let isDarkMode = isDarkModeWindow;
if (!isDefaultTheme && !this.isLegacyVersion) {
// Check for the primary color
isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
docElement.setAttribute("zen-should-be-dark-mode", isDarkMode);
browser.gZenThemePicker.panel.removeAttribute("invalidate-controls");
if (appliesToWindow) {
browser.gZenThemePicker.toolbarBackgroundElement.style.setProperty(
"--zen-main-browser-background-toolbar",
gradientToolbar
);
browser.gZenThemePicker.browserBackgroundElement.style.setProperty(
"--zen-main-browser-background",
gradient
);
const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
if (isDefaultTheme) {
docElement.setAttribute("zen-default-theme", "true");
} else {
docElement.removeAttribute("zen-should-be-dark-mode");
if (!this.isLegacyVersion) {
browser.gZenThemePicker.panel.setAttribute(
"invalidate-controls",
"true"
);
}
docElement.removeAttribute("zen-default-theme");
}
if (dominantColor) {
// Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
// so instead get the pref value directly.
browser.gZenThemePicker.isLegacyVersion =
Services.prefs.getIntPref(
"zen.theme.gradient-legacy-version",
1
) === 0;
const primaryColor = this.getAccentColorForUI(
dominantColor,
isDarkMode
);
docElement.style.setProperty("--zen-primary-color", primaryColor);
let isDarkMode = isDarkModeWindow;
if (!isDefaultTheme && !this.isLegacyVersion) {
// Check for the primary color
isDarkMode =
browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
docElement.setAttribute("zen-should-be-dark-mode", isDarkMode);
browser.gZenThemePicker.panel.removeAttribute(
"invalidate-controls"
);
} else {
docElement.removeAttribute("zen-should-be-dark-mode");
if (!this.isLegacyVersion) {
browser.gZenThemePicker.panel.setAttribute(
"invalidate-controls",
"true"
);
}
}
// Set `--toolbox-textcolor` to have a contrast with the primary color
let textColor = this.getToolbarColor(isDarkMode, dominantColor);
docElement.style.setProperty(
"--toolbox-textcolor",
`rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
);
docElement.style.setProperty(
"--toolbar-color-scheme",
isDarkMode ? "dark" : "light"
);
const primaryColor = this.getAccentColorForUI(
dominantColor,
isDarkMode
);
docElement.style.setProperty("--zen-primary-color", primaryColor);
// Set `--toolbox-textcolor` to have a contrast with the primary color
let textColor = this.getToolbarColor(isDarkMode, dominantColor);
docElement.style.setProperty(
"--toolbox-textcolor",
`rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
);
docElement.style.setProperty(
"--toolbar-color-scheme",
isDarkMode ? "dark" : "light"
);
}
}
if (!skipUpdate) {
@@ -1878,6 +1894,36 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
}
}
#editingWorkspaceId = null;
get editingWorkspaceId() {
return this.#editingWorkspaceId;
}
get workspaceBeingEdited() {
return (
gZenWorkspaces.getWorkspaceFromId(this.#editingWorkspaceId) ??
gZenWorkspaces.getActiveWorkspace()
);
}
/**
* Opens the picker for a space without switching to it.
*
* @param {object} workspace - The space to edit
* @param {Element} anchor - Element to anchor the panel to
* @param {Event} event - The triggering event
*/
openThemePickerForWorkspace(workspace, anchor, event) {
this.#editingWorkspaceId = workspace.uuid;
this.onWorkspaceChange(workspace);
this.panel.removeAttribute("hidepopovertail");
PanelMultiView.openPopup(this.panel, anchor, {
position: "bottomleft topleft",
triggerEvent: event,
});
}
updateCurrentWorkspace(skipSave = true) {
this.updated = skipSave;
const dots = this.panel.querySelectorAll(".zen-theme-picker-dot");
@@ -1916,7 +1962,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
this.currentOpacity,
this.currentTexture
);
let currentWorkspace = gZenWorkspaces.getActiveWorkspace();
let currentWorkspace = this.workspaceBeingEdited;
currentWorkspace.theme = gradient;
if (!skipSave) {
@@ -1928,6 +1974,10 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
skipSave,
skipSave ? gradient : null
);
if (currentWorkspace.uuid !== gZenWorkspaces.activeWorkspace) {
this.invalidateGradientCache(currentWorkspace.uuid);
Services.obs.notifyObservers(null, "zen-space-gradient-update");
}
}
handlePanelClose() {
@@ -1935,6 +1985,11 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
this.updateCurrentWorkspace(false);
}
this.uninitThemePicker();
if (this.#editingWorkspaceId) {
this.#editingWorkspaceId = null;
this.panel.setAttribute("hidepopovertail", "true");
this.onWorkspaceChange(gZenWorkspaces.getActiveWorkspace());
}
}
handlePanelOpen() {

View File

@@ -12,15 +12,23 @@ ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
return document.getElementById("zen-toolbar-background");
});
ChromeUtils.defineESModuleGetters(
lazy,
{ ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs" },
{ global: "current" }
);
export class ZenSpacesSwipe {
_swipeState = {
isGestureActive: false,
lastDelta: 0,
direction: null,
isSwipingLibrary: false,
beforeLibraryState: 0,
};
constructor() {
this.#attachWorkspaceSwipeGestures(gNavToolbox);
this.attachWorkspaceSwipeGestures(gNavToolbox);
this._popupOpenHandler = this._popupOpenHandler.bind(this);
}
@@ -35,20 +43,39 @@ export class ZenSpacesSwipe {
);
}
#attachWorkspaceSwipeGestures(element) {
#readySwipeOpenLibrary() {
const spaces = gZenWorkspaces.getWorkspaces();
const current = gZenWorkspaces.getActiveWorkspaceFromCache();
const libraryEnabled = Services.prefs.getBoolPref("zen.library.enabled");
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
return (
spaces.indexOf(current) === (libraryOnRight ? spaces.length - 1 : 0) &&
libraryEnabled
);
}
attachWorkspaceSwipeGestures(element) {
const gestureControl = {
_handleSwipeMayStart: this._handleSwipeMayStart.bind(this),
_handleSwipeStart: this._handleSwipeStart.bind(this),
_handleSwipeUpdate: this._handleSwipeUpdate.bind(this),
_handleSwipeEnd: this._handleSwipeEnd.bind(this),
_handleSwipeAnimationEnd: this.onSwipeGestureAnimationEnd.bind(this),
};
element.addEventListener(
"MozSwipeGestureMayStart",
this._handleSwipeMayStart.bind(this),
gestureControl._handleSwipeMayStart,
true
);
element.addEventListener(
"MozSwipeGestureStart",
this._handleSwipeStart.bind(this),
gestureControl._handleSwipeStart,
true
);
element.addEventListener(
"MozSwipeGestureUpdate",
this._handleSwipeUpdate.bind(this),
gestureControl._handleSwipeUpdate,
true
);
@@ -56,15 +83,47 @@ export class ZenSpacesSwipe {
// while MozSwipeGesture is fired immediately after swipe ends.
element.addEventListener(
"MozSwipeGesture",
this._handleSwipeEnd.bind(this),
gestureControl._handleSwipeEnd,
true
);
element.addEventListener(
"MozSwipeGestureEnd",
() => {
this.onSwipeGestureAnimationEnd();
},
gestureControl._handleSwipeAnimationEnd,
true
);
return gestureControl;
}
detachWorkspaceSwipeGestures(element, gestureControl) {
element.removeEventListener(
"MozSwipeGestureMayStart",
gestureControl._handleSwipeMayStart,
true
);
element.removeEventListener(
"MozSwipeGestureStart",
gestureControl._handleSwipeStart,
true
);
element.removeEventListener(
"MozSwipeGestureUpdate",
gestureControl._handleSwipeUpdate,
true
);
// Use MozSwipeGesture instead of MozSwipeGestureEnd because MozSwipeGestureEnd is fired after animation ends,
// while MozSwipeGesture is fired immediately after swipe ends.
element.removeEventListener(
"MozSwipeGesture",
gestureControl._handleSwipeEnd,
true
);
element.removeEventListener(
"MozSwipeGestureEnd",
gestureControl._handleSwipeAnimationEnd,
true
);
}
@@ -115,6 +174,8 @@ export class ZenSpacesSwipe {
isGestureActive: true,
lastDelta: 0,
direction: null,
isSwipingLibrary: false,
beforeLibraryState: 0,
};
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", true);
}
@@ -154,6 +215,55 @@ export class ZenSpacesSwipe {
this._swipeState.direction = delta > 0 ? "left" : "right";
}
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
const libraryOpen = lazy.ZenLibrary.isLibraryOpen;
const couldClose = libraryOpen;
const wantsOpen =
!libraryOpen &&
(libraryOnRight ? translateX < 0 : translateX > 0) &&
this.#readySwipeOpenLibrary();
if (wantsOpen || couldClose || this._swipeState.isSwipingLibrary) {
if (!this._swipeState.isSwipingLibrary) {
this._swipeState.isSwipingLibrary = true;
this._swipeState.beforeLibraryState = libraryOpen ? 1 : 0;
lazy.ZenLibrary.startSwipe();
}
const rubberBand = function (offset, dimension, constant = 0.55) {
if (offset === 0 || dimension === 0) {
return 0;
}
return (
dimension *
(1 - Math.exp(-(Math.abs(offset) * constant) / dimension)) *
Math.sign(offset)
);
};
const DAMPING_DIMENSION = 0.2;
const RUBBER_BAND_CONSTANT = 0.08;
const LIBRARY_SWIPE_FULL = 0.8;
const translation = libraryOnRight ? -translateX : translateX;
const deltaProgress = (translation / stripWidth) * LIBRARY_SWIPE_FULL;
const progress = this._swipeState.beforeLibraryState + deltaProgress;
let progressDamped;
if (progress < 0) {
progressDamped =
0 + rubberBand(progress, DAMPING_DIMENSION, RUBBER_BAND_CONSTANT);
} else if (progress > 1) {
progressDamped =
1 + rubberBand(progress - 1, DAMPING_DIMENSION, RUBBER_BAND_CONSTANT);
} else {
progressDamped = progress;
}
lazy.ZenLibrary.swipeProgress(progressDamped);
return;
}
// Apply a translateX to the tab strip to give the user feedback on the swipe
const currentWorkspace = ws.getActiveWorkspaceFromCache();
ws._organizeWorkspaceStripLocations(currentWorkspace, true, translateX);
@@ -173,17 +283,29 @@ export class ZenSpacesSwipe {
const rawDirection = moveForward ? 1 : -1;
const direction = ws.naturalScroll ? -1 : 1;
if (this._swipeState.isSwipingLibrary) {
lazy.ZenLibrary.stopSwipe(rawDirection * direction);
return;
}
await ws.changeWorkspaceShortcut(rawDirection * direction, true);
}
onSwipeGestureAnimationEnd() {
const ws = gZenWorkspaces;
if (this._swipeState.isSwipingLibrary) {
lazy.ZenLibrary.stopSwipe(null);
}
// Reset swipe state
this._swipeState = {
isGestureActive: false,
lastDelta: 0,
direction: null,
isSwipingLibrary: false,
beforeLibraryState: 0,
};
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);

View File

@@ -1425,9 +1425,15 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
* use -1 to avoid selecting any tab.
* @param {object} options - Additional options.
* @param {string|null} options.groupFetchId - An optional group fetch ID.
* @param {boolean} options.activate - Whether to select the split after creating it.
* @returns {object|undefined} The split view data or undefined if the split was not performed.
*/
splitTabs(tabs, gridType, initialIndex = 0, { groupFetchId = null } = {}) {
splitTabs(
tabs,
gridType,
initialIndex = 0,
{ groupFetchId = null, activate = true } = {}
) {
const tabIndexToUse = Math.max(0, initialIndex);
return this.#withoutSplitViewTransition(() => {
// TODO: Add support for splitting essential tabs
@@ -1443,6 +1449,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
const existingSplitTab = tabs.find(tab => tab.splitView);
let shouldActivateSplit =
activate &&
(initialIndex >= 0 || tabs.includes(window.gBrowser.selectedTab)) &&
!this._sessionRestoring;

View File

@@ -83,7 +83,8 @@
height: 100%;
}
#browser {
#browser,
zen-library {
--zen-min-toolbox-padding: 5px;
@media (-moz-platform: macos) {
--zen-min-toolbox-padding: 6px;
@@ -229,7 +230,8 @@
/* ==========================================================================
Navigator Toolbox (Main Sidebar Container) Base Styles
========================================================================== */
#navigator-toolbox {
#navigator-toolbox,
.zen-library-space-body {
/* Define theme variables, including platform specifics for native look */
--zen-toolbox-min-width: 1px;
--border-radius-medium: 14px;
@@ -453,6 +455,7 @@
}
#zen-sidebar-foot-buttons {
position: relative;
background: transparent;
gap: 5px;
align-items: center;
@@ -460,6 +463,10 @@
& > toolbarbutton:not(#zen-workspaces-button) {
padding: 0 !important;
}
:root[zen-right-side="true"][zen-sidebar-expanded="true"] & {
flex-direction: row-reverse;
}
}
#tabbrowser-arrowscrollbox {
@@ -508,7 +515,7 @@
}
}
#navigator-toolbox[zen-sidebar-expanded="true"] {
:is(#navigator-toolbox, .zen-library-space-body)[zen-sidebar-expanded="true"] {
--zen-toolbox-min-width: fit-content;
padding: var(--zen-toolbox-padding);
@@ -564,7 +571,6 @@
& #zen-sidebar-foot-buttons {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
position: relative;
@@ -877,6 +883,10 @@
display: none !important;
}
:root[zen-library-open="true"] #zen-sidebar-splitter {
pointer-events: none !important;
}
#zen-sidebar-splitter {
opacity: 0;
max-width: var(--zen-toolbox-padding) !important;

View File

@@ -35,7 +35,7 @@ const globalActionsTemplate = [
{
l10nId: "zen-action-open-theme-picker",
command: "cmd_zenOpenZenThemePicker",
icon: "chrome://browser/skin/zen-icons/edit-theme.svg",
icon: "chrome://browser/skin/zen-icons/paintbrush-fill.svg",
},
{
l10nId: "zen-action-new-split-view",