mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-18 10:44:46 +00:00
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:
88
locales/en-US/browser/browser/zen-library.ftl
Normal file
88
locales/en-US/browser/browser/zen-library.ftl
Normal 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
9
prefs/zen/library.yaml
Normal 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"
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -72,4 +72,6 @@
|
||||
<command id="cmd_zenNewLiveFolder" />
|
||||
|
||||
<command id="cmd_zenDuplicateTab" />
|
||||
|
||||
<command id="cmd_zenToggleLibrary" />
|
||||
</commandset>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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: '<number>'; initial-value: 1; inherits: false; }
|
||||
@property --zen-circle-anim { syntax: '<number>'; 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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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% {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
297
src/zen/common/styles/zen-library-widget.css
Normal file
297
src/zen/common/styles/zen-library-widget.css
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
592
src/zen/library/ZenLibrary.mjs
Normal file
592
src/zen/library/ZenLibrary.mjs
Normal 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);
|
||||
347
src/zen/library/ZenLibraryDragAndDrop.mjs
Normal file
347
src/zen/library/ZenLibraryDragAndDrop.mjs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
611
src/zen/library/ZenLibraryWidget.sys.mjs
Normal file
611
src/zen/library/ZenLibraryWidget.sys.mjs
Normal 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);
|
||||
},
|
||||
};
|
||||
5
src/zen/library/jar.inc.mn
Normal file
5
src/zen/library/jar.inc.mn
Normal 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
15
src/zen/library/moz.build
Normal 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",
|
||||
]
|
||||
278
src/zen/library/sections/ZenLibraryBoostsSection.mjs
Normal file
278
src/zen/library/sections/ZenLibraryBoostsSection.mjs
Normal 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);
|
||||
664
src/zen/library/sections/ZenLibraryDownloadsSection.mjs
Normal file
664
src/zen/library/sections/ZenLibraryDownloadsSection.mjs
Normal 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
|
||||
);
|
||||
361
src/zen/library/sections/ZenLibraryHistorySection.mjs
Normal file
361
src/zen/library/sections/ZenLibraryHistorySection.mjs
Normal 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);
|
||||
265
src/zen/library/sections/ZenLibrarySearchSection.mjs
Normal file
265
src/zen/library/sections/ZenLibrarySearchSection.mjs
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
820
src/zen/library/sections/ZenLibrarySpacesSection.mjs
Normal file
820
src/zen/library/sections/ZenLibrarySpacesSection.mjs
Normal 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);
|
||||
984
src/zen/library/zen-library.css
Normal file
984
src/zen/library/zen-library.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ DIRS += [
|
||||
"compact-mode",
|
||||
"drag-and-drop",
|
||||
"glance",
|
||||
"library",
|
||||
"live-folders",
|
||||
"mods",
|
||||
"tests",
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user