mirror of
https://github.com/zen-browser/desktop.git
synced 2026-05-25 22:38:32 +00:00
no-bug: Continue library work
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
library-spaces-section-title = Spaces
|
||||
library-downloads-section-title = Downloads
|
||||
library-history-section-title = History
|
||||
library-boosts-section-title = Boosts
|
||||
|
||||
library-search-placeholder =
|
||||
.placeholder = Search…
|
||||
@@ -19,8 +20,21 @@ library-history-yesterday = Yesterday
|
||||
library-history-empty = No history found
|
||||
library-downloads-empty = No downloads found
|
||||
library-spaces-empty = No spaces available
|
||||
library-boosts-empty = No boosts yet
|
||||
library-search-no-results = No results
|
||||
|
||||
library-boosts-search-placeholder =
|
||||
.placeholder = Search Boosts…
|
||||
|
||||
library-boost-toggle =
|
||||
.tooltiptext = Toggle boost for this site
|
||||
library-boost-context-edit =
|
||||
.label = Edit Boost
|
||||
library-boost-context-export =
|
||||
.label = Export Boost
|
||||
library-boost-context-delete =
|
||||
.label = Delete Boost
|
||||
|
||||
library-filter-button = Filter
|
||||
|
||||
library-history-filter-all =
|
||||
@@ -57,12 +71,12 @@ library-item-context-copy-url =
|
||||
.label = Copy URL
|
||||
library-item-context-delete-history =
|
||||
.label = Forget About This Page
|
||||
library-item-context-show-in-folder =
|
||||
.label = Show in Folder
|
||||
library-item-context-open-source =
|
||||
.label = Open Source URL
|
||||
library-item-context-remove =
|
||||
.label = Remove from History
|
||||
library-item-context-delete-file =
|
||||
.label = Delete File
|
||||
|
||||
library-history-action-remove =
|
||||
.tooltiptext = Forget About This Page
|
||||
|
||||
6
prefs/firefox/library.yaml
Normal file
6
prefs/firefox/library.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# 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: false
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..99210f8bb5633d50d2cba24f1e13ca866c5b6959 100644
|
||||
index edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..9fae4c0bca0a7ffb5c3b5517b09d0ef73422813a 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 edeb473e46b3aa4b12eb4b59ce62e5ae48edd2a1..99210f8bb5633d50d2cba24f1e13ca86
|
||||
|
||||
<toolbar id="nav-bar"
|
||||
class="browser-toolbar chromeclass-location"
|
||||
@@ -184,7 +192,6 @@
|
||||
<toolbartabstop/>
|
||||
<html:moz-urlbar id="urlbar"
|
||||
class="urlbar"
|
||||
- popover="manual"
|
||||
focused="true"
|
||||
pageproxystate="invalid"
|
||||
unifiedsearchbutton-available=""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f5c811a82 100644
|
||||
index b23244f9d3278918b016bb3fcab19687bc2e292a..118c7fbc7147c84286692fd7188c870e89be05cb 100644
|
||||
--- a/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
|
||||
@@ -90,6 +90,13 @@ const lazy = XPCOMUtils.declareLazy({
|
||||
@@ -118,7 +118,13 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
startLayoutExtend() {
|
||||
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
|
||||
// Do not expand if the Urlbar does not support being expanded or it is
|
||||
@@ -2710,6 +2778,13 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -2705,11 +2773,19 @@ export class UrlbarInput extends HTMLElement {
|
||||
if (!this.view.isOpen) {
|
||||
return;
|
||||
}
|
||||
+ this.setAttribute("popover", "manual");
|
||||
|
||||
this.#updateTextboxPosition();
|
||||
|
||||
this.setAttribute("breakout-extend", "true");
|
||||
|
||||
@@ -132,7 +138,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
// 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")) {
|
||||
@@ -2729,6 +2804,27 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -2729,7 +2805,29 @@ export class UrlbarInput extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,9 +164,11 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
+ this.removeAttribute("zen-floating-urlbar");
|
||||
+
|
||||
this.removeAttribute("breakout-extend");
|
||||
+ this.removeAttribute("popover");
|
||||
this.#updateTextboxPosition();
|
||||
}
|
||||
@@ -2759,7 +2855,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
|
||||
@@ -2759,7 +2857,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
forceUnifiedSearchButtonAvailable = false
|
||||
) {
|
||||
let prevState = this.getAttribute("pageproxystate");
|
||||
@@ -169,7 +177,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
this.setAttribute("pageproxystate", state);
|
||||
this._inputContainer.setAttribute("pageproxystate", state);
|
||||
this._identityBox?.setAttribute("pageproxystate", state);
|
||||
@@ -3031,10 +3127,12 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -3031,10 +3129,12 @@ export class UrlbarInput extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
this.style.top = px(
|
||||
@@ -182,7 +190,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3093,9 +3191,10 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -3093,9 +3193,10 @@ export class UrlbarInput extends HTMLElement {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -194,7 +202,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
);
|
||||
this.style.setProperty(
|
||||
"--urlbar-height",
|
||||
@@ -3597,6 +3696,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -3597,6 +3698,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
}
|
||||
|
||||
_toggleActionOverride(event) {
|
||||
@@ -202,7 +210,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
if (
|
||||
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
|
||||
event.keyCode == KeyEvent.DOM_VK_ALT ||
|
||||
@@ -3709,8 +3809,8 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -3709,8 +3811,8 @@ export class UrlbarInput extends HTMLElement {
|
||||
if (!this.#isAddressbar) {
|
||||
return val;
|
||||
}
|
||||
@@ -213,7 +221,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
: val;
|
||||
// Only trim value if the directionality doesn't change to RTL and we're not
|
||||
// showing a strikeout https protocol.
|
||||
@@ -4006,6 +4106,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4006,6 +4108,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
resultDetails = null,
|
||||
browser = this.window.gBrowser.selectedBrowser
|
||||
) {
|
||||
@@ -221,7 +229,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
if (this.#isAddressbar) {
|
||||
this.#prepareAddressbarLoad(
|
||||
url,
|
||||
@@ -4117,6 +4218,10 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4117,6 +4220,10 @@ export class UrlbarInput extends HTMLElement {
|
||||
}
|
||||
reuseEmpty = true;
|
||||
}
|
||||
@@ -232,7 +240,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
if (
|
||||
where == "tab" &&
|
||||
reuseEmpty &&
|
||||
@@ -4124,6 +4229,9 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4124,6 +4231,9 @@ export class UrlbarInput extends HTMLElement {
|
||||
) {
|
||||
where = "current";
|
||||
}
|
||||
@@ -242,7 +250,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
return where;
|
||||
}
|
||||
|
||||
@@ -4378,6 +4486,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4378,6 +4488,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
this.setResultForCurrentValue(null);
|
||||
this.handleCommand();
|
||||
this.controller.clearLastQueryContextCache();
|
||||
@@ -250,7 +258,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
|
||||
this._suppressStartQuery = false;
|
||||
});
|
||||
@@ -4385,7 +4494,6 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4385,7 +4496,6 @@ export class UrlbarInput extends HTMLElement {
|
||||
contextMenu.addEventListener("popupshowing", () => {
|
||||
// Close the results pane when the input field contextual menu is open,
|
||||
// because paste and go doesn't want a result selection.
|
||||
@@ -258,7 +266,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
|
||||
let controller =
|
||||
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
|
||||
@@ -4541,7 +4649,11 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4541,7 +4651,11 @@ export class UrlbarInput extends HTMLElement {
|
||||
if (!engineName && !source && !this.hasAttribute("searchmode")) {
|
||||
return;
|
||||
}
|
||||
@@ -271,7 +279,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
if (this._searchModeIndicatorTitle) {
|
||||
this._searchModeIndicatorTitle.textContent = "";
|
||||
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
|
||||
@@ -4851,6 +4963,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4851,6 +4965,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
|
||||
this.document.l10n.setAttributes(
|
||||
this.inputField,
|
||||
@@ -279,7 +287,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
l10nId,
|
||||
l10nId == "urlbar-placeholder-with-name"
|
||||
? { name: engineName }
|
||||
@@ -4964,6 +5077,11 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -4964,6 +5079,11 @@ export class UrlbarInput extends HTMLElement {
|
||||
}
|
||||
|
||||
_on_click(event) {
|
||||
@@ -291,7 +299,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
switch (event.target) {
|
||||
case this.inputField:
|
||||
case this._inputContainer:
|
||||
@@ -5042,7 +5160,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -5042,7 +5162,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +308,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
this.view.autoOpen({ event });
|
||||
} else {
|
||||
if (this._untrimOnFocusAfterKeydown) {
|
||||
@@ -5082,9 +5200,16 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -5082,9 +5202,16 @@ export class UrlbarInput extends HTMLElement {
|
||||
}
|
||||
|
||||
_on_mousedown(event) {
|
||||
@@ -318,7 +326,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
if (
|
||||
event.composedTarget != this.inputField &&
|
||||
event.composedTarget != this._inputContainer
|
||||
@@ -5094,6 +5219,10 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -5094,6 +5221,10 @@ export class UrlbarInput extends HTMLElement {
|
||||
|
||||
this.focusedViaMousedown = !this.focused;
|
||||
this._preventClickSelectsAll = this.focused;
|
||||
@@ -329,7 +337,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
|
||||
// Keep the focus status, since the attribute may be changed
|
||||
// upon calling this.focus().
|
||||
@@ -5129,7 +5258,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -5129,7 +5260,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
}
|
||||
// Don't close the view when clicking on a tab; we may want to keep the
|
||||
// view open on tab switch, and the TabSelect event arrived earlier.
|
||||
@@ -338,7 +346,7 @@ index b23244f9d3278918b016bb3fcab19687bc2e292a..ade1f031bbb68202a37e6c9d3071a73f
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5411,7 +5540,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
@@ -5411,7 +5542,7 @@ export class UrlbarInput extends HTMLElement {
|
||||
// When we are in actions search mode we can show more results so
|
||||
// increase the limit.
|
||||
let maxResults =
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#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 viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="9" stroke="context-stroke" stroke-width="2" fill="context-fill" fill-opacity="context-fill-opacity"/>
|
||||
<path d="M12 8V16M9 13L12 16L15 13" stroke="context-stroke" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,8 @@
|
||||
#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 viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="9" stroke="context-stroke" stroke-width="2" fill="context-fill" fill-opacity="context-fill-opacity"/>
|
||||
<path d="M12 7V12 L 15.5 14" stroke="context-stroke" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,18 @@
|
||||
#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="icon spaces-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<mask id="spaces-mask">
|
||||
<rect x="0" y="0" width="24" height="24" fill="white"/>
|
||||
<rect x="9" y="6" width="10" height="14" rx="2" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g class="back" transform="rotate(-15 12 12)" mask="url(#spaces-mask)">
|
||||
<rect class="back-rect" x="6" y="4" width="10" height="14" rx="2" stroke="context-stroke" stroke-width="2" fill="context-fill" fill-opacity="context-fill-opacity"/>
|
||||
</g>
|
||||
<g class="front">
|
||||
<rect class="front-rect" x="9" y="6" width="10" height="14" rx="2" stroke="context-stroke" stroke-width="2" fill="context-fill" fill-opacity="context-fill-opacity"/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -14,12 +14,12 @@
|
||||
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
|
||||
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
|
||||
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
|
||||
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
|
||||
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
|
||||
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
|
||||
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
|
||||
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
|
||||
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
|
||||
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
|
||||
@@ -162,12 +162,12 @@
|
||||
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
|
||||
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
|
||||
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
|
||||
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
|
||||
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
|
||||
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
|
||||
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
|
||||
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
|
||||
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
|
||||
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
|
||||
@@ -310,12 +310,12 @@
|
||||
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
|
||||
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
|
||||
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
|
||||
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
|
||||
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
|
||||
* skin/classic/browser/zen-icons/bookmark.svg (../shared/zen-icons/nucleo/bookmark.svg)
|
||||
* skin/classic/browser/zen-icons/boost.svg (../shared/zen-icons/nucleo/boost.svg)
|
||||
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
|
||||
* skin/classic/browser/zen-icons/brackets-curly.svg (../shared/zen-icons/nucleo/brackets-curly.svg)
|
||||
* skin/classic/browser/zen-icons/camera-blocked.svg (../shared/zen-icons/nucleo/camera-blocked.svg)
|
||||
* skin/classic/browser/zen-icons/camera-fill.svg (../shared/zen-icons/nucleo/camera-fill.svg)
|
||||
@@ -447,6 +447,9 @@
|
||||
* skin/classic/browser/zen-icons/zoom-out.svg (../shared/zen-icons/nucleo/zoom-out.svg)
|
||||
#endif
|
||||
* skin/classic/browser/zen-icons/urlbar-arrow.svg (../shared/zen-icons/common/urlbar-arrow.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-downloads.svg (../shared/zen-icons/common/library/library-downloads.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-history.svg (../shared/zen-icons/common/library/library-history.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-spaces.svg (../shared/zen-icons/common/library/library-spaces.svg)
|
||||
* skin/classic/browser/zen-icons/selectable/airplane.svg (../shared/zen-icons/common/selectable/airplane.svg)
|
||||
* skin/classic/browser/zen-icons/selectable/american-football.svg (../shared/zen-icons/common/selectable/american-football.svg)
|
||||
* skin/classic/browser/zen-icons/selectable/baseball.svg (../shared/zen-icons/common/selectable/baseball.svg)
|
||||
|
||||
@@ -53,12 +53,15 @@ do_common_icons() {
|
||||
echo "Working on $filename"
|
||||
echo "* skin/classic/browser/zen-icons/$filename (../shared/zen-icons/common/$filename) " >> jar.inc.mn
|
||||
done
|
||||
for filename in common/selectable/*.svg; do
|
||||
# remove the os/ prefix
|
||||
add_header_to_file $filename
|
||||
filename=$(basename $filename)
|
||||
echo "Working on $filename"
|
||||
echo "* skin/classic/browser/zen-icons/selectable/$filename (../shared/zen-icons/common/selectable/$filename) " >> jar.inc.mn
|
||||
# go through all subdirectories of common and do the same
|
||||
for dir in common/*/; do
|
||||
display_dir=$(basename $dir)
|
||||
for filename in $dir/*.svg; do
|
||||
add_header_to_file $filename
|
||||
filename=$(basename $filename)
|
||||
echo "Working on $filename"
|
||||
echo "* skin/classic/browser/zen-icons/$display_dir/$filename (../shared/zen-icons/common/$display_dir/$filename) " >> jar.inc.mn
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"chrome://browser/content/zen-components/ZenEmojiPicker.mjs",
|
||||
"chrome://browser/content/zen-components/ZenLiveFoldersUI.mjs",
|
||||
"chrome://browser/content/zen-components/ZenDownloadAnimation.mjs",
|
||||
"moz-src:///zen/library/ZenLibraryButton.mjs",
|
||||
];
|
||||
|
||||
for (let script of scripts) {
|
||||
|
||||
@@ -13,10 +13,6 @@ body,
|
||||
/* see issue #426 */
|
||||
background: var(--zen-navigator-toolbox-background, transparent) !important;
|
||||
--inactive-titlebar-opacity: 0.8;
|
||||
|
||||
&[zen-library-open="true"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-bar,
|
||||
|
||||
@@ -787,3 +787,162 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-library-button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#zen-library-button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
conic-gradient(
|
||||
var(--zen-colors-primary, currentColor)
|
||||
calc(var(--zen-library-button-progress, 0) * 360deg),
|
||||
color-mix(in srgb, currentColor 25%, transparent) 0
|
||||
);
|
||||
mask: radial-gradient(circle, transparent 3.5px, black 4px);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transform: scale(0.6);
|
||||
transition:
|
||||
opacity 0.18s ease,
|
||||
transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
}
|
||||
|
||||
#zen-library-button[downloading]::after {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
@keyframes zen-library-button-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#zen-library-button[downloading-indeterminate]::after {
|
||||
background: conic-gradient(
|
||||
var(--zen-colors-primary, currentColor) 0deg 90deg,
|
||||
color-mix(in srgb, currentColor 20%, transparent) 90deg 360deg
|
||||
);
|
||||
animation: zen-library-button-spin 1.05s linear infinite;
|
||||
}
|
||||
|
||||
#zen-library-button[downloading]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: var(--zen-colors-primary, currentColor);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Stack of recent download tiles inserted just before
|
||||
* #zen-sidebar-foot-buttons. Visible only while hovering the library button
|
||||
* (or the stack itself). Anchored to the bottom of #navigator-toolbox so it
|
||||
* floats above the tab strip without pushing layout.
|
||||
*/
|
||||
#navigator-toolbox {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zen-library-button-panel {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: var(--zen-library-stack-bottom-offset, 42px);
|
||||
z-index: 2;
|
||||
padding: 6px 8px;
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zen-library-button-panel[data-state="open"] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Fade the bottom of the tab strip so it doesn't visually collide with
|
||||
* the floating download stack. The fade region scales with the actual
|
||||
* stack height (published from JS as --zen-library-stack-height). */
|
||||
#tabbrowser-tabs[zen-library-stack-open="true"] {
|
||||
mask-image: linear-gradient(to bottom, black var(--zen-library-stack-height), black 50%, transparent calc(100% - var(--zen-library-stack-height) + 20px));
|
||||
transition: mask-image 0.18s ease;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-empty {
|
||||
padding: 12px 10px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.12s ease;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-row:hover {
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
.zen-library-button-panel-icon {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-labels {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-name {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-row[data-file-deleted] .zen-library-button-panel-name {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.zen-library-button-panel-status {
|
||||
margin: 0;
|
||||
font-size: 10.5px;
|
||||
opacity: 0.65;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const ZenCustomizableUI = new (class {
|
||||
|
||||
TYPE_TOOLBAR = "toolbar";
|
||||
defaultSidebarIcons = [
|
||||
"zen-library-button",
|
||||
Services.prefs.getBoolPref("zen.library.enabled") ? "zen-library-button" : "downloads-button",
|
||||
"zen-workspaces-button",
|
||||
"zen-create-new-button",
|
||||
];
|
||||
|
||||
@@ -147,10 +147,20 @@ class nsZenDownloadAnimationElement extends HTMLElement {
|
||||
return Services.prefs.getBoolPref("zen.tabs.vertical.right-side");
|
||||
}
|
||||
|
||||
get #downloadButton() {
|
||||
const ids = ["zen-library-button", "downloads-button"];
|
||||
for (const id of ids) {
|
||||
const button = document.getElementById(id);
|
||||
if (button && this.#isElementVisible(button)) {
|
||||
return button;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#determineEndPosition() {
|
||||
const downloadsButton = document.getElementById("downloads-button");
|
||||
const isDownloadButtonVisible =
|
||||
downloadsButton && this.#isElementVisible(downloadsButton);
|
||||
const downloadsButton = this.#downloadButton;
|
||||
const isDownloadButtonVisible = downloadsButton !== null;
|
||||
|
||||
let endPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
let lazy = {};
|
||||
let gZenLibraryInstance = null;
|
||||
|
||||
const PREVIOUS_TAB_PREF = "zen.library.previous-tab";
|
||||
|
||||
ChromeUtils.defineESModuleGetters(
|
||||
lazy,
|
||||
{
|
||||
@@ -30,13 +32,20 @@ ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
|
||||
* spaces, and other related data in a unified interface.
|
||||
*/
|
||||
export class ZenLibrary extends MozLitElement {
|
||||
static #ANIMATION_DURATION = 280;
|
||||
static #ANIMATION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
|
||||
static #TOOLBOX_OPEN_TRANSFORM = "scale(0.96)";
|
||||
static #TOOLBOX_OPEN_OPACITY = "-0.5";
|
||||
|
||||
#initialized = false;
|
||||
#resizeObserver = null;
|
||||
#sections = [];
|
||||
#activeAnimations = new Set();
|
||||
#animating = false;
|
||||
_deletionIdleCallbackId = null;
|
||||
|
||||
static properties = {
|
||||
activeTab: { type: String },
|
||||
_activeTab: { type: String },
|
||||
};
|
||||
|
||||
static queries = {
|
||||
@@ -46,7 +55,19 @@ export class ZenLibrary extends MozLitElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.activeTab = "history";
|
||||
this.activeTab = Services.prefs.getStringPref(PREVIOUS_TAB_PREF, "") || "history";
|
||||
}
|
||||
|
||||
set activeTab(value) {
|
||||
if (this.activeTab === value) {
|
||||
return;
|
||||
}
|
||||
this._activeTab = value;
|
||||
Services.prefs.setStringPref(PREVIOUS_TAB_PREF, value);
|
||||
}
|
||||
|
||||
get activeTab() {
|
||||
return this._activeTab;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
@@ -57,24 +78,11 @@ export class ZenLibrary extends MozLitElement {
|
||||
window.addEventListener("keydown", this);
|
||||
// Add connected call back and make `appContentWrapper` transform translate the oposite of this element
|
||||
this.#resizeObserver = new ResizeObserver(() => {
|
||||
requestAnimationFrame(() => {
|
||||
let isRightSide = gZenVerticalTabsManager._prefsRightSide;
|
||||
let translateX =
|
||||
window.windowUtils.getBoundsWithoutFlushing(this)[
|
||||
isRightSide ? "left" : "right"
|
||||
];
|
||||
let contentPosition = window.windowUtils.getBoundsWithoutFlushing(
|
||||
lazy.appContentWrapper
|
||||
)[isRightSide ? "right" : "left"];
|
||||
let existingTransform = new DOMMatrix(
|
||||
lazy.appContentWrapper.style.transform
|
||||
).m41;
|
||||
translateX = translateX - contentPosition + existingTransform;
|
||||
if (isRightSide) {
|
||||
translateX = -translateX;
|
||||
}
|
||||
lazy.appContentWrapper.style.transform = `translateX(${translateX}px)`;
|
||||
});
|
||||
if (gZenWorkspaces._swipeManager._swipeState.librarySwiping) {
|
||||
return;
|
||||
}
|
||||
let translateX = this.#computeWrapperTargetPx();
|
||||
lazy.appContentWrapper.style.transform = `translateX(${translateX}px)`;
|
||||
});
|
||||
this.#resizeObserver.observe(this);
|
||||
for (const Section of Object.values(lazy.ZenLibrarySections)) {
|
||||
@@ -114,6 +122,9 @@ export class ZenLibrary extends MozLitElement {
|
||||
?active=${this.activeTab === Section.id}
|
||||
@click=${() => (this.activeTab = Section.id)}
|
||||
>
|
||||
<img
|
||||
src=${`chrome://browser/skin/zen-icons/library/library-${Section.id}.svg`}
|
||||
/>
|
||||
<label>${lazy.l10n.formatValueSync(Section.label)}</label>
|
||||
</vbox>
|
||||
`
|
||||
@@ -146,6 +157,11 @@ export class ZenLibrary extends MozLitElement {
|
||||
return this.hasAttribute("open");
|
||||
}
|
||||
|
||||
/** True when a live library instance exists and is in the open state. */
|
||||
static get isOpen() {
|
||||
return gZenLibraryInstance?.isOpen ?? false;
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!gZenLibraryInstance) {
|
||||
gZenLibraryInstance = new ZenLibrary();
|
||||
@@ -163,26 +179,325 @@ export class ZenLibrary extends MozLitElement {
|
||||
}
|
||||
|
||||
static toggle() {
|
||||
if (gZenLibraryInstance?.#animating) {
|
||||
return;
|
||||
}
|
||||
window.docShell.treeOwner
|
||||
.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIAppWindow)
|
||||
.rollupAllPopups();
|
||||
let instance = this.getInstance();
|
||||
instance.toggleAttribute("open");
|
||||
if (!instance.isOpen) {
|
||||
gNavToolbox.removeAttribute("zen-library-open");
|
||||
lazy.appContentWrapper.style.transform = "";
|
||||
if (!instance._deletionIdleCallbackId) {
|
||||
instance._deletionIdleCallbackId = requestIdleCallback(() => {
|
||||
this.clearInstance();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (instance.isOpen) {
|
||||
if (instance._deletionIdleCallbackId) {
|
||||
cancelIdleCallback(instance._deletionIdleCallbackId);
|
||||
instance._deletionIdleCallbackId = null;
|
||||
}
|
||||
gNavToolbox.setAttribute("zen-library-open", "true");
|
||||
instance.#animateOpen();
|
||||
} else {
|
||||
gNavToolbox.removeAttribute("zen-library-open");
|
||||
// #animateClose schedules the instance disposal itself once the slide-
|
||||
// out finishes, so we don't queue an idle callback eagerly here — that
|
||||
// would otherwise race the animation and yank the element out of the
|
||||
// DOM mid-slide.
|
||||
instance.#animateClose();
|
||||
}
|
||||
}
|
||||
|
||||
#cancelActiveAnimations() {
|
||||
for (const anim of this.#activeAnimations) {
|
||||
try {
|
||||
anim.cancel();
|
||||
} catch {
|
||||
/* already settled */
|
||||
}
|
||||
}
|
||||
this.#activeAnimations.clear();
|
||||
}
|
||||
|
||||
#animate(element, keyframes) {
|
||||
const anim = element.animate(keyframes, {
|
||||
duration: ZenLibrary.#ANIMATION_DURATION,
|
||||
easing: ZenLibrary.#ANIMATION_EASING,
|
||||
fill: "forwards",
|
||||
});
|
||||
this.#activeAnimations.add(anim);
|
||||
anim.finished.then(
|
||||
() => this.#activeAnimations.delete(anim),
|
||||
() => this.#activeAnimations.delete(anim)
|
||||
);
|
||||
return anim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors the ResizeObserver's math but returns the translateX (px) instead
|
||||
* of writing it inline. Used when the observer hasn't fired yet (re-opens
|
||||
* on the same instance) or when we need a target without side effects.
|
||||
*/
|
||||
#computeWrapperTargetPx() {
|
||||
const isRightSide = gZenVerticalTabsManager._prefsRightSide;
|
||||
let translateX =
|
||||
window.windowUtils.getBoundsWithoutFlushing(this)[
|
||||
isRightSide ? "left" : "right"
|
||||
];
|
||||
const contentPosition = window.windowUtils.getBoundsWithoutFlushing(
|
||||
lazy.appContentWrapper
|
||||
)[isRightSide ? "right" : "left"];
|
||||
const existingTransform = new DOMMatrix(
|
||||
lazy.appContentWrapper.style.transform
|
||||
).m41;
|
||||
translateX = translateX - contentPosition + existingTransform;
|
||||
return isRightSide ? -translateX : translateX;
|
||||
}
|
||||
|
||||
async #animateOpen() {
|
||||
this.#animating = true;
|
||||
try {
|
||||
this.#cancelActiveAnimations();
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
if (!this.isOpen || !this.isConnected) {
|
||||
return;
|
||||
}
|
||||
// Re-opens on the same instance won't trigger the observer (library
|
||||
// size is unchanged), so fall back to computing the target ourselves.
|
||||
let wrapperTarget = lazy.appContentWrapper.style.transform;
|
||||
if (!wrapperTarget) {
|
||||
wrapperTarget = `translateX(${this.#computeWrapperTargetPx()}px)`;
|
||||
}
|
||||
lazy.appContentWrapper.style.transform = "";
|
||||
|
||||
this.#animate(this, [
|
||||
{ transform: "translateX(-100%)", opacity: 0 },
|
||||
{ transform: "translateX(0)", opacity: 1 },
|
||||
]);
|
||||
this.#animate(gNavToolbox, [
|
||||
{ transform: "scale(1)", opacity: 1 },
|
||||
{
|
||||
transform: ZenLibrary.#TOOLBOX_OPEN_TRANSFORM,
|
||||
opacity: ZenLibrary.#TOOLBOX_OPEN_OPACITY,
|
||||
},
|
||||
]);
|
||||
const wrapperAnim = this.#animate(lazy.appContentWrapper, [
|
||||
{ transform: "translateX(0)" },
|
||||
{ transform: wrapperTarget },
|
||||
]);
|
||||
|
||||
try {
|
||||
await wrapperAnim.finished;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
// Persist the final transform inline so the resize observer's
|
||||
// diff math keeps working after the animation ends.
|
||||
wrapperAnim.commitStyles();
|
||||
wrapperAnim.cancel();
|
||||
this.#activeAnimations.delete(wrapperAnim);
|
||||
} finally {
|
||||
this.#animating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the library for swipe-driven state changes. Works whether the
|
||||
* library is currently open (close swipe) or closed (open swipe). After
|
||||
* this resolves, callers drive `updateSwipeProgress(0..1)` directly until
|
||||
* `finishSwipe(targetOpen)` commits or reverts.
|
||||
*/
|
||||
static async beginSwipe() {
|
||||
// Refuse to enter swipe mode while a non-swipe animation is still
|
||||
// running; let it finish so the start/end states are well-defined.
|
||||
if (gZenLibraryInstance?.#animating) {
|
||||
return;
|
||||
}
|
||||
const instance = this.getInstance();
|
||||
if (instance._deletionIdleCallbackId) {
|
||||
cancelIdleCallback(instance._deletionIdleCallbackId);
|
||||
instance._deletionIdleCallbackId = null;
|
||||
}
|
||||
instance.#cancelActiveAnimations();
|
||||
const wasOpen = instance.hasAttribute("open");
|
||||
if (wasOpen) {
|
||||
// Library is already open; the wrapper's current inline transform IS
|
||||
// the target — no remeasure needed.
|
||||
instance._swipeWrapperTargetPx =
|
||||
new DOMMatrix(lazy.appContentWrapper.style.transform).m41 ||
|
||||
instance.#computeWrapperTargetPx();
|
||||
} else {
|
||||
// Measure the open-state wrapper target without flashing: temporarily
|
||||
// mark [open] so layout reflects the open position, then revert.
|
||||
instance.setAttribute("open", "true");
|
||||
instance.style.visibility = "hidden";
|
||||
await new Promise(r => requestAnimationFrame(r));
|
||||
instance._swipeWrapperTargetPx = instance.#computeWrapperTargetPx();
|
||||
instance.style.visibility = "";
|
||||
instance.removeAttribute("open");
|
||||
lazy.appContentWrapper.style.transform = "";
|
||||
}
|
||||
instance._swipeActive = true;
|
||||
// Initialize visual state to match the current attribute.
|
||||
ZenLibrary.updateSwipeProgress(wasOpen ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the library, toolbox, and content-wrapper styles to a fraction
|
||||
* (0..1) of the way to fully open. Must be preceded by `beginSwipe()`.
|
||||
*
|
||||
* @param {number} progress
|
||||
*/
|
||||
static updateSwipeProgress(progress) {
|
||||
const instance = gZenLibraryInstance;
|
||||
if (!instance?._swipeActive) {
|
||||
return;
|
||||
}
|
||||
const p = Math.max(0, Math.min(1, progress));
|
||||
instance.style.transform = `translateX(${(-1 + p) * 100}%)`;
|
||||
instance.style.opacity = String(p);
|
||||
const targetOpacity = Number(ZenLibrary.#TOOLBOX_OPEN_OPACITY);
|
||||
gNavToolbox.style.setProperty(
|
||||
"transform",
|
||||
`scale(${1 - p * 0.04})`,
|
||||
"important"
|
||||
);
|
||||
gNavToolbox.style.setProperty(
|
||||
"opacity",
|
||||
String(1 - p * (1 - targetOpacity)),
|
||||
"important"
|
||||
);
|
||||
lazy.appContentWrapper.style.setProperty(
|
||||
"transform",
|
||||
`translateX(${p * (instance._swipeWrapperTargetPx ?? 0)}px)`,
|
||||
"important"
|
||||
);
|
||||
instance._swipeProgress = p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish a swipe gesture. If `targetOpen` is true, animate the remaining
|
||||
* distance to fully open; otherwise animate to fully closed and dispose
|
||||
* the instance like a normal close.
|
||||
*
|
||||
* @param {boolean} targetOpen
|
||||
*/
|
||||
static async finishSwipe(targetOpen) {
|
||||
const instance = gZenLibraryInstance;
|
||||
if (!instance?._swipeActive) {
|
||||
return;
|
||||
}
|
||||
instance._swipeActive = false;
|
||||
|
||||
const libFromTransform = instance.style.transform;
|
||||
const libFromOpacity = instance.style.opacity;
|
||||
const tbFromTransform = gNavToolbox.style.transform;
|
||||
const tbFromOpacity = gNavToolbox.style.opacity;
|
||||
const wrapperFromX = new DOMMatrix(lazy.appContentWrapper.style.transform)
|
||||
.m41;
|
||||
|
||||
// Hand off styling to WAAPI by clearing the inline styles we set during
|
||||
// the swipe; the keyframes restore the from-state on their first frame.
|
||||
instance.style.transform = "";
|
||||
instance.style.opacity = "";
|
||||
gNavToolbox.style.transform = "";
|
||||
gNavToolbox.style.opacity = "";
|
||||
lazy.appContentWrapper.style.transform = "";
|
||||
|
||||
if (targetOpen) {
|
||||
instance.setAttribute("open", "true");
|
||||
gNavToolbox.setAttribute("zen-library-open", "true");
|
||||
instance.#animate(instance, [
|
||||
{ transform: libFromTransform, opacity: libFromOpacity },
|
||||
{ transform: "translateX(0)", opacity: 1 },
|
||||
]);
|
||||
instance.#animate(gNavToolbox, [
|
||||
{ transform: tbFromTransform, opacity: tbFromOpacity },
|
||||
{
|
||||
transform: ZenLibrary.#TOOLBOX_OPEN_TRANSFORM,
|
||||
opacity: ZenLibrary.#TOOLBOX_OPEN_OPACITY,
|
||||
},
|
||||
]);
|
||||
const wrapperAnim = instance.#animate(lazy.appContentWrapper, [
|
||||
{ transform: `translateX(${wrapperFromX}px)` },
|
||||
{ transform: `translateX(${instance._swipeWrapperTargetPx}px)` },
|
||||
]);
|
||||
try {
|
||||
await wrapperAnim.finished;
|
||||
if (instance.isOpen) {
|
||||
wrapperAnim.commitStyles();
|
||||
wrapperAnim.cancel();
|
||||
instance.#activeAnimations.delete(wrapperAnim);
|
||||
}
|
||||
} catch {
|
||||
/* cancelled by a follow-up toggle */
|
||||
}
|
||||
} else {
|
||||
instance.removeAttribute("open");
|
||||
gNavToolbox.removeAttribute("zen-library-open");
|
||||
instance.#animate(instance, [
|
||||
{ transform: libFromTransform, opacity: libFromOpacity },
|
||||
{ transform: "translateX(-100%)", opacity: 0 },
|
||||
]);
|
||||
instance.#animate(gNavToolbox, [
|
||||
{ transform: tbFromTransform, opacity: tbFromOpacity },
|
||||
{ transform: "scale(1)", opacity: 1 },
|
||||
]);
|
||||
instance.#animate(lazy.appContentWrapper, [
|
||||
{ transform: `translateX(${wrapperFromX}px)` },
|
||||
{ transform: "translateX(0)" },
|
||||
]);
|
||||
if (!instance._deletionIdleCallbackId) {
|
||||
instance._deletionIdleCallbackId = requestIdleCallback(() => {
|
||||
ZenLibrary.clearInstance();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #animateClose() {
|
||||
this.#animating = true;
|
||||
try {
|
||||
this.#cancelActiveAnimations();
|
||||
const wrapperCurrent =
|
||||
lazy.appContentWrapper.style.transform ||
|
||||
getComputedStyle(lazy.appContentWrapper).transform ||
|
||||
"translateX(0)";
|
||||
lazy.appContentWrapper.style.transform = "";
|
||||
|
||||
const libAnim = this.#animate(this, [
|
||||
{ transform: "translateX(0)", opacity: 1 },
|
||||
{ transform: "translateX(-100%)", opacity: 0 },
|
||||
]);
|
||||
this.#animate(gNavToolbox, [
|
||||
{
|
||||
transform: ZenLibrary.#TOOLBOX_OPEN_TRANSFORM,
|
||||
opacity: ZenLibrary.#TOOLBOX_OPEN_OPACITY,
|
||||
},
|
||||
{ transform: "scale(1)", opacity: 1 },
|
||||
]);
|
||||
this.#animate(lazy.appContentWrapper, [
|
||||
{ transform: wrapperCurrent },
|
||||
{ transform: "translateX(0)" },
|
||||
]);
|
||||
|
||||
try {
|
||||
await libAnim.finished;
|
||||
} catch {
|
||||
// Cancelled by a follow-up open before the slide finished.
|
||||
return;
|
||||
}
|
||||
// If the user re-opened in the meantime, leave the live instance alone.
|
||||
if (this.isOpen) {
|
||||
return;
|
||||
}
|
||||
if (!this._deletionIdleCallbackId) {
|
||||
this._deletionIdleCallbackId = requestIdleCallback(() => {
|
||||
ZenLibrary.clearInstance();
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.#animating = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
556
src/zen/library/ZenLibraryButton.mjs
Normal file
556
src/zen/library/ZenLibraryButton.mjs
Normal file
@@ -0,0 +1,556 @@
|
||||
// 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 { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
DownloadsCommon:
|
||||
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||
DownloadsViewUI:
|
||||
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
});
|
||||
|
||||
const RECENT_KEEP = 5;
|
||||
// Mouse can stray this far past the panel/button before we dismiss the stack.
|
||||
const HOVER_TOLERANCE_TOP_PX = 100;
|
||||
const HOVER_TOLERANCE_SIDE_PX = 40;
|
||||
|
||||
/**
|
||||
* Per-window controller for the toolbar Library button:
|
||||
* - Watches downloads and overlays a progress ring on the button while
|
||||
* anything is in flight (indeterminate spinner when total bytes are
|
||||
* unknown).
|
||||
* - Pops a small panel with the most recent finished/active downloads
|
||||
* when the user hovers the button.
|
||||
*/
|
||||
class nsZenLibraryButton extends nsZenDOMOperatedFeature {
|
||||
#button = null;
|
||||
#downloads = null;
|
||||
#view = null;
|
||||
#active = new Set();
|
||||
#recent = [];
|
||||
#panel = null;
|
||||
#closeAnim = null;
|
||||
#trackingMouse = false;
|
||||
#contextMenuOpen = false;
|
||||
|
||||
init() {
|
||||
this.#button = document.getElementById("zen-library-button");
|
||||
if (!this.#button) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#setupDownloads();
|
||||
|
||||
this.#button.addEventListener("mouseenter", this);
|
||||
window.addEventListener("unload", this, { once: true });
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "mouseenter":
|
||||
this.#onEnter();
|
||||
break;
|
||||
case "mousemove":
|
||||
this.#checkMousePosition(event);
|
||||
break;
|
||||
case "unload":
|
||||
this.#button?.removeEventListener("mouseenter", this);
|
||||
this.#detachMouseTracking();
|
||||
if (this.#downloads && this.#view) {
|
||||
this.#downloads.removeView(this.#view);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#setupDownloads() {
|
||||
this.#downloads = lazy.DownloadsCommon.getData(window, true);
|
||||
this.#view = {
|
||||
onDownloadAdded: dl => {
|
||||
if (!dl.stopped) {
|
||||
this.#active.add(dl);
|
||||
} else if (dl.succeeded) {
|
||||
this.#pushRecent(dl);
|
||||
}
|
||||
this.#updateRing();
|
||||
this.#refreshPanel();
|
||||
},
|
||||
onDownloadChanged: dl => {
|
||||
if (dl.stopped) {
|
||||
this.#active.delete(dl);
|
||||
if (dl.succeeded) {
|
||||
this.#pushRecent(dl);
|
||||
}
|
||||
} else {
|
||||
this.#active.add(dl);
|
||||
}
|
||||
this.#updateRing();
|
||||
this.#refreshPanel();
|
||||
},
|
||||
onDownloadRemoved: dl => {
|
||||
this.#active.delete(dl);
|
||||
this.#recent = this.#recent.filter(d => d !== dl);
|
||||
this.#updateRing();
|
||||
this.#refreshPanel();
|
||||
},
|
||||
};
|
||||
this.#downloads.addView(this.#view);
|
||||
}
|
||||
|
||||
#pushRecent(dl) {
|
||||
if (!this.#recent.includes(dl)) {
|
||||
this.#recent.unshift(dl);
|
||||
this.#recent = this.#recent.slice(0, RECENT_KEEP);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the conic-gradient progress + indeterminate state on the button. */
|
||||
#updateRing() {
|
||||
if (!this.#button) {
|
||||
return;
|
||||
}
|
||||
if (this.#active.size === 0) {
|
||||
this.#button.removeAttribute("downloading");
|
||||
this.#button.removeAttribute("downloading-indeterminate");
|
||||
this.#button.style.removeProperty("--zen-library-button-progress");
|
||||
return;
|
||||
}
|
||||
let total = 0;
|
||||
let current = 0;
|
||||
let hasUnknown = false;
|
||||
for (const dl of this.#active) {
|
||||
if (dl.hasProgress && dl.totalBytes > 0) {
|
||||
total += dl.totalBytes;
|
||||
current += dl.currentBytes;
|
||||
} else {
|
||||
hasUnknown = true;
|
||||
}
|
||||
}
|
||||
this.#button.setAttribute("downloading", "true");
|
||||
if (hasUnknown || total === 0) {
|
||||
this.#button.setAttribute("downloading-indeterminate", "true");
|
||||
this.#button.style.removeProperty("--zen-library-button-progress");
|
||||
} else {
|
||||
this.#button.removeAttribute("downloading-indeterminate");
|
||||
const ratio = Math.max(0, Math.min(1, current / total));
|
||||
this.#button.style.setProperty(
|
||||
"--zen-library-button-progress",
|
||||
String(ratio)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#ensurePanel() {
|
||||
if (this.#panel) {
|
||||
return this.#panel;
|
||||
}
|
||||
const stack = document.createElement("div");
|
||||
stack.id = "zen-library-button-panel";
|
||||
stack.classList.add("zen-library-button-panel");
|
||||
stack.dataset.state = "closed";
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "zen-library-button-panel-list";
|
||||
stack.appendChild(list);
|
||||
|
||||
const host = document.getElementById("zen-sidebar-foot-buttons");
|
||||
host.before(stack);
|
||||
this.#panel = stack;
|
||||
return stack;
|
||||
}
|
||||
|
||||
#refreshPanel() {
|
||||
if (!this.#panel || this.#panel.dataset.state !== "open") {
|
||||
return;
|
||||
}
|
||||
this.#populatePanel();
|
||||
}
|
||||
|
||||
#populatePanel() {
|
||||
const items = [
|
||||
...Array.from(this.#active),
|
||||
...this.#recent.filter(d => !this.#active.has(d)),
|
||||
].slice(0, RECENT_KEEP);
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = this.#ensurePanel();
|
||||
const list = panel.querySelector(".zen-library-button-panel-list");
|
||||
list.replaceChildren();
|
||||
|
||||
for (const dl of items) {
|
||||
list.appendChild(this.#renderRow(dl));
|
||||
}
|
||||
}
|
||||
|
||||
#renderRow(dl) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "zen-library-button-panel-row";
|
||||
if (this.#isFileMissing(dl)) {
|
||||
row.dataset.fileDeleted = "true";
|
||||
}
|
||||
row.addEventListener("click", () => {
|
||||
if (dl.succeeded) {
|
||||
lazy.DownloadsCommon.openDownload(dl).catch(console.error);
|
||||
} else if (dl.source?.url) {
|
||||
window.openTrustedLinkIn(dl.source.url, "tab");
|
||||
}
|
||||
});
|
||||
row.addEventListener("contextmenu", e => this.#showContextMenu(e, dl));
|
||||
|
||||
const icon = document.createElement("img");
|
||||
icon.className = "zen-library-button-panel-icon";
|
||||
icon.alt = "";
|
||||
icon.src = dl.target?.path
|
||||
? `moz-icon://${dl.target.path}?size=16`
|
||||
: "moz-icon://.unknown?size=16";
|
||||
row.appendChild(icon);
|
||||
|
||||
const labels = document.createElement("div");
|
||||
labels.className = "zen-library-button-panel-labels";
|
||||
|
||||
const display = lazy.DownloadsViewUI.getDisplayName(dl);
|
||||
const name = typeof display === "string" ? display : dl.source?.url || "";
|
||||
const label = document.createXULElement("label");
|
||||
label.className = "zen-library-button-panel-name";
|
||||
label.textContent = name;
|
||||
labels.appendChild(label);
|
||||
|
||||
const sublabel = document.createXULElement("label");
|
||||
sublabel.className = "zen-library-button-panel-status";
|
||||
sublabel.textContent = this.#statusFor(dl);
|
||||
labels.appendChild(sublabel);
|
||||
|
||||
row.appendChild(labels);
|
||||
return row;
|
||||
}
|
||||
|
||||
/** True when the on-disk file is gone (deleted / moved). */
|
||||
#isFileMissing(dl) {
|
||||
if (!dl) {
|
||||
return false;
|
||||
}
|
||||
if (dl.deleted) {
|
||||
return true;
|
||||
}
|
||||
return dl.succeeded && dl.target?.exists === false;
|
||||
}
|
||||
|
||||
#statusFor(dl) {
|
||||
const C = lazy.DownloadsCommon;
|
||||
const state = C.stateOfDownload(dl);
|
||||
if (state === C.DOWNLOAD_DOWNLOADING) {
|
||||
const total = dl.hasProgress ? dl.totalBytes : -1;
|
||||
const [status] = lazy.DownloadUtils.getDownloadStatus(
|
||||
dl.currentBytes,
|
||||
total,
|
||||
dl.speed
|
||||
);
|
||||
return status;
|
||||
}
|
||||
if (state === C.DOWNLOAD_FINISHED) {
|
||||
return (
|
||||
lazy.DownloadsViewUI.getSizeWithUnits(dl) ||
|
||||
C.strings.sizeUnknown ||
|
||||
""
|
||||
);
|
||||
}
|
||||
if (state === C.DOWNLOAD_PAUSED) {
|
||||
return C.strings.statePaused || "Paused";
|
||||
}
|
||||
if (state === C.DOWNLOAD_FAILED) {
|
||||
return C.strings.stateFailed || "Failed";
|
||||
}
|
||||
if (state === C.DOWNLOAD_CANCELED) {
|
||||
return C.strings.stateCanceled || "Canceled";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Right-click → small XUL menupopup mirroring the relevant subset of
|
||||
* Firefox's `downloadsContextMenu` (built dynamically so we can react to
|
||||
* each row's current state). Reuses `browser/downloads.ftl` strings.
|
||||
*/
|
||||
#showContextMenu(event, dl) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const C = lazy.DownloadsCommon;
|
||||
const state = C.stateOfDownload(dl);
|
||||
const isFinished = state === C.DOWNLOAD_FINISHED;
|
||||
const isActive =
|
||||
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_PAUSED;
|
||||
const fileExists = isFinished && dl.target?.exists !== false && !dl.deleted;
|
||||
const sourceUrl = dl.source?.originalUrl || dl.source?.url;
|
||||
const items = [];
|
||||
|
||||
if (state === C.DOWNLOAD_DOWNLOADING) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-pause",
|
||||
onClick: () => dl.cancel().catch(() => {}),
|
||||
});
|
||||
} else if (state === C.DOWNLOAD_PAUSED) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-resume",
|
||||
onClick: () => dl.start?.().catch(() => {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-show-menuitem-2",
|
||||
onClick: () => {
|
||||
try {
|
||||
const file = new lazy.FileUtils.File(dl.target.path);
|
||||
C.showDownloadedFile(file);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (sourceUrl) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-go-to-download-page",
|
||||
onClick: () => window.openTrustedLinkIn(sourceUrl, "tab"),
|
||||
});
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-copy-download-link",
|
||||
onClick: () => {
|
||||
const helper = Cc[
|
||||
"@mozilla.org/widget/clipboardhelper;1"
|
||||
].getService(Ci.nsIClipboardHelper);
|
||||
helper.copyString(sourceUrl);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
items.push({ separator: true });
|
||||
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-delete-file",
|
||||
onClick: () => {
|
||||
C.deleteDownloadFiles(
|
||||
dl,
|
||||
lazy.DownloadsViewUI.clearHistoryOnDelete
|
||||
).catch(console.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!isActive) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-remove-from-history",
|
||||
onClick: () => C.deleteDownload(dl).catch(console.error),
|
||||
});
|
||||
}
|
||||
|
||||
if (!items.some(i => !i.separator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const popupSet = document.getElementById("mainPopupSet");
|
||||
const popup = document.createXULElement("menupopup");
|
||||
for (const item of items) {
|
||||
if (item.separator) {
|
||||
if (
|
||||
!popup.lastChild ||
|
||||
popup.lastChild.tagName === "menuseparator"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
popup.appendChild(document.createXULElement("menuseparator"));
|
||||
continue;
|
||||
}
|
||||
const mi = document.createXULElement("menuitem");
|
||||
mi.setAttribute("data-l10n-id", item.l10nId);
|
||||
mi.addEventListener(
|
||||
"command",
|
||||
() => {
|
||||
try {
|
||||
item.onClick?.();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
popup.appendChild(mi);
|
||||
}
|
||||
// Drop a trailing separator if any.
|
||||
while (popup.lastChild?.tagName === "menuseparator") {
|
||||
popup.lastChild.remove();
|
||||
}
|
||||
// Pin the hover panel open for the lifetime of the context menu — the
|
||||
// popup briefly steals focus and would otherwise let the mouse-distance
|
||||
// check dismiss the stack mid-interaction.
|
||||
this.#contextMenuOpen = true;
|
||||
popup.addEventListener(
|
||||
"popuphidden",
|
||||
() => {
|
||||
this.#contextMenuOpen = false;
|
||||
popup.remove();
|
||||
// Re-evaluate distance now that the popup is gone.
|
||||
if (this.#panel?.dataset.state === "open") {
|
||||
this.#checkMousePosition();
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
popupSet.appendChild(popup);
|
||||
popup.openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
}
|
||||
|
||||
#onEnter() {
|
||||
if (this.#active.size === 0 && this.#recent.length === 0) {
|
||||
return;
|
||||
}
|
||||
// Customize mode can move the button out of the sidebar's foot toolbar —
|
||||
// the inline stack only makes sense when it's still there.
|
||||
if (!this.#button.closest("#zen-sidebar-foot-buttons")) {
|
||||
return;
|
||||
}
|
||||
// Interrupt a pending close so we reuse the same element instead of
|
||||
// racing a tear-down.
|
||||
this.#closeAnim?.cancel();
|
||||
this.#closeAnim = null;
|
||||
|
||||
const panel = this.#ensurePanel();
|
||||
this.#populatePanel();
|
||||
panel.dataset.state = "open";
|
||||
this.#updateMaskHeight(panel);
|
||||
panel.animate(
|
||||
[
|
||||
{ opacity: 0, transform: "translateY(8px)" },
|
||||
{ opacity: 1, transform: "translateY(0)" },
|
||||
],
|
||||
{
|
||||
duration: 180,
|
||||
easing: "cubic-bezier(0.32, 0.72, 0, 1)",
|
||||
fill: "forwards",
|
||||
id: "zen-library-stack-open",
|
||||
}
|
||||
);
|
||||
this.#attachMouseTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the panel's rendered height as a CSS variable on the tab strip
|
||||
* so the strip's mask gradient fades over exactly the area the stack
|
||||
* occupies (instead of a hardcoded amount).
|
||||
*/
|
||||
#updateMaskHeight(panel) {
|
||||
const tabs = window.gBrowser?.tabContainer;
|
||||
if (!tabs) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
if (!panel.isConnected) {
|
||||
return;
|
||||
}
|
||||
const h = Math.ceil(panel.getBoundingClientRect().height);
|
||||
tabs.style.setProperty("--zen-library-stack-height", `${h}px`);
|
||||
tabs.setAttribute("zen-library-stack-open", "true");
|
||||
});
|
||||
}
|
||||
|
||||
#attachMouseTracking() {
|
||||
if (this.#trackingMouse) {
|
||||
return;
|
||||
}
|
||||
window.addEventListener("mousemove", this, true);
|
||||
this.#trackingMouse = true;
|
||||
}
|
||||
|
||||
#detachMouseTracking() {
|
||||
if (!this.#trackingMouse) {
|
||||
return;
|
||||
}
|
||||
window.removeEventListener("mousemove", this, true);
|
||||
this.#trackingMouse = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the stack when the cursor moves outside a tolerance zone around the
|
||||
* panel + button: HOVER_TOLERANCE_TOP_PX above, HOVER_TOLERANCE_SIDE_PX on
|
||||
* either side (and the same below). Pinned open while a context menu is up.
|
||||
*/
|
||||
#checkMousePosition(event) {
|
||||
if (this.#contextMenuOpen || !this.#panel) {
|
||||
return;
|
||||
}
|
||||
if (this.#panel.dataset.state !== "open") {
|
||||
this.#detachMouseTracking();
|
||||
return;
|
||||
}
|
||||
const x = event?.clientX ?? -Infinity;
|
||||
const y = event?.clientY ?? -Infinity;
|
||||
const panelRect = this.#panel.getBoundingClientRect();
|
||||
const buttonRect = this.#button?.getBoundingClientRect();
|
||||
const left =
|
||||
Math.min(panelRect.left, buttonRect?.left ?? panelRect.left) -
|
||||
HOVER_TOLERANCE_SIDE_PX;
|
||||
const right =
|
||||
Math.max(panelRect.right, buttonRect?.right ?? panelRect.right) +
|
||||
HOVER_TOLERANCE_SIDE_PX;
|
||||
const top = panelRect.top - HOVER_TOLERANCE_TOP_PX;
|
||||
const bottom =
|
||||
Math.max(panelRect.bottom, buttonRect?.bottom ?? panelRect.bottom) +
|
||||
HOVER_TOLERANCE_SIDE_PX;
|
||||
if (x < left || x > right || y < top || y > bottom) {
|
||||
this.#hide();
|
||||
}
|
||||
}
|
||||
|
||||
#hide() {
|
||||
if (!this.#panel || this.#closeAnim) {
|
||||
return;
|
||||
}
|
||||
this.#detachMouseTracking();
|
||||
const tabs = window.gBrowser?.tabContainer;
|
||||
tabs?.removeAttribute("zen-library-stack-open");
|
||||
const panel = this.#panel;
|
||||
panel.dataset.state = "closed";
|
||||
const anim = panel.animate(
|
||||
[
|
||||
{ opacity: 1, transform: "translateY(0)" },
|
||||
{ opacity: 0, transform: "translateY(8px)" },
|
||||
],
|
||||
{
|
||||
duration: 140,
|
||||
easing: "cubic-bezier(0.32, 0.72, 0, 1)",
|
||||
fill: "forwards",
|
||||
id: "zen-library-stack-close",
|
||||
}
|
||||
);
|
||||
this.#closeAnim = anim;
|
||||
anim.finished.then(
|
||||
() => {
|
||||
if (this.#closeAnim !== anim) {
|
||||
return;
|
||||
}
|
||||
panel.remove();
|
||||
if (this.#panel === panel) {
|
||||
this.#panel = null;
|
||||
}
|
||||
this.#closeAnim = null;
|
||||
tabs?.style.removeProperty("--zen-library-stack-height");
|
||||
},
|
||||
() => {
|
||||
/* cancelled — re-entered while closing */
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
new nsZenLibraryButton();
|
||||
@@ -17,6 +17,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||
});
|
||||
|
||||
@@ -82,6 +83,17 @@ class LibrarySection extends MozLitElement {
|
||||
_l10n(key) {
|
||||
return lazy.l10n.formatValueSync(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the named attribute (e.g. "label") of a Fluent message, or "".
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {string} attrName
|
||||
*/
|
||||
_l10nAttr(key, attrName) {
|
||||
const [msg] = lazy.l10n.formatMessagesSync([{ id: key }]);
|
||||
return msg?.attributes?.find(a => a.name === attrName)?.value ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,15 +159,6 @@ class SearchSection extends LibrarySection {
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
_onClearSearch() {
|
||||
clearTimeout(this.#searchTimer);
|
||||
this.#searchTimer = null;
|
||||
this.searchTerm = "";
|
||||
this.inputValue = "";
|
||||
this.renderRoot.querySelector("#zen-library-search-input")?.focus();
|
||||
this._onSearch("");
|
||||
}
|
||||
|
||||
_onSearch(_term) {}
|
||||
|
||||
_dayLabel(date) {
|
||||
@@ -195,7 +198,7 @@ class SearchSection extends LibrarySection {
|
||||
/**
|
||||
* Called when the user picks a filter; subclasses re-query or re-filter.
|
||||
*
|
||||
* @param _id
|
||||
* @param {string} _id
|
||||
*/
|
||||
_onFilterChange(_id) {}
|
||||
|
||||
@@ -228,27 +231,39 @@ class SearchSection extends LibrarySection {
|
||||
_activeFilterLabel() {
|
||||
const filters = this._filters();
|
||||
const active = filters.find(f => f.id === this.activeFilter) ?? filters[0];
|
||||
return active ? this._l10n(active.label) : "";
|
||||
return active ? this._l10nAttr(active.label, "label") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders one item row. `sideTop`/`sideBottom` are the stacked right-side
|
||||
* info slots (e.g. domain over time for downloads, just time for history).
|
||||
*
|
||||
* @param root0
|
||||
* @param root0.key
|
||||
* @param root0.iconSrc
|
||||
* @param root0.label
|
||||
* @param root0.sublabel
|
||||
* @param root0.sideTop
|
||||
* @param root0.sideBottom
|
||||
* @param root0.payload
|
||||
* @param {object} root0
|
||||
* @param {string} root0.key
|
||||
* @param {string} root0.iconSrc
|
||||
* @param {object} root0.iconTemplate
|
||||
* @param {string} root0.label
|
||||
* @param {string} root0.sublabel
|
||||
* @param {string} root0.sideTop
|
||||
* @param {string} root0.sideBottom
|
||||
* @param {object} root0.payload
|
||||
*/
|
||||
_renderItem({ key, iconSrc, label, sublabel, sideTop, sideBottom, payload }) {
|
||||
_renderItem({
|
||||
key,
|
||||
iconSrc,
|
||||
iconTemplate,
|
||||
label,
|
||||
sublabel,
|
||||
sideTop,
|
||||
sideBottom,
|
||||
payload,
|
||||
fileMissing = false,
|
||||
}) {
|
||||
return html`
|
||||
<div
|
||||
class="library-item"
|
||||
data-key=${key ?? ""}
|
||||
?file-missing=${fileMissing}
|
||||
@click=${e => this._onItemClick(e, payload)}
|
||||
@auxclick=${e => this._onItemClick(e, payload)}
|
||||
@contextmenu=${e => this._onItemContextMenu(e, payload)}
|
||||
@@ -257,12 +272,17 @@ class SearchSection extends LibrarySection {
|
||||
<div class="library-item-background"></div>
|
||||
<div class="library-item-content">
|
||||
<div class="library-item-icon-stack">
|
||||
<img
|
||||
class="library-item-icon-image"
|
||||
src=${iconSrc || ""}
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
${
|
||||
iconTemplate ??
|
||||
html`
|
||||
<img
|
||||
class="library-item-icon-image"
|
||||
src=${iconSrc || ""}
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
<div class="library-item-label-container">
|
||||
<label class="library-item-label">${label}</label>
|
||||
@@ -273,19 +293,19 @@ class SearchSection extends LibrarySection {
|
||||
? html`
|
||||
<div class="library-item-side">
|
||||
${
|
||||
sideTop
|
||||
? html`<label class="library-item-side-top"
|
||||
>${sideTop}</label
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
sideTop
|
||||
? html`<label class="library-item-side-top"
|
||||
>${sideTop}</label
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
${
|
||||
sideBottom
|
||||
? html`<label class="library-item-side-bottom"
|
||||
>${sideBottom}</label
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
sideBottom
|
||||
? html`<label class="library-item-side-bottom"
|
||||
>${sideBottom}</label
|
||||
>`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
@@ -319,7 +339,7 @@ class SearchSection extends LibrarySection {
|
||||
* Subclasses override to provide trailing action buttons. Default is a single
|
||||
* "more options" button that opens the context menu.
|
||||
*
|
||||
* @param payload
|
||||
* @param {object} payload
|
||||
*/
|
||||
_renderItemActions(payload) {
|
||||
return [
|
||||
@@ -333,15 +353,15 @@ class SearchSection extends LibrarySection {
|
||||
/**
|
||||
* Subclasses override; default is a no-op.
|
||||
*
|
||||
* @param _event
|
||||
* @param _payload
|
||||
* @param {Event} _event
|
||||
* @param {object} _payload
|
||||
*/
|
||||
_onItemClick(_event, _payload) {}
|
||||
|
||||
/**
|
||||
* Subclasses override to build a menu; return null to skip the menu.
|
||||
*
|
||||
* @param _payload
|
||||
* @param {object} _payload
|
||||
*/
|
||||
_buildContextMenu(_payload) {
|
||||
return null;
|
||||
@@ -379,10 +399,10 @@ class SearchSection extends LibrarySection {
|
||||
* the requested location, and remove it on hide. We rebuild on every open
|
||||
* because the action set depends on the row's payload.
|
||||
*
|
||||
* @param items
|
||||
* @param opener
|
||||
* @param {Array} items
|
||||
* @param {object} anchor
|
||||
*/
|
||||
_openNativeContextMenu(items, opener) {
|
||||
_openNativeContextMenu(items, anchor) {
|
||||
const popupSet = document.getElementById("mainPopupSet");
|
||||
const popup = document.createXULElement("menupopup");
|
||||
|
||||
@@ -426,17 +446,17 @@ class SearchSection extends LibrarySection {
|
||||
|
||||
popupSet.appendChild(popup);
|
||||
|
||||
if (opener.type === "anchor") {
|
||||
popup.openPopup(opener.el, "after_end", 0, 0, true, false);
|
||||
if (anchor.type === "anchor") {
|
||||
popup.openPopup(anchor.el, "after_end", 0, 0, true, false);
|
||||
} else {
|
||||
popup.openPopupAtScreen(opener.x, opener.y, true);
|
||||
popup.openPopupAtScreen(anchor.x, anchor.y, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the modifier configured for Glance activation is held.
|
||||
*
|
||||
* @param event
|
||||
* @param {Event} event
|
||||
*/
|
||||
_isGlanceActivation(event) {
|
||||
if (!Services.prefs.getBoolPref("zen.glance.enabled", true)) {
|
||||
@@ -457,8 +477,8 @@ class SearchSection extends LibrarySection {
|
||||
/**
|
||||
* Open `url` in a Glance overlay anchored to the clicked item.
|
||||
*
|
||||
* @param event
|
||||
* @param url
|
||||
* @param {Event} event
|
||||
* @param {string} url
|
||||
*/
|
||||
_openInGlance(event, url) {
|
||||
const itemEl = event.currentTarget;
|
||||
@@ -521,13 +541,6 @@ class SearchSection extends LibrarySection {
|
||||
@input=${this._onSearchInput}
|
||||
.value=${this.inputValue}
|
||||
/>
|
||||
<button
|
||||
class="search-clear-button"
|
||||
tabindex="-1"
|
||||
@click=${this._onClearSearch}
|
||||
>
|
||||
<img src="chrome://browser/skin/zen-icons/close.svg" alt="" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
@@ -647,7 +660,7 @@ class ProgressiveSearchSection extends SearchSection {
|
||||
/**
|
||||
* Hook called from SearchSection's results scroll handler.
|
||||
*
|
||||
* @param el
|
||||
* @param {Element} el
|
||||
*/
|
||||
_onScroll(el) {
|
||||
if (this.#renderedItemCount >= this._allItems.length) {
|
||||
@@ -951,8 +964,8 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
|
||||
* Ctrl (or any other modifier / middle-click) falls back to the standard
|
||||
* "where to open" routing.
|
||||
*
|
||||
* @param event
|
||||
* @param item
|
||||
* @param {Event} event
|
||||
* @param {object} item
|
||||
*/
|
||||
_onItemClick(event, item) {
|
||||
if (event.button === 2) {
|
||||
@@ -972,8 +985,9 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
|
||||
!isMiddleClick &&
|
||||
Services.prefs.getBoolPref("zen.glance.enabled", true)
|
||||
) {
|
||||
// Glance overlays on top of the library; keep the library open so the
|
||||
// user can fire another glance without re-opening it.
|
||||
this._openInGlance(event, item.url);
|
||||
this._closeLibrary();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1024,7 +1038,6 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
|
||||
// the cursor and the original item element isn't tracked here.
|
||||
const fakeEvent = { currentTarget: this };
|
||||
this._openInGlance(fakeEvent, item.url);
|
||||
this._closeLibrary();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1200,15 +1213,81 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
this._formatTime(this.#downloadTime(dl)),
|
||||
].filter(Boolean);
|
||||
|
||||
const state = lazy.DownloadsCommon.stateOfDownload(dl);
|
||||
const isActive =
|
||||
state === lazy.DownloadsCommon.DOWNLOAD_DOWNLOADING ||
|
||||
state === lazy.DownloadsCommon.DOWNLOAD_PAUSED ||
|
||||
state === lazy.DownloadsCommon.DOWNLOAD_NOTSTARTED;
|
||||
const fileMissing =
|
||||
dl.deleted ||
|
||||
(dl.succeeded && dl.target?.exists === false);
|
||||
|
||||
return this._renderItem({
|
||||
key: `${this.#downloadTime(dl)}|${dl.source.url}`,
|
||||
iconSrc: iconPath,
|
||||
iconTemplate: isActive ? this.#renderDownloadProgress(dl) : null,
|
||||
label: nameStr,
|
||||
sublabel: sublabelParts.join(" · "),
|
||||
payload: dl,
|
||||
fileMissing,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SVG progress ring that replaces the file icon while a download is in
|
||||
* flight. The ring fills clockwise to match `currentBytes / totalBytes`;
|
||||
* downloads of unknown size spin instead. Hovering swaps the ring for an
|
||||
* X — clicking it cancels and drops any partial data.
|
||||
*
|
||||
* @param {object} dl
|
||||
*/
|
||||
#renderDownloadProgress(dl) {
|
||||
const RADIUS = 8;
|
||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||
const hasProgress = dl.hasProgress && dl.totalBytes > 0;
|
||||
const ratio = hasProgress
|
||||
? Math.max(0, Math.min(1, dl.currentBytes / dl.totalBytes))
|
||||
: 0;
|
||||
const dashOffset = CIRCUMFERENCE * (1 - ratio);
|
||||
|
||||
return html`
|
||||
<button
|
||||
class="library-download-progress"
|
||||
data-progress=${hasProgress ? "determinate" : "indeterminate"}
|
||||
title="Cancel download"
|
||||
@click=${e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
dl.cancel().catch(() => {});
|
||||
dl.removePartialData().catch(console.error);
|
||||
}}
|
||||
@auxclick=${e => e.stopPropagation()}
|
||||
>
|
||||
<svg
|
||||
class="library-download-progress-ring"
|
||||
viewBox="0 0 18 18"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<circle class="track" cx="9" cy="9" r=${RADIUS}></circle>
|
||||
<circle
|
||||
class="arc"
|
||||
cx="9"
|
||||
cy="9"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${CIRCUMFERENCE}
|
||||
stroke-dashoffset=${dashOffset}
|
||||
></circle>
|
||||
</svg>
|
||||
<img
|
||||
class="library-download-progress-cancel"
|
||||
src="chrome://browser/skin/zen-icons/close.svg"
|
||||
alt=""
|
||||
/>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
#sourceDomain(dl) {
|
||||
const url = dl.source.originalUrl || dl.source.url || "";
|
||||
try {
|
||||
@@ -1236,7 +1315,6 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
dl.source.referrerInfo?.originalReferrer?.spec || dl.source.url;
|
||||
if (previewUrl) {
|
||||
this._openInGlance(event, previewUrl);
|
||||
this._closeLibrary();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1258,56 +1336,84 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors Firefox's `downloadsContextMenu` so the labels stay in sync with
|
||||
* the rest of the browser. Built dynamically because the relevant entries
|
||||
* change per state (pause/resume, show, delete, etc.).
|
||||
*/
|
||||
_buildContextMenu(dl) {
|
||||
if (!dl) {
|
||||
return null;
|
||||
}
|
||||
const state = lazy.DownloadsCommon.stateOfDownload(dl);
|
||||
const isFinished = state === lazy.DownloadsCommon.DOWNLOAD_FINISHED;
|
||||
const C = lazy.DownloadsCommon;
|
||||
const state = C.stateOfDownload(dl);
|
||||
const isFinished = state === C.DOWNLOAD_FINISHED;
|
||||
const isActive =
|
||||
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_PAUSED;
|
||||
const fileExists = isFinished && dl.target.exists !== false && !dl.deleted;
|
||||
const sourceUrl = dl.source.originalUrl || dl.source.url;
|
||||
|
||||
const items = [];
|
||||
|
||||
if (state === C.DOWNLOAD_DOWNLOADING) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-pause",
|
||||
onClick: () => dl.cancel().catch(() => {}),
|
||||
});
|
||||
} else if (state === C.DOWNLOAD_PAUSED) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-resume",
|
||||
onClick: () => dl.start?.().catch(() => {}),
|
||||
});
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "library-item-context-open",
|
||||
onClick: () => {
|
||||
lazy.DownloadsCommon.openDownload(dl).catch(console.error);
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
l10nId: "library-item-context-show-in-folder",
|
||||
l10nId: "downloads-cmd-show-menuitem-2",
|
||||
onClick: () => {
|
||||
try {
|
||||
const file = new lazy.FileUtils.File(dl.target.path);
|
||||
lazy.DownloadsCommon.showDownloadedFile(file);
|
||||
C.showDownloadedFile(file);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
});
|
||||
items.push({ separator: true });
|
||||
}
|
||||
|
||||
if (sourceUrl) {
|
||||
items.push({
|
||||
l10nId: "library-item-context-open-source",
|
||||
l10nId: "downloads-cmd-go-to-download-page",
|
||||
onClick: () => {
|
||||
window.openTrustedLinkIn(sourceUrl, "tab");
|
||||
this._closeLibrary();
|
||||
},
|
||||
});
|
||||
items.push({
|
||||
l10nId: "library-item-context-copy-url",
|
||||
l10nId: "downloads-cmd-copy-download-link",
|
||||
onClick: () => lazy.clipboardHelper.copyString(sourceUrl),
|
||||
});
|
||||
items.push({ separator: true });
|
||||
}
|
||||
items.push({
|
||||
l10nId: "library-item-context-remove",
|
||||
onClick: () => {
|
||||
lazy.DownloadsCommon.deleteDownload(dl).catch(console.error);
|
||||
},
|
||||
});
|
||||
|
||||
items.push({ separator: true });
|
||||
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-delete-file",
|
||||
onClick: () => {
|
||||
C.deleteDownloadFiles(
|
||||
dl,
|
||||
lazy.DownloadsViewUI.clearHistoryOnDelete
|
||||
).catch(console.error);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!isActive) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-remove-from-history",
|
||||
onClick: () => C.deleteDownload(dl).catch(console.error),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -1454,6 +1560,216 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boosts section: lists every saved boost across all domains. Each row shows
|
||||
* the boost's name + domain, a hover-revealed export button, and a toggle
|
||||
* pill that reflects (and flips) the domain's active-boost state.
|
||||
*/
|
||||
class ZenLibraryBoostsSection extends SearchSection {
|
||||
static id = "boosts";
|
||||
static label = "library-boosts-section-title";
|
||||
|
||||
#observer = null;
|
||||
|
||||
get _searchPlaceholder() {
|
||||
return "library-boosts-search-placeholder";
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#observer = () => this.requestUpdate();
|
||||
Services.obs.addObserver(this.#observer, "zen-boosts-update");
|
||||
Services.obs.addObserver(this.#observer, "zen-boosts-active-change");
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.#observer) {
|
||||
Services.obs.removeObserver(this.#observer, "zen-boosts-update");
|
||||
Services.obs.removeObserver(this.#observer, "zen-boosts-active-change");
|
||||
this.#observer = null;
|
||||
}
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
_onSearch(_term) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
/** Flat list of every boost across all domains, filtered by search term. */
|
||||
#getBoosts() {
|
||||
const manager = lazy.gZenBoostsManager;
|
||||
if (!manager?.registeredDomains) {
|
||||
return [];
|
||||
}
|
||||
const term = this.searchTerm?.trim().toLowerCase();
|
||||
const items = [];
|
||||
for (const [domain, entry] of manager.registeredDomains) {
|
||||
for (const [id, boostEntry] of entry.boostEntries) {
|
||||
const displayName = boostEntry.boostData?.boostName || "";
|
||||
if (
|
||||
term &&
|
||||
!displayName.toLowerCase().includes(term) &&
|
||||
!domain.toLowerCase().includes(term)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
id,
|
||||
domain,
|
||||
name: displayName,
|
||||
isActive: entry.activeBoostId === id,
|
||||
});
|
||||
}
|
||||
}
|
||||
items.sort((a, b) =>
|
||||
(a.name || a.domain).localeCompare(b.name || b.domain)
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
renderSearchResults() {
|
||||
const boosts = this.#getBoosts();
|
||||
if (boosts.length === 0) {
|
||||
return html`
|
||||
<div
|
||||
class="empty-state"
|
||||
data-l10n-id=${this.searchTerm ? "library-search-no-results" : "library-boosts-empty"}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
return html`${boosts.map(b => this.#renderBoost(b))}`;
|
||||
}
|
||||
|
||||
#renderBoost(boost) {
|
||||
return html`
|
||||
<div
|
||||
class="library-item library-boost-item"
|
||||
data-key=${`${boost.domain}|${boost.id}`}
|
||||
?active=${boost.isActive}
|
||||
@click=${e => this.#openBoost(e, boost)}
|
||||
@contextmenu=${e => this._onItemContextMenu(e, boost)}
|
||||
>
|
||||
<div class="library-item-stack">
|
||||
<div class="library-item-background"></div>
|
||||
<div class="library-item-content">
|
||||
<div class="library-boost-icon" ?inactive=${!boost.isActive}>
|
||||
<img
|
||||
class="library-boost-icon-image"
|
||||
src="page-icon:https://${boost.domain}/"
|
||||
alt=""
|
||||
role="presentation"
|
||||
/>
|
||||
</div>
|
||||
<div class="library-item-label-container">
|
||||
<label class="library-item-label"
|
||||
>${boost.name || boost.domain}</label
|
||||
>
|
||||
<label class="library-item-sublabel">${boost.domain}</label>
|
||||
</div>
|
||||
<button
|
||||
class="library-boost-toggle"
|
||||
data-l10n-id="library-boost-toggle"
|
||||
role="switch"
|
||||
aria-checked=${boost.isActive ? "true" : "false"}
|
||||
?checked=${boost.isActive}
|
||||
tabindex="-1"
|
||||
@click=${e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.#toggle(boost);
|
||||
}}
|
||||
@auxclick=${e => e.stopPropagation()}
|
||||
>
|
||||
<span class="library-boost-toggle-thumb"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the boost's domain in a Glance overlay and pop the boost editor
|
||||
* window next to it so the user can tweak the boost while previewing.
|
||||
*
|
||||
* @param {Event} event
|
||||
* @param {object} boost
|
||||
*/
|
||||
#openBoost(event, boost) {
|
||||
const url = `https://${boost.domain}/`;
|
||||
this._openInGlance(event, url);
|
||||
try {
|
||||
const stored = lazy.gZenBoostsManager.loadBoostFromStore(
|
||||
boost.domain,
|
||||
boost.id
|
||||
);
|
||||
if (stored) {
|
||||
const uri = Services.io.newURI(url);
|
||||
lazy.gZenBoostsManager.openBoostWindow(window, stored, uri);
|
||||
}
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
#toggle(boost) {
|
||||
lazy.gZenBoostsManager.toggleBoostActiveForDomain(boost.domain, boost.id);
|
||||
}
|
||||
|
||||
#export(boost) {
|
||||
const manager = lazy.gZenBoostsManager;
|
||||
const stored = manager.loadBoostFromStore(boost.domain, boost.id);
|
||||
if (!stored) {
|
||||
return;
|
||||
}
|
||||
manager.exportBoost(window, {
|
||||
domain: boost.domain,
|
||||
id: boost.id,
|
||||
boostEntry: stored.boostEntry,
|
||||
});
|
||||
}
|
||||
|
||||
_buildContextMenu(boost) {
|
||||
if (!boost) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
{
|
||||
l10nId: "library-boost-context-edit",
|
||||
onClick: () => {
|
||||
const stored = lazy.gZenBoostsManager.loadBoostFromStore(
|
||||
boost.domain,
|
||||
boost.id
|
||||
);
|
||||
if (!stored) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const uri = Services.io.newURI(`https://${boost.domain}/`);
|
||||
lazy.gZenBoostsManager.openBoostWindow(window, stored, uri);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
l10nId: "library-boost-context-export",
|
||||
onClick: () => this.#export(boost),
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
l10nId: "library-boost-context-delete",
|
||||
onClick: () => {
|
||||
lazy.gZenBoostsManager.deleteBoost({
|
||||
domain: boost.domain,
|
||||
id: boost.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/** Spaces section: Borgir man will do it :) */
|
||||
class ZenLibrarySpacesSection extends LibrarySection {
|
||||
static largeContent = true;
|
||||
@@ -1464,6 +1780,7 @@ class ZenLibrarySpacesSection extends LibrarySection {
|
||||
export const ZenLibrarySections = {
|
||||
history: ZenLibraryHistorySection,
|
||||
downloads: ZenLibraryDownloadsSection,
|
||||
boosts: ZenLibraryBoostsSection,
|
||||
spaces: ZenLibrarySpacesSection,
|
||||
};
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
|
||||
MOZ_SRC_FILES += [
|
||||
"ZenLibrary.mjs",
|
||||
"ZenLibraryButton.mjs",
|
||||
"ZenLibrarySections.mjs",
|
||||
]
|
||||
|
||||
@@ -20,13 +20,16 @@
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
:host(zen-library[open]) {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
/* Layout Components */
|
||||
@@ -87,6 +90,8 @@
|
||||
padding: 12px 0;
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
@@ -103,6 +108,19 @@
|
||||
&[active]::before {
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
}
|
||||
|
||||
& img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
fill: rgba(255, 255, 255, 0.8);
|
||||
stroke: var(--zen-colors-primary);
|
||||
fill-opacity: 0;
|
||||
-moz-context-properties: fill, fill-opacity, stroke;
|
||||
}
|
||||
|
||||
&[active] img {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Search Bar */
|
||||
@@ -188,37 +206,6 @@
|
||||
&:focus-within .search-input {
|
||||
color: var(--toolbar-field-focus-color, light-dark(black, rgb(251,251,254)));
|
||||
}
|
||||
|
||||
& .search-clear-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
margin-inline-start: 6px;
|
||||
padding: 12px;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: calc(var(--border-radius-medium) - 2px);
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
pointer-events: none;
|
||||
flex-shrink: 0;
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
|
||||
& img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--urlbar-box-hover-bgcolor, light-dark(rgba(0,0,0,0.08), rgba(255,255,255,0.1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-filter-button {
|
||||
@@ -235,7 +222,6 @@
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
line-height: var(--tab-label-line-height);
|
||||
cursor: pointer;
|
||||
fill: currentColor;
|
||||
fill-opacity: 0.5;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
@@ -311,7 +297,6 @@
|
||||
border-radius: 0;
|
||||
color: inherit;
|
||||
color-scheme: unset;
|
||||
cursor: pointer;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@@ -367,6 +352,85 @@
|
||||
-moz-context-properties: fill, stroke;
|
||||
}
|
||||
|
||||
.library-download-progress {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-inline-start: calc(var(--toolbarbutton-inner-padding) / 2);
|
||||
margin-inline-end: calc(var(--toolbarbutton-inner-padding) * 1.5);
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: currentColor;
|
||||
|
||||
.library-item-stack:hover & {
|
||||
color: var(--zen-colors-primary);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.library-download-progress-ring .track {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, currentColor 22%, transparent);
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.library-download-progress-ring .arc {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50% 50%;
|
||||
transition: stroke-dashoffset 0.25s linear;
|
||||
}
|
||||
|
||||
.library-download-progress[data-progress="indeterminate"] .arc {
|
||||
stroke-dasharray: 14 50 !important;
|
||||
animation: zen-library-download-spin 1.2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes zen-library-download-spin {
|
||||
from {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(270deg);
|
||||
}
|
||||
}
|
||||
|
||||
.library-download-progress-cancel {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.library-download-progress-cancel line {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
.library-item-stack:hover .library-download-progress-ring .arc,
|
||||
.library-item-stack:hover .library-download-progress-ring .track {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.library-item-stack:hover .library-download-progress-cancel {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.library-item-label-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -385,6 +449,11 @@
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.library-item[file-missing] .library-item-label {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* finished (i hope so) */
|
||||
.library-item-sublabel {
|
||||
margin: 0;
|
||||
@@ -445,7 +514,6 @@
|
||||
border: none;
|
||||
border-radius: calc(var(--border-radius-medium) - 6px);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill, fill-opacity;
|
||||
|
||||
@@ -474,3 +542,99 @@
|
||||
font-weight: 600;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Boosts section */
|
||||
|
||||
.library-boost-item .library-item-content {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.library-boost-icon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin-inline-end: 8px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.library-boost-icon-image {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 7.5px;
|
||||
}
|
||||
|
||||
/* Slashed-out look when inactive — mirrors the screenshot's first row. */
|
||||
.library-boost-icon[inactive] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.library-boost-icon[inactive]::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to bottom right,
|
||||
transparent calc(50% - 1px),
|
||||
currentColor calc(50% - 1px),
|
||||
currentColor calc(50% + 1px),
|
||||
transparent calc(50% + 1px)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.library-boost-toggle {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
width: 36px;
|
||||
height: 22px;
|
||||
padding: 2px;
|
||||
appearance: none;
|
||||
background: color-mix(in srgb, currentColor 18%, transparent);
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
opacity 0.12s ease;
|
||||
}
|
||||
|
||||
/* Stays visible while it represents an "on" state so the user still sees
|
||||
* which boosts are enabled at a glance; otherwise reveal on row hover. */
|
||||
.library-boost-item:hover .library-boost-toggle,
|
||||
.library-boost-toggle:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.library-boost-item .library-item-content {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.library-boost-toggle[checked] {
|
||||
background: var(--zen-colors-primary);
|
||||
}
|
||||
|
||||
.library-boost-toggle-thumb {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: light-dark(white, rgba(255, 255, 255, 0.95));
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
transition: transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.library-boost-toggle[checked] .library-boost-toggle-thumb {
|
||||
transform: translateX(14px);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(
|
||||
lazy,
|
||||
{
|
||||
ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs",
|
||||
},
|
||||
{ global: "current" }
|
||||
);
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
|
||||
return document.getElementById("zen-browser-background");
|
||||
});
|
||||
@@ -12,6 +20,11 @@ ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
|
||||
return document.getElementById("zen-toolbar-background");
|
||||
});
|
||||
|
||||
// Distance (in swipe-translate units, after the configured multiplier) that
|
||||
// corresponds to a fully open library. Crossing this on swipe-end commits.
|
||||
const LIBRARY_SWIPE_FULL_DISTANCE_FACTOR = 0.6;
|
||||
const LIBRARY_SWIPE_COMMIT_THRESHOLD = 0.3;
|
||||
|
||||
export class ZenSpacesSwipe {
|
||||
_swipeState = {
|
||||
isGestureActive: false,
|
||||
@@ -115,14 +128,25 @@ export class ZenSpacesSwipe {
|
||||
isGestureActive: true,
|
||||
lastDelta: 0,
|
||||
direction: null,
|
||||
librarySwiping: false,
|
||||
libraryStartProgress: 0,
|
||||
libraryProgress: 0,
|
||||
};
|
||||
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", true);
|
||||
}
|
||||
|
||||
_handleSwipeUpdate(event) {
|
||||
const ws = gZenWorkspaces;
|
||||
/** True when the active workspace is the first one in the cached list. */
|
||||
#isAtFirstWorkspace() {
|
||||
const workspaces = gZenWorkspaces.getWorkspaces();
|
||||
const active = gZenWorkspaces.getActiveWorkspaceFromCache();
|
||||
return workspaces.indexOf(active) === 0;
|
||||
}
|
||||
|
||||
if (!ws.workspaceEnabled || !this._swipeState?.isGestureActive) {
|
||||
_handleSwipeUpdate(event) {
|
||||
if (
|
||||
!gZenWorkspaces.workspaceEnabled ||
|
||||
!this._swipeState?.isGestureActive
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -150,13 +174,45 @@ export class ZenSpacesSwipe {
|
||||
}
|
||||
|
||||
if (Math.abs(delta) > 0.9) {
|
||||
delete ws._hasAnimatedBackgrounds;
|
||||
delete gZenWorkspaces._hasAnimatedBackgrounds;
|
||||
this._swipeState.direction = delta > 0 ? "left" : "right";
|
||||
}
|
||||
|
||||
// The library can hijack the swipe in two cases:
|
||||
// - Already open → swipe leftwards (translateX < 0) closes it.
|
||||
// - Closed and on the first workspace → swipe rightwards (translateX > 0)
|
||||
// opens it from the left edge.
|
||||
const libraryOpen = lazy.ZenLibrary.isOpen;
|
||||
const wantsClose = libraryOpen && translateX < 0;
|
||||
const wantsOpen =
|
||||
!libraryOpen && translateX > 0 && this.#isAtFirstWorkspace();
|
||||
if (wantsOpen || wantsClose || this._swipeState.librarySwiping) {
|
||||
if (!this._swipeState.librarySwiping) {
|
||||
this._swipeState.librarySwiping = true;
|
||||
this._swipeState.libraryStartProgress = libraryOpen ? 1 : 0;
|
||||
// Fire-and-forget; the first update before measurement completes
|
||||
// re-applies the current start progress, which is already visible.
|
||||
lazy.ZenLibrary.beginSwipe();
|
||||
}
|
||||
const deltaProgress =
|
||||
translateX / (stripWidth * LIBRARY_SWIPE_FULL_DISTANCE_FACTOR);
|
||||
const progress = Math.max(
|
||||
0,
|
||||
Math.min(1, this._swipeState.libraryStartProgress + deltaProgress)
|
||||
);
|
||||
this._swipeState.libraryProgress = progress;
|
||||
lazy.ZenLibrary.updateSwipeProgress(progress);
|
||||
// Skip the workspace-strip translate while the library owns the swipe.
|
||||
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);
|
||||
const currentWorkspace = gZenWorkspaces.getActiveWorkspaceFromCache();
|
||||
gZenWorkspaces._organizeWorkspaceStripLocations(
|
||||
currentWorkspace,
|
||||
true,
|
||||
translateX
|
||||
);
|
||||
}
|
||||
|
||||
async _handleSwipeEnd(event) {
|
||||
@@ -167,6 +223,17 @@ export class ZenSpacesSwipe {
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// If the swipe was driving the library, commit to whichever side of the
|
||||
// threshold the progress landed on and don't change workspace.
|
||||
if (this._swipeState.librarySwiping) {
|
||||
const targetOpen =
|
||||
this._swipeState.libraryProgress >= LIBRARY_SWIPE_COMMIT_THRESHOLD;
|
||||
delete this._swipeState.librarySwiping;
|
||||
await lazy.ZenLibrary.finishSwipe(targetOpen);
|
||||
return;
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.matches(":-moz-locale-dir(rtl)");
|
||||
const moveForward =
|
||||
(event.direction === SimpleGestureEvent.DIRECTION_RIGHT) !== isRTL;
|
||||
@@ -179,6 +246,10 @@ export class ZenSpacesSwipe {
|
||||
onSwipeGestureAnimationEnd() {
|
||||
const ws = gZenWorkspaces;
|
||||
|
||||
if (this._swipeState.librarySwiping) {
|
||||
lazy.ZenLibrary.finishSwipe(false);
|
||||
}
|
||||
|
||||
// Reset swipe state
|
||||
this._swipeState = {
|
||||
isGestureActive: false,
|
||||
|
||||
Reference in New Issue
Block a user