mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-25 05:36:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca522d448d | ||
|
|
9749703d39 | ||
|
|
ef2ea05af5 | ||
|
|
4d8ce9f3eb | ||
|
|
9db97b76fb | ||
|
|
d7393603ee | ||
|
|
3cff4ec8aa | ||
|
|
e0ec6d6a30 | ||
|
|
ec86f7bf88 | ||
|
|
89ff9f3895 | ||
|
|
ad253cf2af | ||
|
|
e74571eead |
@@ -1,112 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
zen-library-button =
|
||||
.label = Library
|
||||
.tooltiptext = Library
|
||||
|
||||
library-footer-close-button =
|
||||
.tooltiptext = Close library
|
||||
library-footer-donate-button =
|
||||
.tooltiptext = Donate to Zen
|
||||
|
||||
library-history-section-title = History
|
||||
library-downloads-section-title = Downloads
|
||||
library-boosts-section-title = Boosts
|
||||
library-media-section-title = Media
|
||||
library-spaces-section-title = Spaces
|
||||
|
||||
library-filter-button = Filter
|
||||
library-filter-done = Done
|
||||
|
||||
library-history-filter-title = Filter History…
|
||||
library-history-filter-when = When was it visited?
|
||||
library-filter-today = Today
|
||||
library-filter-week = This Week
|
||||
library-filter-month = This Month
|
||||
library-history-filter-sort = Sort by
|
||||
library-history-sort-date = By Date
|
||||
library-history-sort-site = By Site
|
||||
library-history-sort-most-visited = By Most Visited
|
||||
library-history-sort-last-visited = By Last Visited
|
||||
library-history-site-other = Other
|
||||
library-history-search-placeholder =
|
||||
.placeholder = Search History…
|
||||
library-history-empty = No history found
|
||||
library-media-empty = No media found
|
||||
library-media-search-placeholder =
|
||||
.placeholder = Search Media…
|
||||
library-media-loading = Looking for media…
|
||||
library-media-opt-in-title = Your Media in the Library
|
||||
library-media-opt-in-body = Quickly grab screenshots, images and videos from your computer by showing them in the Zen Library.
|
||||
library-media-opt-in-button = Show Media
|
||||
library-media-opt-in-note = Media never leaves your computer and is not viewable by anyone but you.
|
||||
library-media-filter-title = Filter Media…
|
||||
library-media-filter-folders = Where to look
|
||||
library-media-open-in = Open in { $app }
|
||||
library-media-close =
|
||||
.title = Close
|
||||
library-media-previous =
|
||||
.title = Previous
|
||||
library-media-next =
|
||||
.title = Next
|
||||
library-media-more =
|
||||
.title = More options
|
||||
library-media-menu-open =
|
||||
.label = Open
|
||||
library-media-menu-copy =
|
||||
.label = Copy
|
||||
library-history-forget-button =
|
||||
.title = Remove from history
|
||||
library-history-reopen-button =
|
||||
.title = Reopen page
|
||||
|
||||
library-downloads-empty = No downloads found
|
||||
library-downloads-more-button =
|
||||
.title = More options
|
||||
library-downloads-menu-open =
|
||||
.label = Open { $name }
|
||||
library-downloads-menu-copy =
|
||||
.label = Copy { $name }
|
||||
library-downloads-menu-hide =
|
||||
.label = Hide from Zen
|
||||
library-downloads-menu-trash =
|
||||
.label = Move to Trash
|
||||
library-downloads-opening-in = { PLATFORM() ->
|
||||
[macos] Opening in Finder…
|
||||
[windows] Opening in File Explorer…
|
||||
*[other] Opening in file manager…
|
||||
}
|
||||
library-downloads-cancel-button =
|
||||
.title = Cancel download
|
||||
library-downloads-filter-title = Filter Downloads…
|
||||
library-downloads-filter-type = File type
|
||||
library-downloads-filter-when = When was it downloaded?
|
||||
library-downloads-type-images = Images
|
||||
library-downloads-type-video = Video
|
||||
library-downloads-type-audio = Audio
|
||||
library-downloads-type-documents = Documents
|
||||
library-downloads-type-archives = Archives
|
||||
library-downloads-type-apps = Apps
|
||||
library-downloads-retry-button =
|
||||
.title = Retry download
|
||||
|
||||
library-spaces-theme-button =
|
||||
.title = Change theme
|
||||
library-spaces-move-button =
|
||||
.title = Drag to reorder
|
||||
library-spaces-actions-button =
|
||||
.tooltiptext = More options
|
||||
library-spaces-icon-button =
|
||||
.title = Change icon
|
||||
library-spaces-name-button =
|
||||
.title = Rename space
|
||||
|
||||
library-boosts-search-placeholder =
|
||||
.placeholder = Search Boosts…
|
||||
library-boosts-empty = No boosts yet
|
||||
library-boosts-toggle =
|
||||
.aria-label = Enable boost
|
||||
library-boosts-menu-edit =
|
||||
.label = Edit Boost
|
||||
@@ -1,15 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
- name: zen.library.enabled
|
||||
value: "@IS_TWILIGHT@"
|
||||
|
||||
- name: zen.library.last-tab
|
||||
value: "history"
|
||||
|
||||
- name: zen.library.media.enabled
|
||||
value: false
|
||||
|
||||
- name: zen.library.media.folders
|
||||
value: "downloads,documents,pictures"
|
||||
@@ -20,6 +20,8 @@
|
||||
- name: zen.theme.acrylic-elements
|
||||
value: "@IS_TWILIGHT@"
|
||||
|
||||
- name: zen.theme.squircle-browser-view
|
||||
value: "@IS_TWILIGHT@"
|
||||
|
||||
- name: zen.theme.disable-lightweight
|
||||
value: true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/.stylelintignore b/.stylelintignore
|
||||
index 185490999507b8a5032977237af50f5e61c71df1..4b1b1dc76d316f38f60c90e6cbd74d9569800d04 100644
|
||||
index 185490999507b8a5032977237af50f5e61c71df1..77c1635d90c6480af4ebd3c6e3ec49780eaca571 100644
|
||||
--- a/.stylelintignore
|
||||
+++ b/.stylelintignore
|
||||
@@ -106,3 +106,20 @@ build/pgo/blueprint/**/*.css
|
||||
@@ -106,3 +106,19 @@ build/pgo/blueprint/**/*.css
|
||||
# under our control or we don't want to modify at this point:
|
||||
testing/web-platform/mozilla/
|
||||
testing/web-platform/tests/
|
||||
@@ -20,6 +20,5 @@ index 185490999507b8a5032977237af50f5e61c71df1..4b1b1dc76d316f38f60c90e6cbd74d95
|
||||
+zen/split-view/zen-split-view.css
|
||||
+zen/spaces/zen-workspaces.css
|
||||
+zen/common/styles/zen-toolbar.css
|
||||
+zen/common/styles/zen-single-components.css
|
||||
+
|
||||
+*.inc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
index a76bb7aae7636c07989a4acb35281777d3103505..d35aa0457307e95ad00464861fcee2827325193e 100644
|
||||
index a76bb7aae7636c07989a4acb35281777d3103505..c5eafbdad5270cd9926ed4f1d850d134440d775b 100644
|
||||
--- a/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||
@@ -2,7 +2,7 @@
|
||||
@@ -55,11 +55,3 @@ index a76bb7aae7636c07989a4acb35281777d3103505..d35aa0457307e95ad00464861fcee282
|
||||
|
||||
<toolbar id="nav-bar"
|
||||
class="browser-toolbar"
|
||||
@@ -180,7 +188,6 @@
|
||||
<toolbartabstop/>
|
||||
<html:moz-urlbar id="urlbar"
|
||||
class="urlbar"
|
||||
- popover="manual"
|
||||
pageproxystate="invalid"
|
||||
unifiedsearchbutton-available=""
|
||||
sap-name="urlbar">
|
||||
|
||||
@@ -22,4 +22,3 @@
|
||||
#include ../../../zen/live-folders/jar.inc.mn
|
||||
#include ../../../zen/space-routing/jar.inc.mn
|
||||
#include ../../../zen/share/jar.inc.mn
|
||||
#include ../../../zen/library/jar.inc.mn
|
||||
|
||||
@@ -72,6 +72,4 @@
|
||||
<command id="cmd_zenNewLiveFolder" />
|
||||
|
||||
<command id="cmd_zenDuplicateTab" />
|
||||
|
||||
<command id="cmd_zenToggleLibrary" />
|
||||
</commandset>
|
||||
|
||||
@@ -13,5 +13,4 @@
|
||||
<link rel="localization" href="browser/zen-live-folders.ftl"/>
|
||||
<link rel="localization" href="browser/zen-space-routing.ftl"/>
|
||||
<link rel="localization" href="browser/zen-share.ftl"/>
|
||||
<link rel="localization" href="browser/zen-library.ftl"/>
|
||||
</linkset>
|
||||
|
||||
@@ -126,11 +126,8 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
startLayoutExtend() {
|
||||
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
|
||||
// Do not expand if the Urlbar does not support being expanded or it is
|
||||
@@ -3193,8 +3264,16 @@ ${
|
||||
}
|
||||
|
||||
@@ -3195,6 +3266,13 @@ ${
|
||||
this.toggleAttribute("breakout-extend", true);
|
||||
+ this.#showBreakoutPopover();
|
||||
this.#updateTextboxPosition();
|
||||
|
||||
+ this.window.gZenUIManager.onUrlbarOpen();
|
||||
@@ -143,7 +140,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
// Enable the animation only after the first extend call to ensure it
|
||||
// doesn't run when opening a new window.
|
||||
if (!this.hasAttribute("breakout-extend-animate")) {
|
||||
@@ -3218,7 +3297,33 @@ ${
|
||||
@@ -3218,6 +3296,29 @@ ${
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,13 +168,9 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
+ this.removeAttribute("zen-floating-urlbar");
|
||||
+
|
||||
this.toggleAttribute("breakout-extend", false);
|
||||
+ if (this.#isAddressbar) {
|
||||
+ this.#hideBreakoutPopover();
|
||||
+ }
|
||||
this.#updateTextboxPosition();
|
||||
}
|
||||
|
||||
@@ -3256,7 +3361,7 @@ ${
|
||||
@@ -3256,7 +3357,7 @@ ${
|
||||
forceUnifiedSearchButtonAvailable = false
|
||||
) {
|
||||
let prevState = this.getAttribute("pageproxystate");
|
||||
@@ -186,7 +179,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
this.setAttribute("pageproxystate", state);
|
||||
this._inputContainer.setAttribute("pageproxystate", state);
|
||||
this._identityBox?.setAttribute("pageproxystate", state);
|
||||
@@ -3533,7 +3638,11 @@ ${
|
||||
@@ -3533,7 +3634,11 @@ ${
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,38 +192,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
}
|
||||
|
||||
#updateTextboxPositionNextFrame() {
|
||||
@@ -3553,12 +3662,29 @@ ${
|
||||
this.removeAttribute("breakout");
|
||||
this.parentNode.removeAttribute("breakout");
|
||||
this.style.top = "";
|
||||
+ this.#hideBreakoutPopover();
|
||||
+ this._layoutBreakoutUpdateKey = {};
|
||||
+ }
|
||||
+
|
||||
+ #showBreakoutPopover() {
|
||||
+ if (this.#isAddressbar) {
|
||||
+ this.setAttribute("popover", "manual");
|
||||
+ }
|
||||
+ if (!this.matches(":popover-open")) {
|
||||
+ this.showPopover();
|
||||
+ }
|
||||
+ this.#fixAddressbarSearchbarOrder();
|
||||
+ }
|
||||
+
|
||||
+ #hideBreakoutPopover() {
|
||||
try {
|
||||
this.hidePopover();
|
||||
} catch (ex) {
|
||||
// No big deal if not a popover already.
|
||||
}
|
||||
- this._layoutBreakoutUpdateKey = {};
|
||||
+ if (this.#isAddressbar) {
|
||||
+ this.removeAttribute("popover");
|
||||
+ }
|
||||
}
|
||||
|
||||
incrementBreakoutBlockerCount() {
|
||||
@@ -3591,9 +3717,10 @@ ${
|
||||
@@ -3591,9 +3696,10 @@ ${
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,19 +204,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
);
|
||||
|
||||
if (this.#breakoutBlockerCount) {
|
||||
@@ -3602,8 +3729,9 @@ ${
|
||||
|
||||
this.setAttribute("breakout", "true");
|
||||
this.parentNode.setAttribute("breakout", "true");
|
||||
- this.showPopover();
|
||||
- this.#fixAddressbarSearchbarOrder();
|
||||
+ if (!this.#isAddressbar || this.hasAttribute("breakout-extend")) {
|
||||
+ this.#showBreakoutPopover();
|
||||
+ }
|
||||
this.#updateTextboxPosition();
|
||||
|
||||
resolve();
|
||||
@@ -4108,6 +4236,7 @@ ${
|
||||
@@ -4108,6 +4214,7 @@ ${
|
||||
}
|
||||
|
||||
_toggleActionOverride(event) {
|
||||
@@ -262,7 +212,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
if (
|
||||
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
|
||||
event.keyCode == KeyEvent.DOM_VK_ALT ||
|
||||
@@ -4206,9 +4335,10 @@ ${
|
||||
@@ -4206,9 +4313,10 @@ ${
|
||||
if (!this.#isAddressbar) {
|
||||
return val;
|
||||
}
|
||||
@@ -276,7 +226,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
// Only trim value if the directionality doesn't change to RTL and we're not
|
||||
// showing a strikeout https protocol.
|
||||
return UrlbarContentUtils.isTextDirectionRTL(trimmedValue, window) ||
|
||||
@@ -4410,6 +4540,11 @@ ${
|
||||
@@ -4410,6 +4518,11 @@ ${
|
||||
keepViewOpen = false,
|
||||
browserId = null,
|
||||
}) {
|
||||
@@ -288,7 +238,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
let keyDownEnterDeferred;
|
||||
if (
|
||||
this._keyDownEnterDeferred &&
|
||||
@@ -4731,6 +4866,7 @@ ${
|
||||
@@ -4731,6 +4844,7 @@ ${
|
||||
this.setResultForCurrentValue(null);
|
||||
this.handleCommand();
|
||||
this.controller.clearLastQueryContextCache();
|
||||
@@ -296,7 +246,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
|
||||
this._suppressStartQuery = false;
|
||||
});
|
||||
@@ -4742,7 +4878,6 @@ ${
|
||||
@@ -4742,7 +4856,6 @@ ${
|
||||
// Close the results pane, because paste and go doesn't want a result
|
||||
// selection. This has to happen before the menu opens: ending
|
||||
// breakout-extend once it's open keeps it from showing (bug 2037468).
|
||||
@@ -304,7 +254,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
|
||||
let controller =
|
||||
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
|
||||
@@ -5025,7 +5160,11 @@ ${
|
||||
@@ -5025,7 +5138,11 @@ ${
|
||||
if (!engineName && !source && !this.hasAttribute("searchmode")) {
|
||||
return;
|
||||
}
|
||||
@@ -317,7 +267,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
if (this._searchModeIndicatorTitle) {
|
||||
this._searchModeIndicatorTitle.textContent = "";
|
||||
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
|
||||
@@ -5335,6 +5474,7 @@ ${
|
||||
@@ -5335,6 +5452,7 @@ ${
|
||||
|
||||
this.document.l10n.setAttributes(
|
||||
this.inputField,
|
||||
@@ -325,7 +275,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
l10nId,
|
||||
l10nId == "urlbar-placeholder-with-name"
|
||||
? { name: engineName }
|
||||
@@ -5385,6 +5525,12 @@ ${
|
||||
@@ -5385,6 +5503,12 @@ ${
|
||||
}
|
||||
|
||||
logger().debug("Blur Event");
|
||||
@@ -338,7 +288,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
// We cannot count every blur events after a missed engagement as abandoment
|
||||
// because the user may have clicked on some view element that executes
|
||||
// a command causing a focus change. For example opening preferences from
|
||||
@@ -5464,6 +5610,11 @@ ${
|
||||
@@ -5464,6 +5588,11 @@ ${
|
||||
}
|
||||
|
||||
_on_click(event) {
|
||||
@@ -350,7 +300,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
switch (event.target) {
|
||||
case this.inputField:
|
||||
case this._inputContainer:
|
||||
@@ -5558,10 +5709,11 @@ ${
|
||||
@@ -5558,10 +5687,11 @@ ${
|
||||
}
|
||||
if (untrim) {
|
||||
this.setValue(this._untrimmedValue);
|
||||
@@ -363,7 +313,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
this.view.autoOpen({ event });
|
||||
} else {
|
||||
if (this._untrimOnFocusAfterKeydown) {
|
||||
@@ -5605,9 +5757,16 @@ ${
|
||||
@@ -5605,9 +5735,16 @@ ${
|
||||
}
|
||||
|
||||
_on_mousedown(event) {
|
||||
@@ -381,7 +331,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
if (
|
||||
event.composedTarget != this.inputField &&
|
||||
event.composedTarget != this._inputContainer
|
||||
@@ -5617,6 +5776,10 @@ ${
|
||||
@@ -5617,6 +5754,10 @@ ${
|
||||
|
||||
this.focusedViaMousedown = !this.focused;
|
||||
this.#preventClickSelectsAll = this.focused;
|
||||
@@ -392,7 +342,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
|
||||
// Keep the focus status, since the attribute may be changed
|
||||
// upon calling this.focus().
|
||||
@@ -5654,7 +5817,7 @@ ${
|
||||
@@ -5654,7 +5795,7 @@ ${
|
||||
// view open on tab switch, and the TabSelect event arrived earlier.
|
||||
// Also ignore mousedown on the urlbarView context menu: opening/closing the
|
||||
// view is already handled by the result opening flow.
|
||||
@@ -401,7 +351,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5948,7 +6111,7 @@ ${
|
||||
@@ -5948,7 +6089,7 @@ ${
|
||||
// When we are in actions search mode we can show more results so
|
||||
// increase the limit.
|
||||
let maxResults =
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,37 +0,0 @@
|
||||
#filter dumbComments emptyLines substitution
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="context-fill" fill-opacity="context-fill-opacity" class="zen-library-icon" viewBox="4 4 24 24"><style>@property --zen-triangle-anim { syntax: '<number>'; initial-value: 1; inherits: false; }
|
||||
@property --zen-circle-anim { syntax: '<number>'; initial-value: 1; inherits: false; }
|
||||
@keyframes StarBackward {
|
||||
0% { transform: rotate(120deg); }
|
||||
100% { transform: rotate(0deg); }
|
||||
}
|
||||
@keyframes TriangleBackward {
|
||||
0% { --zen-triangle-anim: 1; }
|
||||
100% { --zen-triangle-anim: 0; }
|
||||
}
|
||||
@keyframes CircleBackward {
|
||||
0% { --zen-circle-anim: 1; }
|
||||
100% { --zen-circle-anim: 0; }
|
||||
}
|
||||
@keyframes GlowBackward {
|
||||
0% { opacity: 0.4; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
@keyframes GlowElementsBackward {
|
||||
0% { opacity: 0.5; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
.zen-library-star { transform-origin: 10px 10.2px; animation: StarBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
|
||||
.zen-library-triangle-group { transform-origin: 17px 9.2px; animation: TriangleBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; transform: translate(calc(var(--zen-triangle-anim) * 2px), 0px) rotate(calc(var(--zen-triangle-anim) * 365deg)) scale(max(1 - var(--zen-triangle-anim) / 0.65, (var(--zen-triangle-anim) - 0.65) / 0.35)); }
|
||||
.zen-library-circle { transform-origin: 0px 0px; animation: CircleBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; transform: translate(calc(var(--zen-circle-anim) * 1px), calc(var(--zen-circle-anim) * -1.5px)) scale(max(0, max((var(--zen-circle-anim) - 0.75) / 0.25, min(1, 1 - (var(--zen-circle-anim) - 0.3) / 0.35)))); }
|
||||
.zen-library-glow { filter: blur(2px); animation: GlowBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
|
||||
.zen-library-glow-element { filter: blur(2px); animation: GlowElementsBackward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
|
||||
.zen-library-glow-bg, .zen-library-glow-element { fill: transparent !important; stroke: none !important; }
|
||||
.zen-library-glow-rays { stroke: transparent !important; fill: none !important; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.zen-library-glow-bg, .zen-library-glow-element { fill: white !important; }
|
||||
.zen-library-glow-rays { stroke: white !important; }
|
||||
}</style><defs><mask id="zen-library-box-mask"><rect width="40" height="40" x="-10" y="-10" fill="#fff"/><rect width="40" height="40" x="-10" y="11.635" fill="#000"/></mask></defs><g fill-rule="evenodd"><g mask="url(#zen-library-box-mask)"><path d="M7.773 11.635c.363.228.467.44.445.659l-.182 1.788a.579.579 0 0 0 .946.503l1.38-1.153a.58.58 0 0 1 .604-.085l1.644.725a.578.578 0 0 0 .77-.745l-.679-1.692a.58.58 0 0 1 .115-.577l1.198-1.34a.578.578 0 0 0-.47-.962l-1.794.121a.58.58 0 0 1-.539-.286l-.904-1.553a.578.578 0 0 0-1.06.15l-.439 1.743a.58.58 0 0 1-.438.424l-1.757.38a.578.578 0 0 0-.186 1.055z" class="zen-library-star zen-library-glow-element"/><path d="M7.773 11.635c.363.228.467.44.445.659l-.182 1.788a.579.579 0 0 0 .946.503l1.38-1.153a.58.58 0 0 1 .604-.085l1.644.725a.578.578 0 0 0 .77-.745l-.679-1.692a.58.58 0 0 1 .115-.577l1.198-1.34a.578.578 0 0 0-.47-.962l-1.794.121a.58.58 0 0 1-.539-.286l-.904-1.553a.578.578 0 0 0-1.06.15l-.439 1.743a.58.58 0 0 1-.438.424l-1.757.38a.578.578 0 0 0-.186 1.055z" class="zen-library-star"/></g><g transform="translate(14, 6.2)"><circle r="1.4" class="zen-library-circle zen-library-glow-element"/><circle r="1" class="zen-library-circle"/></g><g class="zen-library-triangle-group"><g transform="translate(17, 9.2)"><path d="M-1.1 1.8c-.3.2-.6 0-.6-.3v-3c0-.3.3-.5.6-.3L1.4-.4c.3.2.3.6 0 .8Z" class="zen-library-triangle zen-library-glow-element"/><path d="M-1.1 1.8c-.3.2-.6 0-.6-.3v-3c0-.3.3-.5.6-.3L1.4-.4c.3.2.3.6 0 .8Z" class="zen-library-triangle"/></g></g><g class="zen-library-glow"><path d="m6.6 6.6 1.44 6.1h9.92l1.44-6.1Z" class="zen-library-glow-bg"/><path d="m6.1 6.6 1.94 6.1h9.92l1.94-6.1" class="zen-library-glow-rays"/></g><path d="M7.218 13.287c0-.456.37-.826.826-.826h9.913c.457 0 .826.37.826.826v4.131a3.304 3.304 0 0 1-3.304 3.304h-4.956a3.305 3.305 0 0 1-3.305-3.304zm3.391 1.239c0-.456.37-.826.826-.826h3.304a.826.826 0 1 1 0 1.652h-3.304a.826.826 0 0 1-.826-.826" class="zen-library-box"/></g></svg>
|
||||
@@ -143,7 +143,6 @@
|
||||
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
|
||||
* skin/classic/browser/zen-icons/translations.svg (../shared/zen-icons/nucleo/translations.svg)
|
||||
* skin/classic/browser/zen-icons/trash.svg (../shared/zen-icons/nucleo/trash.svg)
|
||||
* skin/classic/browser/zen-icons/u-turn-to-left.svg (../shared/zen-icons/nucleo/u-turn-to-left.svg)
|
||||
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
|
||||
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
|
||||
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
|
||||
@@ -295,7 +294,6 @@
|
||||
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
|
||||
* skin/classic/browser/zen-icons/translations.svg (../shared/zen-icons/nucleo/translations.svg)
|
||||
* skin/classic/browser/zen-icons/trash.svg (../shared/zen-icons/nucleo/trash.svg)
|
||||
* skin/classic/browser/zen-icons/u-turn-to-left.svg (../shared/zen-icons/nucleo/u-turn-to-left.svg)
|
||||
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
|
||||
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
|
||||
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
|
||||
@@ -447,7 +445,6 @@
|
||||
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
|
||||
* skin/classic/browser/zen-icons/translations.svg (../shared/zen-icons/nucleo/translations.svg)
|
||||
* skin/classic/browser/zen-icons/trash.svg (../shared/zen-icons/nucleo/trash.svg)
|
||||
* skin/classic/browser/zen-icons/u-turn-to-left.svg (../shared/zen-icons/nucleo/u-turn-to-left.svg)
|
||||
* skin/classic/browser/zen-icons/unpin.svg (../shared/zen-icons/nucleo/unpin.svg)
|
||||
* skin/classic/browser/zen-icons/video-blocked-fill.svg (../shared/zen-icons/nucleo/video-blocked-fill.svg)
|
||||
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
|
||||
@@ -546,11 +543,4 @@
|
||||
* skin/classic/browser/zen-icons/selectable/warning.svg (../shared/zen-icons/common/selectable/warning.svg)
|
||||
* skin/classic/browser/zen-icons/selectable/water.svg (../shared/zen-icons/common/selectable/water.svg)
|
||||
* skin/classic/browser/zen-icons/selectable/weight.svg (../shared/zen-icons/common/selectable/weight.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-boosts-sprite.svg (../shared/zen-icons/common/library/library-boosts-sprite.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-downloads-sprite.svg (../shared/zen-icons/common/library/library-downloads-sprite.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-history-sprite.svg (../shared/zen-icons/common/library/library-history-sprite.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-media-sprite.svg (../shared/zen-icons/common/library/library-media-sprite.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-spaces-sprite.svg (../shared/zen-icons/common/library/library-spaces-sprite.svg)
|
||||
* skin/classic/browser/zen-icons/library/library-sprite.svg (../shared/zen-icons/common/library/library-sprite.svg)
|
||||
* skin/classic/browser/zen-icons/library/library.svg (../shared/zen-icons/common/library/library.svg)
|
||||
skin/classic/browser/zen-icons/icons.css (../shared/zen-icons/icons.css)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#filter dumbComments emptyLines substitution
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If applicable, you may obtain one at
|
||||
# http://mozilla.org/MPL/2.0/.
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="context-fill" fill-opacity="context-fill-opacity" viewBox="0 0 18 18"><path d="M5.22 2.22a.75.75 0 1 1 1.06 1.06L4.06 5.5H12a4.5 4.5 0 0 1 0 9H8.25a.75.75 0 0 1 0-1.5H12a3 3 0 0 0 0-6H4.06l2.22 2.22a.75.75 0 0 1-1.06 1.06l-3.5-3.5a.75.75 0 0 1-.165-.812l.004-.01a1 1 0 0 1 .056-.105 1 1 0 0 1 .105-.133z"/></svg>
|
||||
@@ -60,13 +60,6 @@ do_common_icons() {
|
||||
echo "Working on $filename"
|
||||
echo "* skin/classic/browser/zen-icons/selectable/$filename (../shared/zen-icons/common/selectable/$filename) " >> jar.inc.mn
|
||||
done
|
||||
for filename in common/library/*.svg; do
|
||||
# remove the os/ prefix
|
||||
add_header_to_file $filename
|
||||
filename=$(basename $filename)
|
||||
echo "Working on $filename"
|
||||
echo "* skin/classic/browser/zen-icons/library/$filename (../shared/zen-icons/common/library/$filename) " >> jar.inc.mn
|
||||
done
|
||||
}
|
||||
|
||||
do_icons nucleo WIN # TODO: use windows icons
|
||||
|
||||
@@ -1,886 +0,0 @@
|
||||
diff --git a/gfx/wr/webrender/src/command_buffer.rs b/gfx/wr/webrender/src/command_buffer.rs
|
||||
index 9369cea..8c70d29 100644
|
||||
--- a/gfx/wr/webrender/src/command_buffer.rs
|
||||
+++ b/gfx/wr/webrender/src/command_buffer.rs
|
||||
@@ -476,6 +476,9 @@ pub struct CommandBufferBuilder {
|
||||
|
||||
/// List of render tasks that depend on the task that will be created for this builder.
|
||||
pub extra_dependencies: Vec<RenderTaskId>,
|
||||
+
|
||||
+ /// If true, backdrop-filters on this (tiled) surface also read back the slices below it.
|
||||
+ pub backdrop_reads_lower_slices: bool,
|
||||
}
|
||||
|
||||
impl CommandBufferBuilder {
|
||||
@@ -485,12 +488,14 @@ impl CommandBufferBuilder {
|
||||
establishes_sub_graph: false,
|
||||
resolve_source: None,
|
||||
extra_dependencies: Vec::new(),
|
||||
+ backdrop_reads_lower_slices: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a tiled command buffer builder.
|
||||
pub fn new_tiled(
|
||||
tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
|
||||
+ backdrop_reads_lower_slices: bool,
|
||||
) -> Self {
|
||||
CommandBufferBuilder {
|
||||
kind: CommandBufferBuilderKind::Tiled {
|
||||
@@ -499,6 +504,7 @@ impl CommandBufferBuilder {
|
||||
establishes_sub_graph: false,
|
||||
resolve_source: None,
|
||||
extra_dependencies: Vec::new(),
|
||||
+ backdrop_reads_lower_slices,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +524,7 @@ impl CommandBufferBuilder {
|
||||
establishes_sub_graph,
|
||||
resolve_source: None,
|
||||
extra_dependencies: Vec::new(),
|
||||
+ backdrop_reads_lower_slices: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
diff --git a/gfx/wr/webrender/src/frame_builder.rs b/gfx/wr/webrender/src/frame_builder.rs
|
||||
index 068182d..1442528 100644
|
||||
--- a/gfx/wr/webrender/src/frame_builder.rs
|
||||
+++ b/gfx/wr/webrender/src/frame_builder.rs
|
||||
@@ -17,7 +17,7 @@ use crate::gpu_types::{PrimitiveHeaders, ZBufferIdGenerator};
|
||||
use crate::gpu_types::QuadSegment;
|
||||
use crate::internal_types::{FastHashMap, PlaneSplitter, FrameStamp};
|
||||
use crate::invalidation::DirtyRegion;
|
||||
-use crate::tile_cache::{SliceId, TileCacheInstance};
|
||||
+use crate::tile_cache::{SliceId, TileCacheInstance, update_cross_slice_backdrops};
|
||||
use crate::picture::PictureInstance;
|
||||
use crate::picture::ResolvedSurfaceTexture;
|
||||
use crate::picture::{RasterConfig, PictureScratch};
|
||||
@@ -487,6 +487,25 @@ impl FrameBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
+ if render_picture_cache_slices {
|
||||
+ let slice_ids: Vec<SliceId> = scene.tile_cache_pictures
|
||||
+ .iter()
|
||||
+ .filter_map(|pic_index| {
|
||||
+ match scene.prim_store.pictures[pic_index.0 as usize].raster_config {
|
||||
+ Some(RasterConfig { composite_mode: PictureCompositeMode::TileCache { slice_id }, .. }) => Some(slice_id),
|
||||
+ _ => None,
|
||||
+ }
|
||||
+ })
|
||||
+ .collect();
|
||||
+
|
||||
+ update_cross_slice_backdrops(
|
||||
+ &slice_ids,
|
||||
+ tile_caches,
|
||||
+ &visibility_context,
|
||||
+ &composite_state.compositor_kind,
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
scratch.primitive.frame.assert_draws_resolved();
|
||||
|
||||
profile.end_time(profiler::FRAME_VISIBILITY_TIME);
|
||||
diff --git a/gfx/wr/webrender/src/picture.rs b/gfx/wr/webrender/src/picture.rs
|
||||
index 9b100ee..8f476e5 100644
|
||||
--- a/gfx/wr/webrender/src/picture.rs
|
||||
+++ b/gfx/wr/webrender/src/picture.rs
|
||||
@@ -140,7 +140,8 @@ use std::ops::Range;
|
||||
use crate::picture_textures::PictureCacheTextureHandle;
|
||||
use crate::util::{MaxRect, Recycler, ScaleOffset};
|
||||
use crate::tile_cache::{SliceDebugInfo, TileDebugInfo, DirtyTileDebugInfo, CompositorClipDebugInfo};
|
||||
-use crate::tile_cache::{SliceId, TileCacheInstance, TileSurface, NativeSurface};
|
||||
+use crate::tile_cache::{SliceId, Tile, TileCacheInstance, TileSurface, NativeSurface};
|
||||
+use crate::render_target::{LowerSliceSource, LowerSliceSourceKind};
|
||||
use crate::tile_cache::{BackdropKind, BackdropSurface};
|
||||
use crate::tile_cache::{TileKey, SubSliceIndex};
|
||||
use crate::invalidation::InvalidationReason;
|
||||
@@ -1660,6 +1661,32 @@ fn prepare_tiled_picture_surface(
|
||||
let device_pixel_scale = frame_state
|
||||
.surfaces[surface_index.0]
|
||||
.device_pixel_scale;
|
||||
+
|
||||
+ let pic_to_root = if tile_cache.feeds_cross_slice_backdrop {
|
||||
+ frame_context.spatial_tree
|
||||
+ .get_relative_transform(tile_cache.spatial_node_index, frame_context.root_spatial_node_index)
|
||||
+ .as_2d_scale_offset()
|
||||
+ } else {
|
||||
+ None
|
||||
+ };
|
||||
+ let rounded_clip = tile_cache.compositor_clip.map(|clip_index| {
|
||||
+ let clip = frame_state.composite_state.get_compositor_clip(clip_index);
|
||||
+ (clip.rect.cast_unit(), clip.radius)
|
||||
+ });
|
||||
+ let local_clip_rect = tile_cache.local_clip_rect;
|
||||
+ let lower_slice_source = move |tile: &Tile, kind: LowerSliceSourceKind, rect: PictureRect, sub_slice_index: usize| {
|
||||
+ let pic_to_root = pic_to_root?;
|
||||
+ let clip_rect = local_clip_rect
|
||||
+ .intersection(&tile.cached_surface.current_descriptor.local_valid_rect)?;
|
||||
+
|
||||
+ Some(LowerSliceSource {
|
||||
+ kind,
|
||||
+ rect: pic_to_root.map_rect(&rect),
|
||||
+ clip_rect: pic_to_root.map_rect(&clip_rect),
|
||||
+ rounded_clip,
|
||||
+ sub_slice_index,
|
||||
+ })
|
||||
+ };
|
||||
let mut at_least_one_tile_visible = false;
|
||||
|
||||
// Get the overall device space rect of the picture cache. Used to clip
|
||||
@@ -1784,6 +1811,19 @@ fn prepare_tiled_picture_surface(
|
||||
|
||||
at_least_one_tile_visible = true;
|
||||
|
||||
+ if tile.feeds_cross_slice_backdrop {
|
||||
+ if let Some(TileSurface::Color { color }) = tile.surface {
|
||||
+ if let Some(source) = lower_slice_source(
|
||||
+ tile,
|
||||
+ LowerSliceSourceKind::Color(color),
|
||||
+ tile.cached_surface.current_descriptor.local_valid_rect,
|
||||
+ sub_slice_index,
|
||||
+ ) {
|
||||
+ frame_state.surface_builder.lower_slice_sources.push(source);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
if let TileSurface::Texture { descriptor, .. } = tile.surface.as_mut().unwrap() {
|
||||
match descriptor {
|
||||
SurfaceTextureDescriptor::TextureCache { ref handle, .. } => {
|
||||
@@ -1972,7 +2012,8 @@ fn prepare_tiled_picture_surface(
|
||||
// TODO(gw): As a performance optimization, we could skip the resolve picture
|
||||
// if the dirty rect is the same as the resolve rect (probably quite
|
||||
// common for effects that scroll underneath a backdrop-filter, for example).
|
||||
- let use_tile_composite = !tile.cached_surface.sub_graphs.is_empty();
|
||||
+ let use_tile_composite = !tile.cached_surface.sub_graphs.is_empty() ||
|
||||
+ tile.feeds_cross_slice_backdrop;
|
||||
|
||||
if use_tile_composite {
|
||||
let mut local_content_rect = tile.cached_surface.local_dirty_rect;
|
||||
@@ -2056,12 +2097,24 @@ fn prepare_tiled_picture_surface(
|
||||
),
|
||||
);
|
||||
|
||||
+ let lower_slice_source = if tile.feeds_cross_slice_backdrop {
|
||||
+ lower_slice_source(
|
||||
+ tile,
|
||||
+ LowerSliceSourceKind::Task(render_task_id),
|
||||
+ (content_device_rect.to_f32() / device_pixel_scale).cast_unit(),
|
||||
+ sub_slice_index,
|
||||
+ )
|
||||
+ } else {
|
||||
+ None
|
||||
+ };
|
||||
+
|
||||
surface_render_tasks.insert(
|
||||
tile_key,
|
||||
SurfaceTileDescriptor {
|
||||
current_task_id: render_task_id,
|
||||
composite_task_id: Some(composite_task_id),
|
||||
dirty_rect: tile.cached_surface.local_dirty_rect,
|
||||
+ lower_slice_source,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
@@ -2097,6 +2150,7 @@ fn prepare_tiled_picture_surface(
|
||||
current_task_id: render_task_id,
|
||||
composite_task_id: None,
|
||||
dirty_rect: tile.cached_surface.local_dirty_rect,
|
||||
+ lower_slice_source: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -2262,7 +2316,10 @@ fn prepare_tiled_picture_surface(
|
||||
);
|
||||
}
|
||||
|
||||
- let descriptor = SurfaceDescriptor::new_tiled(surface_render_tasks);
|
||||
+ let descriptor = SurfaceDescriptor::new_tiled(
|
||||
+ surface_render_tasks,
|
||||
+ tile_cache.backdrop_reads_lower_slices,
|
||||
+ );
|
||||
|
||||
frame_state.surface_builder.push_surface(
|
||||
surface_index,
|
||||
diff --git a/gfx/wr/webrender/src/render_target.rs b/gfx/wr/webrender/src/render_target.rs
|
||||
index 2185365..d3ca780 100644
|
||||
--- a/gfx/wr/webrender/src/render_target.rs
|
||||
+++ b/gfx/wr/webrender/src/render_target.rs
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
|
||||
use api::units::*;
|
||||
-use api::{ColorF, LineOrientation, BorderStyle};
|
||||
+use api::{BorderRadius, ColorF, LineOrientation, BorderStyle};
|
||||
use crate::batch::{AlphaBatchBuilder, AlphaBatchContainer, BatchTextures, TextureSet};
|
||||
use crate::batch::{BatchBuilder, INVALID_SEGMENT_INDEX, ClipMaskInstanceList};
|
||||
use crate::render_task::{SubTask, RectangleClipSubTask, ImageClipSubTask};
|
||||
@@ -631,6 +631,33 @@ pub struct ResolveOp {
|
||||
/// back the region the backdrop actually covers rather than one offset by
|
||||
/// the difference between the two raster roots.
|
||||
pub dest_to_src_raster: ScaleOffset,
|
||||
+ /// Content of the picture cache slices below the backdrop root, drawn bottom-up
|
||||
+ /// under the content of the src tasks.
|
||||
+ pub lower_slice_sources: Vec<LowerSliceSource>,
|
||||
+ /// Maps a rect from the dest surface's raster space into the root coordinate system.
|
||||
+ pub dest_raster_to_root: ScaleOffset,
|
||||
+}
|
||||
+
|
||||
+#[cfg_attr(feature = "capture", derive(Serialize))]
|
||||
+#[cfg_attr(feature = "replay", derive(Deserialize))]
|
||||
+#[derive(Debug, PartialEq, Clone)]
|
||||
+pub enum LowerSliceSourceKind {
|
||||
+ Task(RenderTaskId),
|
||||
+ Color(ColorF),
|
||||
+}
|
||||
+
|
||||
+/// Content of a picture cache tile that a backdrop-filter in a slice above it reads
|
||||
+/// back.
|
||||
+#[cfg_attr(feature = "capture", derive(Serialize))]
|
||||
+#[cfg_attr(feature = "replay", derive(Deserialize))]
|
||||
+#[derive(Debug, PartialEq, Clone)]
|
||||
+pub struct LowerSliceSource {
|
||||
+ pub kind: LowerSliceSourceKind,
|
||||
+ /// The rect covered by the task's content, or by the color.
|
||||
+ pub rect: LayoutRect,
|
||||
+ pub clip_rect: LayoutRect,
|
||||
+ pub rounded_clip: Option<(LayoutRect, BorderRadius)>,
|
||||
+ pub sub_slice_index: usize,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "capture", derive(Serialize))]
|
||||
diff --git a/gfx/wr/webrender/src/renderer/composite.rs b/gfx/wr/webrender/src/renderer/composite.rs
|
||||
index 032a0e1..dd91394 100644
|
||||
--- a/gfx/wr/webrender/src/renderer/composite.rs
|
||||
+++ b/gfx/wr/webrender/src/renderer/composite.rs
|
||||
@@ -6,6 +6,7 @@ use api::units::*;
|
||||
use api::{ColorF, ImageBufferKind, ImageRendering, PremultipliedColorF};
|
||||
use crate::batch::BatchTextures;
|
||||
use crate::composite::{
|
||||
+ CompositorClip,
|
||||
CompositeState, CompositeSurfaceFormat, CompositeTileSurface, CompositorConfig,
|
||||
CompositorInputLayer, CompositorSurfaceUsage, CompositorSurfaceTransform,
|
||||
CompositeRoundedCorner, NativeTileId, ResolvedExternalSurface,
|
||||
@@ -20,7 +21,11 @@ use crate::debug_item::DebugItem;
|
||||
use crate::segment::EdgeMask;
|
||||
use crate::device::DrawTarget;
|
||||
use crate::gpu_types::{CompositeInstance, ZBufferId};
|
||||
-use crate::internal_types::{FastHashMap, TextureSource};
|
||||
+use crate::internal_types::{FastHashMap, Swizzle, TextureSource};
|
||||
+use crate::render_target::{LowerSliceSourceKind, ResolveOp};
|
||||
+use crate::render_task::RenderTaskKind;
|
||||
+use crate::render_task_graph::RenderTaskGraph;
|
||||
+use api::BorderRadius;
|
||||
use crate::picture::ResolvedSurfaceTexture;
|
||||
use super::RenderResults;
|
||||
use crate::profiler::{self};
|
||||
@@ -435,6 +440,134 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
+ pub(super) fn draw_resolve_with_lower_slices(
|
||||
+ &mut self,
|
||||
+ resolve_op: &ResolveOp,
|
||||
+ render_tasks: &RenderTaskGraph,
|
||||
+ draw_target: DrawTarget,
|
||||
+ projection: &default::Transform3D<f32>,
|
||||
+ src_copies: &[(TextureSource, DeviceIntRect, DeviceIntRect)],
|
||||
+ stats: &mut RendererStats,
|
||||
+ ) {
|
||||
+ let dest_task = &render_tasks[resolve_op.dest_task_id];
|
||||
+ let dest_info = match dest_task.kind {
|
||||
+ RenderTaskKind::Picture(ref info) => info,
|
||||
+ _ => panic!("bug: not a picture"),
|
||||
+ };
|
||||
+ let dest_task_rect = DeviceRect::from_origin_and_size(
|
||||
+ dest_task.get_target_rect().min.to_f32(),
|
||||
+ dest_info.content_size.to_f32(),
|
||||
+ );
|
||||
+ let dest_offset = dest_task_rect.min.to_vector() - dest_info.content_origin.to_vector();
|
||||
+ let root_to_dest = |rect: &LayoutRect| -> DeviceRect {
|
||||
+ let raster: WorldRect = resolve_op.dest_raster_to_root.unmap_rect(rect);
|
||||
+ let device: DeviceRect = (raster * dest_info.device_pixel_scale).cast_unit();
|
||||
+ device.translate(dest_offset)
|
||||
+ };
|
||||
+ let radius_scale = dest_info.device_pixel_scale.0 / resolve_op.dest_raster_to_root.scale.x;
|
||||
+
|
||||
+ let mut draws = Vec::new();
|
||||
+
|
||||
+ for source in &resolve_op.lower_slice_sources {
|
||||
+ let clip_rect = match root_to_dest(&source.clip_rect).intersection(&dest_task_rect) {
|
||||
+ Some(clip_rect) => clip_rect,
|
||||
+ None => continue,
|
||||
+ };
|
||||
+ let rect = root_to_dest(&source.rect);
|
||||
+ let clip = source.rounded_clip.map(|(clip_rect, radius)| {
|
||||
+ CompositorClip {
|
||||
+ rect: root_to_dest(&clip_rect),
|
||||
+ radius: BorderRadius {
|
||||
+ top_left: radius.top_left * radius_scale,
|
||||
+ top_right: radius.top_right * radius_scale,
|
||||
+ bottom_left: radius.bottom_left * radius_scale,
|
||||
+ bottom_right: radius.bottom_right * radius_scale,
|
||||
+ ..radius
|
||||
+ },
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ match source.kind {
|
||||
+ LowerSliceSourceKind::Task(task_id) => {
|
||||
+ let task = &render_tasks[task_id];
|
||||
+ let texture = TextureSource::TextureCache(task.get_target_texture(), Swizzle::default());
|
||||
+ let uv_rect = task.get_target_rect().to_f32();
|
||||
+ let instance = CompositeInstance::new_rgb(
|
||||
+ rect,
|
||||
+ clip_rect,
|
||||
+ PremultipliedColorF::WHITE,
|
||||
+ TexelRect::new(uv_rect.min.x, uv_rect.min.y, uv_rect.max.x, uv_rect.max.y),
|
||||
+ false,
|
||||
+ (false, false),
|
||||
+ clip.as_ref(),
|
||||
+ );
|
||||
+ let texture_size = self.texture_resolver.get_texture_size(&texture).to_f32();
|
||||
+ draws.push((instance, texture, Some(texture_size)));
|
||||
+ }
|
||||
+ LowerSliceSourceKind::Color(color) => {
|
||||
+ let instance = CompositeInstance::new(
|
||||
+ rect,
|
||||
+ clip_rect,
|
||||
+ color.premultiplied(),
|
||||
+ (false, false),
|
||||
+ clip.as_ref(),
|
||||
+ );
|
||||
+ draws.push((instance, TextureSource::Dummy, None));
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ for (texture, src, dest) in src_copies {
|
||||
+ let dest = dest.to_f32();
|
||||
+ let src = src.to_f32();
|
||||
+ let instance = CompositeInstance::new_rgb(
|
||||
+ dest,
|
||||
+ dest,
|
||||
+ PremultipliedColorF::WHITE,
|
||||
+ TexelRect::new(src.min.x, src.min.y, src.max.x, src.max.y),
|
||||
+ false,
|
||||
+ (false, false),
|
||||
+ None,
|
||||
+ );
|
||||
+ let texture_size = self.texture_resolver.get_texture_size(texture).to_f32();
|
||||
+ draws.push((instance, *texture, Some(texture_size)));
|
||||
+ }
|
||||
+
|
||||
+ self.device.bind_draw_target(draw_target);
|
||||
+ self.device.disable_depth();
|
||||
+ self.device.disable_depth_write();
|
||||
+ self.set_blend(true, FramebufferKind::Other);
|
||||
+ self.set_blend_mode_premultiplied_alpha(FramebufferKind::Other);
|
||||
+
|
||||
+ for (instance, texture, texture_size) in draws {
|
||||
+ let features = instance.get_rgb_features();
|
||||
+
|
||||
+ self.shaders
|
||||
+ .borrow_mut()
|
||||
+ .get_composite_shader(
|
||||
+ CompositeSurfaceFormat::Rgba,
|
||||
+ texture.image_buffer_kind(),
|
||||
+ features,
|
||||
+ ).bind(
|
||||
+ &mut self.device,
|
||||
+ projection,
|
||||
+ texture_size,
|
||||
+ &mut self.renderer_errors,
|
||||
+ &mut self.profile,
|
||||
+ &mut self.command_log,
|
||||
+ );
|
||||
+
|
||||
+ self.draw_instanced_batch(
|
||||
+ &[instance],
|
||||
+ VertexArrayKind::Composite,
|
||||
+ &BatchTextures::composite_rgb(texture),
|
||||
+ stats,
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ self.set_blend(false, FramebufferKind::Other);
|
||||
+ }
|
||||
+
|
||||
// Composite tiles in a swapchain. When using LayerCompositor, we may
|
||||
// split the compositing in to multiple swapchains.
|
||||
fn composite_pass(
|
||||
diff --git a/gfx/wr/webrender/src/renderer/mod.rs b/gfx/wr/webrender/src/renderer/mod.rs
|
||||
index d177263..a45ec70 100644
|
||||
--- a/gfx/wr/webrender/src/renderer/mod.rs
|
||||
+++ b/gfx/wr/webrender/src/renderer/mod.rs
|
||||
@@ -2288,6 +2288,8 @@ impl Renderer {
|
||||
resolve_ops: &[ResolveOp],
|
||||
render_tasks: &RenderTaskGraph,
|
||||
draw_target: DrawTarget,
|
||||
+ projection: &default::Transform3D<f32>,
|
||||
+ stats: &mut RendererStats,
|
||||
) {
|
||||
if resolve_ops.is_empty() {
|
||||
return;
|
||||
@@ -2300,6 +2302,8 @@ impl Renderer {
|
||||
resolve_op,
|
||||
render_tasks,
|
||||
draw_target,
|
||||
+ projection,
|
||||
+ stats,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2729,7 +2733,12 @@ impl Renderer {
|
||||
resolve_op: &ResolveOp,
|
||||
render_tasks: &RenderTaskGraph,
|
||||
draw_target: DrawTarget,
|
||||
+ projection: &default::Transform3D<f32>,
|
||||
+ stats: &mut RendererStats,
|
||||
) {
|
||||
+ let has_lower_slices = !resolve_op.lower_slice_sources.is_empty();
|
||||
+ let mut src_copies = Vec::new();
|
||||
+
|
||||
for src_task_id in &resolve_op.src_task_ids {
|
||||
let src_task = &render_tasks[*src_task_id];
|
||||
let src_info = match src_task.kind {
|
||||
@@ -2818,6 +2827,12 @@ impl Renderer {
|
||||
src_task.get_target_texture(),
|
||||
Swizzle::default(),
|
||||
);
|
||||
+
|
||||
+ if has_lower_slices {
|
||||
+ src_copies.push((texture_source, src, dest));
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
let (cache_texture, _) = self.texture_resolver
|
||||
.resolve(&texture_source).expect("bug: no source texture");
|
||||
|
||||
@@ -2836,6 +2851,17 @@ impl Renderer {
|
||||
);
|
||||
}
|
||||
}
|
||||
+
|
||||
+ if has_lower_slices {
|
||||
+ self.draw_resolve_with_lower_slices(
|
||||
+ resolve_op,
|
||||
+ render_tasks,
|
||||
+ draw_target,
|
||||
+ projection,
|
||||
+ &src_copies,
|
||||
+ stats,
|
||||
+ );
|
||||
+ }
|
||||
}
|
||||
|
||||
fn draw_picture_cache_target(
|
||||
@@ -3385,6 +3411,8 @@ impl Renderer {
|
||||
&target.resolve_ops,
|
||||
render_tasks,
|
||||
draw_target,
|
||||
+ &projection,
|
||||
+ stats,
|
||||
);
|
||||
|
||||
// Handle any blits from the texture cache to this target.
|
||||
diff --git a/gfx/wr/webrender/src/surface.rs b/gfx/wr/webrender/src/surface.rs
|
||||
index 20536bb..f8a8709 100644
|
||||
--- a/gfx/wr/webrender/src/surface.rs
|
||||
+++ b/gfx/wr/webrender/src/surface.rs
|
||||
@@ -14,7 +14,7 @@ use crate::picture_composite_mode::PictureCompositeMode;
|
||||
use crate::tile_cache::{TileKey, SubSliceIndex, MAX_COMPOSITOR_SURFACES};
|
||||
use crate::prim_store::PictureIndex;
|
||||
use crate::render_task_graph::{RenderTaskId, RenderTaskGraphBuilder};
|
||||
-use crate::render_target::ResolveOp;
|
||||
+use crate::render_target::{LowerSliceSource, LowerSliceSourceKind, ResolveOp};
|
||||
use crate::render_task::{RenderTask, RenderTaskKind, RenderTaskLocation};
|
||||
use crate::space::SpaceMapper;
|
||||
use crate::spatial_tree::{CoordinateSpaceMapping, SpatialTree, SpatialNodeIndex};
|
||||
@@ -380,6 +380,8 @@ pub struct SurfaceTileDescriptor {
|
||||
pub composite_task_id: Option<RenderTaskId>,
|
||||
/// Dirty rect for this tile
|
||||
pub dirty_rect: PictureRect,
|
||||
+ /// Set if this tile's content is read back by a backdrop-filter in a slice above.
|
||||
+ pub lower_slice_source: Option<LowerSliceSource>,
|
||||
}
|
||||
|
||||
// Details of how a surface is rendered
|
||||
@@ -387,6 +389,7 @@ pub enum SurfaceDescriptorKind {
|
||||
// Picture cache tiles
|
||||
Tiled {
|
||||
tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
|
||||
+ backdrop_reads_lower_slices: bool,
|
||||
},
|
||||
// A single surface (e.g. for an opacity filter)
|
||||
Simple {
|
||||
@@ -410,10 +413,12 @@ impl SurfaceDescriptor {
|
||||
// Create a picture cache tiled surface
|
||||
pub fn new_tiled(
|
||||
tiles: FastHashMap<TileKey, SurfaceTileDescriptor>,
|
||||
+ backdrop_reads_lower_slices: bool,
|
||||
) -> Self {
|
||||
SurfaceDescriptor {
|
||||
kind: SurfaceDescriptorKind::Tiled {
|
||||
tiles,
|
||||
+ backdrop_reads_lower_slices,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -526,6 +531,9 @@ pub struct SurfaceBuilder {
|
||||
// A map of the output render tasks from any sub-graphs that haven't
|
||||
// been consumed by BackdropRender prims yet
|
||||
pub sub_graph_output_map: FastHashMap<PictureIndex, RenderTaskId>,
|
||||
+ // Content of the picture cache slices built so far that backdrop-filters in
|
||||
+ // slices above them read back, ordered bottom-up
|
||||
+ pub lower_slice_sources: Vec<LowerSliceSource>,
|
||||
}
|
||||
|
||||
impl SurfaceBuilder {
|
||||
@@ -534,6 +542,7 @@ impl SurfaceBuilder {
|
||||
current_cmd_buffers: CommandBufferTargets::new(),
|
||||
builder_stack: Vec::new(),
|
||||
sub_graph_output_map: FastHashMap::default(),
|
||||
+ lower_slice_sources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,9 +583,10 @@ impl SurfaceBuilder {
|
||||
|
||||
let builder = if let Some(descriptor) = descriptor {
|
||||
match descriptor.kind {
|
||||
- SurfaceDescriptorKind::Tiled { tiles } => {
|
||||
+ SurfaceDescriptorKind::Tiled { tiles, backdrop_reads_lower_slices } => {
|
||||
CommandBufferBuilder::new_tiled(
|
||||
tiles,
|
||||
+ backdrop_reads_lower_slices,
|
||||
)
|
||||
}
|
||||
SurfaceDescriptorKind::Simple { render_task_id, dirty_rect, .. } => {
|
||||
@@ -700,6 +710,7 @@ impl SurfaceBuilder {
|
||||
// Get info about the resolve operation to copy from parent surface or tiles to the picture cache task
|
||||
if let Some(resolve_task_id) = builder.resolve_source {
|
||||
let mut src_task_ids = Vec::new();
|
||||
+ let reads_lower_slices = self.builder_stack.last().unwrap().backdrop_reads_lower_slices;
|
||||
|
||||
// Make the output of the sub-graph a dependency of the new replacement tile task
|
||||
let _old = self.sub_graph_output_map.insert(
|
||||
@@ -869,6 +880,27 @@ impl SurfaceBuilder {
|
||||
&src_task_ids,
|
||||
);
|
||||
|
||||
+ let mut lower_slice_sources = Vec::new();
|
||||
+ let mut dest_raster_to_root = ScaleOffset::identity();
|
||||
+
|
||||
+ if reads_lower_slices && !self.lower_slice_sources.is_empty() {
|
||||
+ let dest_raster = raster_spatial_node(rg_builder, resolve_task_id);
|
||||
+ let to_root = spatial_tree
|
||||
+ .get_relative_transform(dest_raster, spatial_tree.root_reference_frame_index())
|
||||
+ .as_2d_scale_offset();
|
||||
+
|
||||
+ if let Some(to_root) = to_root {
|
||||
+ dest_raster_to_root = to_root;
|
||||
+ lower_slice_sources = self.lower_slice_sources.clone();
|
||||
+
|
||||
+ for source in &lower_slice_sources {
|
||||
+ if let LowerSliceSourceKind::Task(task_id) = source.kind {
|
||||
+ rg_builder.add_dependency(resolve_task_id, task_id);
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
let dest_task = rg_builder.get_task_mut(resolve_task_id);
|
||||
|
||||
match dest_task.kind {
|
||||
@@ -878,6 +910,8 @@ impl SurfaceBuilder {
|
||||
src_task_ids,
|
||||
dest_task_id: resolve_task_id,
|
||||
dest_to_src_raster,
|
||||
+ lower_slice_sources,
|
||||
+ dest_raster_to_root,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
@@ -917,7 +951,16 @@ impl SurfaceBuilder {
|
||||
} else {
|
||||
match builder.kind {
|
||||
CommandBufferBuilderKind::Tiled { ref tiles } => {
|
||||
+ let first_source = self.lower_slice_sources.len();
|
||||
+
|
||||
for (_, descriptor) in tiles {
|
||||
+ if let Some(ref source) = descriptor.lower_slice_source {
|
||||
+ self.lower_slice_sources.push(LowerSliceSource {
|
||||
+ kind: LowerSliceSourceKind::Task(descriptor.current_task_id),
|
||||
+ ..source.clone()
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
if let Some(composite_task_id) = descriptor.composite_task_id {
|
||||
rg_builder.add_dependency(
|
||||
composite_task_id,
|
||||
@@ -933,6 +976,8 @@ impl SurfaceBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
+
|
||||
+ self.lower_slice_sources[first_source ..].sort_by_key(|source| source.sub_slice_index);
|
||||
}
|
||||
CommandBufferBuilderKind::Simple { render_task_id: child_task_id, root_task_id: child_root_task_id, .. } => {
|
||||
match self.builder_stack.last().unwrap().kind {
|
||||
diff --git a/gfx/wr/webrender/src/tile_cache/mod.rs b/gfx/wr/webrender/src/tile_cache/mod.rs
|
||||
index 731be26..d20e6ac 100644
|
||||
--- a/gfx/wr/webrender/src/tile_cache/mod.rs
|
||||
+++ b/gfx/wr/webrender/src/tile_cache/mod.rs
|
||||
@@ -198,6 +198,8 @@ pub struct TileCacheParams {
|
||||
// This is only a suggestion - the tile cache will clamp this as a reasonable number
|
||||
// and only promote a limited number of surfaces.
|
||||
pub yuv_image_surface_count: usize,
|
||||
+ // If true, backdrop-filters in this slice also read back content from the slices below it.
|
||||
+ pub backdrop_reads_lower_slices: bool,
|
||||
}
|
||||
|
||||
/// The backing surface for this tile.
|
||||
@@ -250,6 +252,8 @@ pub struct Tile {
|
||||
pub cached_surface: CachedSurface,
|
||||
/// Raster-space rect for this tile, cached to avoid recomputing per primitive.
|
||||
pub local_raster_rect: RasterRect,
|
||||
+ /// If true, this tile is read back this frame by a slice above.
|
||||
+ pub feeds_cross_slice_backdrop: bool,
|
||||
}
|
||||
|
||||
impl Tile {
|
||||
@@ -269,6 +273,7 @@ impl Tile {
|
||||
z_id: ZBufferId::invalid(),
|
||||
cached_surface: CachedSurface::new(),
|
||||
local_raster_rect: RasterRect::zero(),
|
||||
+ feeds_cross_slice_backdrop: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -828,6 +833,15 @@ pub struct TileCacheInstance {
|
||||
pub yuv_images_remaining: usize,
|
||||
/// Persistent cache for computing and storing raster-space primitive corners.
|
||||
corners_cache: CornersCache,
|
||||
+ /// If true, backdrop-filters in this slice also read back content from the slices below it.
|
||||
+ pub backdrop_reads_lower_slices: bool,
|
||||
+ /// The picture-space coverage rects of the backdrop-filters found in this slice this frame.
|
||||
+ backdrop_render_rects: Vec<PictureRect>,
|
||||
+ /// The picture to root transform of this slice in the previous frame, used to detect
|
||||
+ /// movement relative to backdrop-filters in slices above.
|
||||
+ prev_pic_to_root: Option<ScaleOffset>,
|
||||
+ /// If true, some tiles of this slice are read back by a backdrop-filter in a slice above.
|
||||
+ pub feeds_cross_slice_backdrop: bool,
|
||||
}
|
||||
|
||||
impl TileCacheInstance {
|
||||
@@ -893,6 +907,10 @@ impl TileCacheInstance {
|
||||
yuv_images_count: params.yuv_image_surface_count,
|
||||
yuv_images_remaining: 0,
|
||||
corners_cache: CornersCache::new(),
|
||||
+ backdrop_reads_lower_slices: params.backdrop_reads_lower_slices,
|
||||
+ backdrop_render_rects: Vec::new(),
|
||||
+ prev_pic_to_root: None,
|
||||
+ feeds_cross_slice_backdrop: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,6 +988,7 @@ impl TileCacheInstance {
|
||||
self.background_color = params.background_color;
|
||||
self.shared_clip_leaf_id = params.shared_clip_leaf_id;
|
||||
self.shared_clip_node_id = params.shared_clip_node_id;
|
||||
+ self.backdrop_reads_lower_slices = params.backdrop_reads_lower_slices;
|
||||
|
||||
// Since the slice flags may have changed, ensure we re-evaluate the
|
||||
// appropriate tile size for this cache next update.
|
||||
@@ -1040,6 +1059,7 @@ impl TileCacheInstance {
|
||||
self.local_rect = pic_rect;
|
||||
self.local_clip_rect = PictureRect::max_rect();
|
||||
self.deferred_dirty_tests.clear();
|
||||
+ self.backdrop_render_rects.clear();
|
||||
self.mix_blend_pic_rects.clear();
|
||||
self.underlays.clear();
|
||||
self.overlay_region = PictureRect::zero();
|
||||
@@ -2667,6 +2687,8 @@ impl TileCacheInstance {
|
||||
}
|
||||
}
|
||||
|
||||
+ self.backdrop_render_rects.push(pic_coverage_rect);
|
||||
+
|
||||
// For backdrop-filter, we need to check if any of the dirty rects
|
||||
// in tiles that are affected by the filter primitive are dirty.
|
||||
self.deferred_dirty_tests.push(DeferredDirtyTest {
|
||||
@@ -3264,6 +3286,159 @@ impl TileCacheInstance {
|
||||
}
|
||||
|
||||
|
||||
+fn slice_is_dirty_in_rect(tile_cache: &TileCacheInstance, rect: &PictureRect) -> bool {
|
||||
+ tile_cache.sub_slices.iter().any(|sub_slice| {
|
||||
+ sub_slice.tiles.values().any(|tile| {
|
||||
+ tile.is_visible &&
|
||||
+ !tile.cached_surface.is_valid &&
|
||||
+ tile.cached_surface.local_dirty_rect.intersects(rect)
|
||||
+ })
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
+fn invalidate_slice_rect(
|
||||
+ tile_cache: &mut TileCacheInstance,
|
||||
+ rect: &PictureRect,
|
||||
+ supports_dirty_rects: bool,
|
||||
+ feeds_cross_slice_backdrop: bool,
|
||||
+) {
|
||||
+ let mut rects = vec![*rect];
|
||||
+
|
||||
+ rects.extend(
|
||||
+ tile_cache.backdrop_render_rects
|
||||
+ .iter()
|
||||
+ .filter(|backdrop_rect| backdrop_rect.intersects(rect))
|
||||
+ .cloned()
|
||||
+ );
|
||||
+
|
||||
+ for sub_slice in &mut tile_cache.sub_slices {
|
||||
+ for tile in sub_slice.tiles.values_mut() {
|
||||
+ if !tile.is_visible {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ for rect in &rects {
|
||||
+ if !tile.cached_surface.local_rect.intersects(rect) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ tile.invalidate(Some(*rect), InvalidationReason::SurfaceContentChanged);
|
||||
+ if !supports_dirty_rects {
|
||||
+ tile.cached_surface.local_dirty_rect = tile.cached_surface.local_rect;
|
||||
+ }
|
||||
+ tile.feeds_cross_slice_backdrop |= feeds_cross_slice_backdrop;
|
||||
+ tile_cache.feeds_cross_slice_backdrop |= feeds_cross_slice_backdrop;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+pub fn update_cross_slice_backdrops(
|
||||
+ slice_ids: &[SliceId],
|
||||
+ tile_caches: &mut FastHashMap<SliceId, Box<TileCacheInstance>>,
|
||||
+ frame_context: &FrameVisibilityContext,
|
||||
+ compositor_kind: &CompositorKind,
|
||||
+) {
|
||||
+ let supports_dirty_rects = match compositor_kind {
|
||||
+ CompositorKind::Draw { .. } | CompositorKind::Layer { .. } => {
|
||||
+ frame_context.config.gpu_supports_render_target_partial_update
|
||||
+ }
|
||||
+ CompositorKind::Native { capabilities, .. } => {
|
||||
+ capabilities.max_update_rects > 0
|
||||
+ }
|
||||
+ };
|
||||
+
|
||||
+ let any_backdrop = slice_ids.iter().any(|slice_id| {
|
||||
+ let tile_cache = &tile_caches[slice_id];
|
||||
+ tile_cache.backdrop_reads_lower_slices && !tile_cache.backdrop_render_rects.is_empty()
|
||||
+ });
|
||||
+
|
||||
+ let mut pic_to_root = Vec::with_capacity(slice_ids.len());
|
||||
+ let mut moved = Vec::with_capacity(slice_ids.len());
|
||||
+
|
||||
+ for slice_id in slice_ids {
|
||||
+ let tile_cache = tile_caches.get_mut(slice_id).expect("bug: non-existent tile cache");
|
||||
+
|
||||
+ if tile_cache.feeds_cross_slice_backdrop {
|
||||
+ tile_cache.feeds_cross_slice_backdrop = false;
|
||||
+ for sub_slice in &mut tile_cache.sub_slices {
|
||||
+ for tile in sub_slice.tiles.values_mut() {
|
||||
+ tile.feeds_cross_slice_backdrop = false;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if !any_backdrop {
|
||||
+ tile_cache.prev_pic_to_root = None;
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ let transform = frame_context.spatial_tree
|
||||
+ .get_relative_transform(tile_cache.spatial_node_index, frame_context.root_spatial_node_index)
|
||||
+ .as_2d_scale_offset();
|
||||
+
|
||||
+ moved.push(transform != tile_cache.prev_pic_to_root);
|
||||
+ tile_cache.prev_pic_to_root = transform;
|
||||
+ pic_to_root.push(transform);
|
||||
+ }
|
||||
+
|
||||
+ if !any_backdrop {
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ for top in 1 .. slice_ids.len() {
|
||||
+ let top_transform = match pic_to_root[top] {
|
||||
+ Some(transform) => transform,
|
||||
+ None => continue,
|
||||
+ };
|
||||
+
|
||||
+ let backdrop_rects = {
|
||||
+ let tile_cache = &tile_caches[&slice_ids[top]];
|
||||
+ if !tile_cache.backdrop_reads_lower_slices {
|
||||
+ continue;
|
||||
+ }
|
||||
+ tile_cache.backdrop_render_rects.clone()
|
||||
+ };
|
||||
+
|
||||
+ for backdrop_rect in backdrop_rects {
|
||||
+ let root_rect: LayoutRect = top_transform.map_rect(&backdrop_rect);
|
||||
+
|
||||
+ let lower_rects: Vec<(usize, PictureRect)> = (0 .. top)
|
||||
+ .filter_map(|lower| {
|
||||
+ let rect: PictureRect = pic_to_root[lower]?.unmap_rect(&root_rect);
|
||||
+ Some((lower, rect))
|
||||
+ })
|
||||
+ .collect();
|
||||
+
|
||||
+ let needs_update = moved[top] ||
|
||||
+ slice_is_dirty_in_rect(&tile_caches[&slice_ids[top]], &backdrop_rect) ||
|
||||
+ lower_rects.iter().any(|(lower, rect)| {
|
||||
+ moved[*lower] || slice_is_dirty_in_rect(&tile_caches[&slice_ids[*lower]], rect)
|
||||
+ });
|
||||
+
|
||||
+ if !needs_update {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+ invalidate_slice_rect(
|
||||
+ tile_caches.get_mut(&slice_ids[top]).unwrap(),
|
||||
+ &backdrop_rect,
|
||||
+ supports_dirty_rects,
|
||||
+ false,
|
||||
+ );
|
||||
+
|
||||
+ for (lower, rect) in lower_rects {
|
||||
+ invalidate_slice_rect(
|
||||
+ tile_caches.get_mut(&slice_ids[lower]).unwrap(),
|
||||
+ &rect,
|
||||
+ supports_dirty_rects,
|
||||
+ true,
|
||||
+ );
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
/// A SubSlice represents a potentially overlapping set of tiles within a picture cache. Most
|
||||
/// picture cache instances will have only a single sub-slice. The exception to this is when
|
||||
/// a picture cache has compositor surfaces, in which case sub slices are used to interleave
|
||||
diff --git a/gfx/wr/webrender/src/tile_cache/slice_builder.rs b/gfx/wr/webrender/src/tile_cache/slice_builder.rs
|
||||
index f81edc9..d8759bc 100644
|
||||
--- a/gfx/wr/webrender/src/tile_cache/slice_builder.rs
|
||||
+++ b/gfx/wr/webrender/src/tile_cache/slice_builder.rs
|
||||
@@ -688,6 +688,7 @@ fn create_tile_cache(
|
||||
virtual_surface_size: frame_builder_config.compositor_kind.get_virtual_surface_size(),
|
||||
image_surface_count: prim_list.image_surface_count,
|
||||
yuv_image_surface_count: prim_list.yuv_image_surface_count,
|
||||
+ backdrop_reads_lower_slices: iframe_clip.is_none() && slice > 0,
|
||||
});
|
||||
|
||||
let pic_index = prim_store.pictures.alloc().init(PictureInstance::new_image(
|
||||
@@ -13,9 +13,10 @@
|
||||
"name": "Issue 15123"
|
||||
},
|
||||
{
|
||||
"type": "phabricator",
|
||||
"id": "D327914",
|
||||
"name": "Backdrop filter reads web content"
|
||||
"type": "local",
|
||||
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
|
||||
// once it gets accepted.
|
||||
"path": "firefox/allow_backdrop_to_work_on_transparency.patch"
|
||||
},
|
||||
{
|
||||
"type": "local",
|
||||
|
||||
@@ -69,9 +69,7 @@ export class nsZenBoostEditor {
|
||||
* @returns {ZenBoostsParent} Boost JSActor parent
|
||||
*/
|
||||
get zenBoostsParent() {
|
||||
const linkedBrowser =
|
||||
this.editorWindow.browser ??
|
||||
this.openerWindow.gBrowser.selectedTab.linkedBrowser;
|
||||
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
|
||||
const actor =
|
||||
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
|
||||
return actor;
|
||||
|
||||
@@ -606,12 +606,9 @@ class nsZenBoostsManager {
|
||||
* @param {Window} parentWindow - The parent browser window
|
||||
* @param {Boost} boost - The boost which will be edited
|
||||
* @param {nsIURI} domainUri - The boost which will be edited
|
||||
* @param {object} [options]
|
||||
* @param {Element} [options.browser] - Browser showing the site when it is
|
||||
* not the selected tab, e.g. a detached glance
|
||||
* @returns {Window|null} The instanced editor window
|
||||
*/
|
||||
openBoostWindow(parentWindow, boost, domainUri, { browser = null } = {}) {
|
||||
openBoostWindow(parentWindow, boost, domainUri) {
|
||||
if (!this.canBoostSite(domainUri)) {
|
||||
console.error(
|
||||
"[ZenBoostsManager] Cannot open editor for boost with invalid domain."
|
||||
@@ -651,51 +648,38 @@ class nsZenBoostsManager {
|
||||
null
|
||||
);
|
||||
|
||||
// Close the editor if the tab is switched
|
||||
const onTabSelect = editor.close.bind(editor);
|
||||
parentWindow.gBrowser.tabContainer.addEventListener(
|
||||
"TabSelect",
|
||||
onTabSelect,
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
|
||||
const progressListener = {
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIWebProgressListener",
|
||||
"nsISupportsWeakReference",
|
||||
]),
|
||||
onLocationChange: webProgress => {
|
||||
if (webProgress.isTopLevel) {
|
||||
editor.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
const onTabSelect = editor.close.bind(editor);
|
||||
if (browser) {
|
||||
browser.addProgressListener(progressListener);
|
||||
editor.addEventListener(
|
||||
"unload",
|
||||
() => {
|
||||
browser.removeProgressListener(progressListener);
|
||||
editor.browser = null;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
} else {
|
||||
// Close the editor if the tab is switched or navigates.
|
||||
parentWindow.gBrowser.tabContainer.addEventListener(
|
||||
"TabSelect",
|
||||
onTabSelect,
|
||||
{ once: true }
|
||||
);
|
||||
parentWindow.gBrowser.addProgressListener(progressListener);
|
||||
editor.addEventListener(
|
||||
"unload",
|
||||
() => {
|
||||
parentWindow.gBrowser.tabContainer.removeEventListener(
|
||||
"TabSelect",
|
||||
onTabSelect
|
||||
);
|
||||
parentWindow.gBrowser.removeProgressListener(progressListener);
|
||||
editor.browser = null;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
editor.browser = browser;
|
||||
parentWindow.gBrowser.addProgressListener(progressListener);
|
||||
|
||||
editor.addEventListener(
|
||||
"unload",
|
||||
() => {
|
||||
parentWindow.gBrowser.tabContainer.removeEventListener(
|
||||
"TabSelect",
|
||||
onTabSelect
|
||||
);
|
||||
parentWindow.gBrowser.removeProgressListener(progressListener);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
// Give the domain
|
||||
editor.domain = domain;
|
||||
editor.openerWindow = parentWindow;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
content/browser/zen-styles/zen-browser-ui.css (../../zen/common/styles/zen-browser-ui.css)
|
||||
content/browser/zen-styles/zen-animations.css (../../zen/common/styles/zen-animations.css)
|
||||
content/browser/zen-styles/zen-panel-ui.css (../../zen/common/styles/zen-panel-ui.css)
|
||||
* content/browser/zen-styles/zen-single-components.css (../../zen/common/styles/zen-single-components.css)
|
||||
content/browser/zen-styles/zen-single-components.css (../../zen/common/styles/zen-single-components.css)
|
||||
content/browser/zen-styles/zen-sidebar.css (../../zen/common/styles/zen-sidebar.css)
|
||||
* content/browser/zen-styles/zen-toolbar.css (../../zen/common/styles/zen-toolbar.css)
|
||||
content/browser/zen-styles/zen-browser-container.css (../../zen/common/styles/zen-browser-container.css)
|
||||
@@ -31,7 +31,6 @@
|
||||
content/browser/zen-styles/zen-popup.css (../../zen/common/styles/zen-popup.css)
|
||||
content/browser/zen-styles/zen-branding.css (../../zen/common/styles/zen-branding.css)
|
||||
content/browser/zen-styles/zen-sidebar-notification.css (../../zen/common/styles/zen-sidebar-notification.css)
|
||||
* content/browser/zen-styles/zen-backdrop-filters.svg (../../zen/common/styles/zen-backdrop-filters.svg)
|
||||
|
||||
content/browser/zen-styles/zen-panels/bookmarks.css (../../zen/common/styles/zen-panels/bookmarks.css)
|
||||
content/browser/zen-styles/zen-panels/print.css (../../zen/common/styles/zen-panels/print.css)
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
% file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
color-scheme: dark;
|
||||
--zen-backdrop-underlay: url("chrome://browser/content/zen-styles/zen-backdrop-filters.svg#underlay-dark");
|
||||
--zen-urlbar-outline-offset: -2px;
|
||||
--zen-urlbar-filter: blur(40px) saturate(110%) brightness(25%) contrast(100%);
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
% file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
color-scheme: light;
|
||||
--zen-backdrop-underlay: url("chrome://browser/content/zen-styles/zen-backdrop-filters.svg#underlay-light");
|
||||
--zen-urlbar-outline-offset: 0px;
|
||||
--zen-urlbar-filter: blur(40px) saturate(110%) brightness(75%) contrast(100%);
|
||||
|
||||
@@ -4,38 +4,6 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@keyframes library-badge-download {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
20% {
|
||||
transform: translateY(12px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zen-library-sprite-hover {
|
||||
from {
|
||||
background-position-x: 0%;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position-x: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zen-library-sprite-unhover {
|
||||
from {
|
||||
background-position-x: 100%;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position-x: 0%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zen-back-and-forth-text {
|
||||
0%,
|
||||
10% {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#filter dumbComments emptyLines substitution
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<filter id="underlay-dark" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-color="black" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="underlay-light" x="0" y="0" width="100%" height="100%" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-color="white" />
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</svg>
|
||||
@@ -9,9 +9,13 @@
|
||||
overflow: clip;
|
||||
|
||||
:root:not([zen-no-padding="true"]) &:not(.zen-glance-overlay) {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: round;
|
||||
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
|
||||
border-radius: var(--zen-native-inner-radius);
|
||||
box-shadow: var(--zen-big-shadow);
|
||||
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: round;
|
||||
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
|
||||
}
|
||||
}
|
||||
|
||||
& browser[type="content"] {
|
||||
@@ -30,6 +34,18 @@
|
||||
color-scheme: env(-moz-content-preferred-color-scheme);
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line media-query-no-invalid */
|
||||
@media -moz-pref("zen.theme.acrylic-elements") {
|
||||
& browser[type="content"] {
|
||||
/* For the rendering engine to apply layering optimizations. This
|
||||
* is a hacky solution, but it works for now. Note that this makes
|
||||
* zen REALLY slow, so lets not enable it by default until we have
|
||||
* a better solution. */
|
||||
translate: 0;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line media-query-no-invalid */
|
||||
|
||||
@@ -100,14 +100,14 @@ body,
|
||||
display: flex;
|
||||
opacity: var(--zen-grainy-background-opacity, 0);
|
||||
|
||||
&:not([swipe-gesture="true"]) {
|
||||
:root:not([swipe-gesture="true"]) & {
|
||||
/* note: Keep in sync with kGlobalAnimationDuration */
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#tabbrowser-arrowscrollbox[swipe-gesture] {
|
||||
:root[swipe-gesture] #tabbrowser-arrowscrollbox {
|
||||
/* See gh-13703 for rationale. */
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ body,
|
||||
}
|
||||
|
||||
& > * {
|
||||
z-index: var(--zen-main-app-child-z-index, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line media-query-no-invalid */
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
.zen-library-media-preview {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
--zen-main-app-child-z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zen-library-media-preview-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: light-dark(rgba(255, 255, 255, 0.55), rgba(0, 0, 0, 0.45));
|
||||
}
|
||||
|
||||
.zen-library-media-preview-panel {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min(78vw, 1100px);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: light-dark(rgb(245, 245, 247), rgb(28, 30, 32));
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.zen-library-media-preview-header {
|
||||
--zen-toolbar-button-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
flex: none;
|
||||
color: light-dark(rgb(28, 30, 32), rgb(233, 234, 237));
|
||||
}
|
||||
|
||||
.zen-library-media-preview-name {
|
||||
margin-inline-start: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zen-library-media-preview-date {
|
||||
font-size: 13px;
|
||||
opacity: 0.6;
|
||||
margin-inline-end: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zen-library-media-preview .toolbarbutton-1 > .toolbarbutton-icon {
|
||||
box-sizing: border-box;
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--toolbarbutton-icon-fill);
|
||||
}
|
||||
|
||||
.zen-library-media-preview-open {
|
||||
& > .toolbarbutton-text {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--toolbarbutton-border-radius);
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover > .toolbarbutton-text {
|
||||
background: color-mix(in srgb, currentColor 20%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-media-preview-stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: min(64vh, 620px);
|
||||
padding: 16px;
|
||||
background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.28));
|
||||
border-top: 1px solid light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.07));
|
||||
}
|
||||
|
||||
.zen-library-media-preview-media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -1,313 +0,0 @@
|
||||
|
||||
@property --zen-library-progress {
|
||||
syntax: "<number>";
|
||||
inherits: true;
|
||||
initial-value: 0;
|
||||
}
|
||||
|
||||
#zen-sidebar-foot-buttons,
|
||||
#tabbrowser-tabs {
|
||||
--zen-library-open-duration: 0.3s;
|
||||
--zen-library-open-easing: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
--zen-library-entry-easing: linear(0, 0.34 12%, 0.7 24%, 0.93 38%, 1.03 55%, 1);
|
||||
transition: --zen-library-progress var(--zen-library-open-duration) var(--zen-library-open-easing);
|
||||
|
||||
&[zen-library-stack-open] {
|
||||
--zen-library-progress: 1;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-library-button {
|
||||
position: relative;
|
||||
|
||||
--zen-library-sprite-size: 18px;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: var(--zen-library-sprite-size);
|
||||
height: var(--zen-library-sprite-size);
|
||||
background-image: url("chrome://browser/skin/zen-icons/library/library-sprite.svg");
|
||||
scale: 1.08;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&::before {
|
||||
animation: zen-library-sprite-unhover 0.3s steps(36, jump-none);
|
||||
}
|
||||
&:hover::before {
|
||||
animation: zen-library-sprite-hover 0.35s steps(36, jump-none) forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#zen-library-button > .toolbarbutton-badge-stack {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: none !important;
|
||||
pointer-events: none;
|
||||
|
||||
&[downloading] {
|
||||
animation: 0.4s library-badge-download ease;
|
||||
}
|
||||
|
||||
/* The badge flies from its spot on the button to the newest entry's badge
|
||||
* along an arc, the target being set as the list opens. */
|
||||
& #library-button-badge {
|
||||
position: absolute;
|
||||
margin: 0 !important;
|
||||
transform-origin: left top;
|
||||
|
||||
--zen-library-badge-x: 14px;
|
||||
--zen-library-badge-y: -8px;
|
||||
--zen-library-badge-to-x: var(--zen-library-badge-x);
|
||||
--zen-library-badge-to-y: var(--zen-library-badge-y);
|
||||
--zen-library-badge-mid-x: calc(
|
||||
(var(--zen-library-badge-x) + var(--zen-library-badge-to-x)) / 2 +
|
||||
abs(var(--zen-library-badge-to-y) - var(--zen-library-badge-y)) / 2
|
||||
);
|
||||
--zen-library-badge-mid-y: calc(
|
||||
(var(--zen-library-badge-y) + var(--zen-library-badge-to-y)) / 2 -
|
||||
abs(var(--zen-library-badge-to-x) - var(--zen-library-badge-x)) / 2
|
||||
);
|
||||
--zen-library-left: calc(1 - var(--zen-library-progress));
|
||||
left: calc(
|
||||
var(--zen-library-left) * var(--zen-library-left) * var(--zen-library-badge-x) +
|
||||
2 * var(--zen-library-left) * var(--zen-library-progress) * var(--zen-library-badge-mid-x) +
|
||||
var(--zen-library-progress) * var(--zen-library-progress) * var(--zen-library-badge-to-x)
|
||||
);
|
||||
top: calc(
|
||||
var(--zen-library-left) * var(--zen-library-left) * var(--zen-library-badge-y) +
|
||||
2 * var(--zen-library-left) * var(--zen-library-progress) * var(--zen-library-badge-mid-y) +
|
||||
var(--zen-library-progress) * var(--zen-library-progress) * var(--zen-library-badge-to-y)
|
||||
);
|
||||
|
||||
&:not([downloading]) {
|
||||
transform: scale(calc((var(--zen-library-progress) * 0.5 + 0.5) * 100%));
|
||||
}
|
||||
|
||||
&[downloading] {
|
||||
transform: scale(calc((var(--zen-library-progress) * 0.2 + 0.8) * 100%));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#library-button-badge,
|
||||
#zen-sidebar-foot-buttons[zen-library-badge] #zen-library-download-list > :last-child .zen-library-download-badge {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#zen-sidebar-foot-buttons[zen-library-badge] {
|
||||
& #library-button-badge {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&[zen-library-stack-open] {
|
||||
& #library-button-badge {
|
||||
visibility: hidden;
|
||||
transition: visibility 0s var(--zen-library-open-duration);
|
||||
}
|
||||
|
||||
& #zen-library-download-list > :last-child .zen-library-download-badge {
|
||||
visibility: visible;
|
||||
transition: visibility 0s var(--zen-library-open-duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-download-badge {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zen-library-download-badge:not([downloading]) {
|
||||
background-image: var(--download-image);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: var(--border-radius-xsmall);
|
||||
background-size: cover;
|
||||
scale: 2;
|
||||
|
||||
& .zen-library-download-progress {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-download-badge[downloading] {
|
||||
will-change: transform;
|
||||
align-self: start;
|
||||
transform: translateX(-2px);
|
||||
|
||||
--border-color: color-mix(in srgb, var(--zen-primary-color), light-dark(rgba(0,0,0,.8), white) 70%);
|
||||
|
||||
background-color: color-mix(in srgb, var(--zen-primary-color), light-dark(white, rgba(0,0,0,.5)) 80%);
|
||||
border: 3px solid transparent;
|
||||
box-shadow: 0 0 6px 2px rgba(0,0,0,.1);
|
||||
color: var(--border-color);
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 100%;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.zen-library-download-badge[downloading]::after {
|
||||
background-image: url("chrome://browser/skin/zen-icons/forward.svg");
|
||||
rotate: 90deg;
|
||||
|
||||
position: absolute;
|
||||
content: '';
|
||||
background-size: contain;
|
||||
inset: 2px;
|
||||
fill: currentColor;
|
||||
-moz-context-properties: fill;
|
||||
}
|
||||
|
||||
.zen-library-download-progress {
|
||||
position: absolute;
|
||||
inset: 1px;
|
||||
border-radius: 50%;
|
||||
|
||||
--badge-size: 25px;
|
||||
--border-width: 4px;
|
||||
--size: calc(var(--badge-size) + 2 * var(--border-width));
|
||||
margin: calc(-1 * var(--border-width));
|
||||
|
||||
--thickness: 3px;
|
||||
--progress: var(--zen-accent-button-background);
|
||||
|
||||
background:
|
||||
conic-gradient(
|
||||
from 0deg,
|
||||
var(--progress) calc(var(--value) * 1%),
|
||||
transparent 0
|
||||
)
|
||||
border-box,
|
||||
var(--border-color) border-box;
|
||||
|
||||
mask: radial-gradient(
|
||||
circle closest-side,
|
||||
transparent calc(100% - var(--thickness)),
|
||||
black calc(100% - var(--thickness))
|
||||
);
|
||||
}
|
||||
|
||||
#zen-sidebar-foot-buttons:is([zen-library-stack-open], [zen-library-stack-closing]) {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#zen-library-download-list {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
|
||||
visibility: hidden;
|
||||
transition: visibility 0s var(--zen-library-open-duration);
|
||||
|
||||
#zen-sidebar-foot-buttons[zen-library-stack-open] & {
|
||||
visibility: visible;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
padding-top: 10px;
|
||||
gap: 4px;
|
||||
|
||||
.zen-library-download-list-download {
|
||||
container-type: inline-size;
|
||||
-moz-window-dragging: no-drag;
|
||||
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding: 0 4px;
|
||||
|
||||
--zen-library-progress: 0;
|
||||
--zen-library-entry-index: 0;
|
||||
--zen-library-entry-delay: calc(var(--zen-library-entry-index) * 12ms);
|
||||
--zen-library-entry-settle: 0s;
|
||||
transition:
|
||||
--zen-library-progress 0.22s var(--zen-library-entry-easing) var(--zen-library-entry-delay),
|
||||
pointer-events 0s var(--zen-library-entry-settle) allow-discrete;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
&:nth-last-child(2) {
|
||||
--zen-library-entry-index: 1;
|
||||
}
|
||||
&:nth-last-child(3) {
|
||||
--zen-library-entry-index: 2;
|
||||
}
|
||||
&:nth-last-child(4) {
|
||||
--zen-library-entry-index: 3;
|
||||
}
|
||||
|
||||
#zen-sidebar-foot-buttons[zen-library-stack-open] & {
|
||||
--zen-library-progress: 1;
|
||||
--zen-library-entry-settle: calc(0.22s + var(--zen-library-entry-delay));
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
transform: translateY(calc((1 - var(--zen-library-progress)) * 25px));
|
||||
opacity: var(--zen-library-progress);
|
||||
|
||||
&:hover {
|
||||
background: var(--zen-toolbar-element-bg);
|
||||
}
|
||||
|
||||
& .zen-library-download-list-title-container {
|
||||
@container (max-width: 50px) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
margin: auto 0;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
|
||||
& .zen-library-download-list-title {
|
||||
font-size: 11pt;
|
||||
opacity: 0.9;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
& .zen-library-download-list-subtitle {
|
||||
font-size: 9pt;
|
||||
opacity: 0.6;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
& .zen-library-download-badge {
|
||||
margin: auto 20px auto 8px;
|
||||
@container (max-width: 50px) {
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& .zen-library-download-action {
|
||||
display: none;
|
||||
margin: auto 4px auto 0;
|
||||
|
||||
@container (max-width: 50px) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[downloading] .zen-library-download-action {
|
||||
display: flex;
|
||||
list-style-image: url("chrome://browser/skin/zen-icons/close.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.urlbar {
|
||||
--urlbarview-separator-color: light-dark(rgb(233, 233, 233), rgb(46 46 47));
|
||||
--urlbarview-separator-color: light-dark(hsl(0, 0%, 85%), hsl(0, 0%, 15%));
|
||||
--urlbarview-background-color-hover: var(--toolbarbutton-background-color-hover);
|
||||
border-radius: calc(var(--toolbarbutton-border-radius) - 2px);
|
||||
--urlbarView-results-padding: 10px !important;
|
||||
@@ -27,9 +27,6 @@
|
||||
--urlbar-margin-inline: 5px;
|
||||
--urlbar-container-padding: 4px;
|
||||
|
||||
--urlbar-icon-border-radius: 8px !important;
|
||||
--urlbar-inner-border-radius: 8px !important;
|
||||
|
||||
&[breakout-extend] {
|
||||
--urlbar-container-padding: 0px;
|
||||
}
|
||||
@@ -277,31 +274,29 @@
|
||||
}
|
||||
|
||||
& .urlbar-background {
|
||||
--zen-urlbar-background-base: light-dark(rgb(255, 255, 255), rgb(24 24 24));
|
||||
--zen-urlbar-background-base: light-dark(#fbfbfb, color-mix(in srgb, hsl(0, 0%, 6.7%), var(--zen-colors-primary) 30%));
|
||||
/* stylelint-disable-next-line media-query-no-invalid */
|
||||
@media -moz-pref("zen.theme.acrylic-elements") and (not (prefers-reduced-transparency: reduce)) {
|
||||
&:not(:-moz-window-inactive) {
|
||||
--zen-urlbar-background-transparent: light-dark(
|
||||
rgba(255, 255, 255, .93),
|
||||
rgba(20, 20, 20, .93)
|
||||
);
|
||||
}
|
||||
--zen-urlbar-background-transparent: light-dark(
|
||||
color-mix(in srgb, white 80%, transparent),
|
||||
color-mix(in srgb, var(--zen-urlbar-background-base) 65%, transparent)
|
||||
);
|
||||
}
|
||||
background-color: var(--zen-urlbar-background-transparent, var(--zen-urlbar-background-base)) !important;
|
||||
box-shadow: 0 30px 140px -15px light-dark(rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.6)) !important;
|
||||
backdrop-filter: none !important;
|
||||
outline: 0.5px solid light-dark(rgba(0, 0, 0, 0.2), rgb(75, 75, 75)) !important;
|
||||
outline: 0.5px solid light-dark(rgba(0, 0, 0, 0.2), rgba(255, 255, 255, 0.1)) !important;
|
||||
outline-offset: var(--zen-urlbar-outline-offset) !important;
|
||||
|
||||
/* stylelint-disable-next-line media-query-no-invalid */
|
||||
@media -moz-pref("zen.theme.acrylic-elements") {
|
||||
backdrop-filter: var(--zen-backdrop-underlay) blur(40px) !important;
|
||||
backdrop-filter: var(--zen-urlbar-filter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
.urlbar-background {
|
||||
border-radius: 15px !important;
|
||||
border-radius: 14px !important;
|
||||
}
|
||||
|
||||
&[breakout-extend][animate-searchmode="true"]::before {
|
||||
@@ -321,6 +316,9 @@
|
||||
}
|
||||
|
||||
:root[zen-single-toolbar="true"] {
|
||||
--urlbar-icon-border-radius: 8px !important;
|
||||
--urlbar-inner-border-radius: 6px !important;
|
||||
|
||||
#identity-permission-box > *:not(#zen-site-data-icon-button) {
|
||||
visibility: collapse;
|
||||
}
|
||||
@@ -512,7 +510,7 @@
|
||||
#urlbar[open][zen-floating-urlbar="true"] #identity-icon-box,
|
||||
:root[zen-single-toolbar="true"] #urlbar[breakout-extend] #identity-icon-box {
|
||||
--urlbar-box-hover-bgcolor: transparent;
|
||||
margin-inline: 2px 4px;
|
||||
margin-inline: 2px 8px;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -538,8 +536,8 @@
|
||||
.urlbarView-favicon {
|
||||
margin-left: 0 !important;
|
||||
margin-right: 12px !important;
|
||||
padding: 4px !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 6px !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
.urlbarView-userContext {
|
||||
@@ -654,9 +652,7 @@
|
||||
|
||||
.urlbarView-row {
|
||||
--urlbarView-row-padding-inline: 8px;
|
||||
--urlbarview-row-padding-block: 12px;
|
||||
--urlbarView-row-gutter: 0px;
|
||||
|
||||
--urlbarview-row-padding-block: 10px;
|
||||
color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)) !important;
|
||||
|
||||
&:hover,
|
||||
@@ -666,7 +662,7 @@
|
||||
}
|
||||
|
||||
&[selected] {
|
||||
--zen-selected-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.3), rgba(255, 255, 255, 0.1)));
|
||||
--zen-selected-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.1)));
|
||||
--zen-selected-color: color-mix(in srgb, var(--zen-selected-bg), black 30%);
|
||||
background-color: var(--zen-selected-bg) !important;
|
||||
|
||||
@@ -698,7 +694,7 @@
|
||||
}
|
||||
|
||||
#urlbar-results {
|
||||
max-height: 244px;
|
||||
max-height: 252px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
margin-block-start: calc(var(--urlbarView-results-padding) - 2px);
|
||||
|
||||
@@ -800,16 +800,3 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#tabbrowser-tabs:is([zen-library-stack-open], [zen-library-stack-closing]) {
|
||||
--gradient-pos: calc(100% - var(--zen-library-stack-height, 0px) + 20px);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
black
|
||||
calc(var(--gradient-pos) - 50px),
|
||||
color-mix(transparent calc(var(--zen-library-progress) * 100%), black)
|
||||
var(--gradient-pos));
|
||||
}
|
||||
|
||||
%include zen-library-widget.css
|
||||
%include zen-library-media-preview.css
|
||||
|
||||
@@ -4,25 +4,17 @@
|
||||
|
||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
ZenLibraryWidget: "moz-src:///zen/library/ZenLibraryWidget.sys.mjs",
|
||||
});
|
||||
|
||||
export const ZenCustomizableUI = new (class {
|
||||
constructor() {}
|
||||
|
||||
TYPE_TOOLBAR = "toolbar";
|
||||
defaultSidebarIcons = [
|
||||
Services.prefs.getBoolPref("zen.library.enabled")
|
||||
? "zen-library-button"
|
||||
: "downloads-button",
|
||||
"downloads-button",
|
||||
"zen-workspaces-button",
|
||||
"zen-create-new-button",
|
||||
];
|
||||
|
||||
startup(CustomizableUIInternal) {
|
||||
CustomizableUIInternal.createBuiltinWidget(lazy.ZenLibraryWidget);
|
||||
CustomizableUIInternal.registerArea(
|
||||
"zen-sidebar-top-buttons",
|
||||
{
|
||||
|
||||
@@ -7,8 +7,6 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
CustomizableUI:
|
||||
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
|
||||
SessionStore:
|
||||
"moz-src:///browser/components/sessionstore/SessionStore.sys.mjs",
|
||||
});
|
||||
@@ -25,7 +23,6 @@ class nsZenUIMigration {
|
||||
console.error("ZenUIMigration: Error during migration", e);
|
||||
}
|
||||
}
|
||||
this.#migrateLibraryButton();
|
||||
this.clearVariables();
|
||||
if (this.shouldRestart) {
|
||||
Services.startup.quit(
|
||||
@@ -54,41 +51,6 @@ class nsZenUIMigration {
|
||||
this._migrationVersion = this.MIGRATION_VERSION;
|
||||
}
|
||||
|
||||
#migrateLibraryButton() {
|
||||
const donePref = "zen.library.migrated-downloads-button";
|
||||
if (
|
||||
!Services.prefs.getBoolPref("zen.library.enabled", false) ||
|
||||
Services.prefs.getBoolPref(donePref, false)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// A toolbar's saved placements are only there once a window has it.
|
||||
const footButtons = "zen-sidebar-foot-buttons";
|
||||
const listener = {
|
||||
onAreaNodeRegistered: area => {
|
||||
if (area !== footButtons) {
|
||||
return;
|
||||
}
|
||||
lazy.CustomizableUI.removeListener(listener);
|
||||
Services.prefs.setBoolPref(donePref, true);
|
||||
const downloads =
|
||||
lazy.CustomizableUI.getPlacementOfWidget("downloads-button");
|
||||
if (
|
||||
downloads?.area === footButtons &&
|
||||
!lazy.CustomizableUI.getPlacementOfWidget("zen-library-button")
|
||||
) {
|
||||
lazy.CustomizableUI.addWidgetToArea(
|
||||
"zen-library-button",
|
||||
footButtons,
|
||||
downloads.position
|
||||
);
|
||||
lazy.CustomizableUI.removeWidgetFromArea("downloads-button");
|
||||
}
|
||||
},
|
||||
};
|
||||
lazy.CustomizableUI.addListener(listener);
|
||||
}
|
||||
|
||||
_migrateV1() {
|
||||
// If there's an userChrome.css or userContent.css existing, we set
|
||||
// 'toolkit.legacyUserProfileCustomizations.stylesheets' back to true
|
||||
|
||||
@@ -166,14 +166,6 @@ document.addEventListener(
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "cmd_zenToggleLibrary": {
|
||||
const { ZenLibrary } = ChromeUtils.importESModule(
|
||||
"moz-src:///zen/library/ZenLibrary.mjs",
|
||||
{ global: "current" }
|
||||
);
|
||||
ZenLibrary.toggle();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
gZenGlanceManager.handleMainCommandSet(event);
|
||||
if (event.target.id.startsWith("cmd_zenWorkspaceSwitch")) {
|
||||
|
||||
@@ -61,12 +61,6 @@ ChromeUtils.defineLazyGetter(lazy, "mainAppWrapper", () =>
|
||||
document.getElementById("zen-main-app-wrapper")
|
||||
);
|
||||
|
||||
ChromeUtils.defineESModuleGetters(
|
||||
lazy,
|
||||
{ ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs" },
|
||||
{ global: "current" }
|
||||
);
|
||||
|
||||
window.gZenCompactModeManager = {
|
||||
_flashTimeouts: {},
|
||||
_eventListeners: [],
|
||||
@@ -175,9 +169,6 @@ window.gZenCompactModeManager = {
|
||||
},
|
||||
|
||||
set preference(value) {
|
||||
if (lazy.ZenLibrary.isLibrarySlightlyOpen) {
|
||||
return;
|
||||
}
|
||||
if (!this.shouldBeCompact) {
|
||||
value = false;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,8 @@
|
||||
background: light-dark(#e9e9e9, #131313);
|
||||
box-shadow: var(--zen-big-shadow);
|
||||
@media -moz-pref('zen.theme.acrylic-elements') {
|
||||
&:not(:-moz-window-inactive) {
|
||||
background: transparent;
|
||||
backdrop-filter: blur(42px) saturate(110%) brightness(0.25) contrast(100%) !important;
|
||||
}
|
||||
background: transparent;
|
||||
backdrop-filter: blur(42px) saturate(110%) brightness(0.25) contrast(100%) !important;
|
||||
}
|
||||
|
||||
&::before,
|
||||
|
||||
@@ -148,11 +148,9 @@ class nsZenDownloadAnimationElement extends HTMLElement {
|
||||
}
|
||||
|
||||
#determineEndPosition() {
|
||||
const downloadsButton = [
|
||||
document.getElementById("zen-library-button"),
|
||||
document.getElementById("downloads-button"),
|
||||
].find(button => button && this.#isElementVisible(button));
|
||||
const isDownloadButtonVisible = !!downloadsButton;
|
||||
const downloadsButton = document.getElementById("downloads-button");
|
||||
const isDownloadButtonVisible =
|
||||
downloadsButton && this.#isElementVisible(downloadsButton);
|
||||
|
||||
let endPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
background-color: var(--zen-primary-color);
|
||||
mask: url("chrome://browser/skin/zen-icons/library/library.svg") no-repeat center;
|
||||
mask: url("chrome://browser/content/zen-images/downloads/archive.svg") no-repeat center;
|
||||
mask-size: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
}
|
||||
|
||||
startTabDrag(event, tab, ...args) {
|
||||
const options = args[0] ?? {};
|
||||
this.ZenDragAndDropService.onDragStart(1);
|
||||
this.#isOutOfWindow = false;
|
||||
gZenCompactModeManager._isTabBeingDragged = true;
|
||||
@@ -146,19 +145,13 @@
|
||||
if (isTabGroupLabel(tab)) {
|
||||
tab = tab.group;
|
||||
}
|
||||
const shown = options.dragImageSource ?? tab;
|
||||
let draggingTabs;
|
||||
if (options.dragImageSource) {
|
||||
draggingTabs = [shown];
|
||||
} else {
|
||||
draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
|
||||
}
|
||||
const draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
|
||||
const { offsetX, offsetY } = this.#getDragImageOffset(
|
||||
event,
|
||||
shown,
|
||||
tab,
|
||||
draggingTabs
|
||||
);
|
||||
const dragImage = this._createDragImageForTabs(draggingTabs);
|
||||
const dragImage = this.#createDragImageForTabs(draggingTabs);
|
||||
this.originalDragImageArgs = [dragImage, offsetX, offsetY];
|
||||
this.#dragImageEssential = tab.hasAttribute("zen-essential");
|
||||
dt.setDragImage(...this.originalDragImageArgs);
|
||||
@@ -166,13 +159,11 @@
|
||||
tab.style.visibility = "hidden";
|
||||
}
|
||||
this._dropLandingArmed =
|
||||
options.armLanding ??
|
||||
(!args[0]?.fromTabList &&
|
||||
this.getDropEffectForTabDrag(event) == "move");
|
||||
!args[0]?.fromTabList && this.getDropEffectForTabDrag(event) == "move";
|
||||
this.ZenDragAndDropService.armDropLanding(this._dropLandingArmed);
|
||||
}
|
||||
|
||||
_createDragImageForTabs(movingTabs) {
|
||||
#createDragImageForTabs(movingTabs) {
|
||||
const periphery = gZenWorkspaces.activeWorkspaceElement.querySelector(
|
||||
"#tabbrowser-arrowscrollbox-periphery"
|
||||
);
|
||||
@@ -684,7 +675,7 @@
|
||||
return;
|
||||
}
|
||||
this.#handle_sidebarDragOver(event);
|
||||
this._handle_tabDragOverToSplit(event);
|
||||
this.#handle_tabDragOverToSplit(event);
|
||||
}
|
||||
|
||||
#shouldSwitchSpace(event) {
|
||||
@@ -730,30 +721,16 @@
|
||||
|
||||
#onSpaceChanged(spaceChanged, dt) {
|
||||
if (AppConstants.platform !== "macosx") {
|
||||
// See the hack in _createDragImageForTabs for more details which
|
||||
// See the hack in #createDragImageForTabs for more details which
|
||||
// explains why we need to do this on non-macOS platforms.
|
||||
return;
|
||||
}
|
||||
this._recolorDragImage(spaceChanged);
|
||||
this._refreshDragImage(dt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recolours the current drag image's tab clones for a workspace's theme,
|
||||
* so the image reads as the space it is over.
|
||||
*
|
||||
* @param {object} workspace
|
||||
*/
|
||||
_recolorDragImage(workspace) {
|
||||
const wrapper = this.originalDragImageArgs?.[0];
|
||||
if (!wrapper || !workspace) {
|
||||
return;
|
||||
}
|
||||
let tabs = this.originalDragImageArgs[0].children;
|
||||
const { isDarkMode, isExplicitMode } =
|
||||
gZenThemePicker.getGradientForWorkspace(workspace, {
|
||||
gZenThemePicker.getGradientForWorkspace(spaceChanged, {
|
||||
getGradient: false,
|
||||
});
|
||||
for (const tab of wrapper.querySelectorAll("tab")) {
|
||||
for (let tab of tabs) {
|
||||
if (isExplicitMode) {
|
||||
tab.style.colorScheme = isDarkMode ? "dark" : "light";
|
||||
tab.style.setProperty(
|
||||
@@ -765,21 +742,9 @@
|
||||
tab.style.removeProperty("--tab-selected-textcolor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-hands the drag image to the drag session once its new styles have
|
||||
* been laid out.
|
||||
*
|
||||
* @param {DataTransfer} dt
|
||||
*/
|
||||
_refreshDragImage(dt) {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// The drag may have ended over the two frames, clearing the image.
|
||||
if (this.originalDragImageArgs.length) {
|
||||
dt.updateDragImage(...this.originalDragImageArgs);
|
||||
}
|
||||
dt.updateDragImage(...this.originalDragImageArgs);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -837,60 +802,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The element a drop on `element` acts on.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @returns {Element}
|
||||
*/
|
||||
_targetForDrop(element) {
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a drop lands before (vs after) an element, from how far down its
|
||||
* box the pointer sits and the move-over threshold pref.
|
||||
*
|
||||
* @param {DragEvent} event
|
||||
* @param {DOMRect} rect - The element's bounds
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_dropsBefore(event, rect) {
|
||||
const threshold =
|
||||
Services.prefs.getIntPref(
|
||||
"browser.tabs.dragDrop.moveOverThresholdPercent"
|
||||
) / 100;
|
||||
return (event.clientY - rect.top) / rect.height <= threshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays the shared drop indicator across an element, horizontally.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Element} [options.parent] - Reparent the indicator here first
|
||||
* @param {number} options.left - Inset from the left, in px
|
||||
* @param {number} options.width - Indicator width, in px
|
||||
* @param {number} options.top - Top offset, in px
|
||||
* @returns {Element} The indicator
|
||||
*/
|
||||
_placeDropIndicator({ parent = null, left, width, top }) {
|
||||
const indicator = gZenPinnedTabManager.dragIndicator;
|
||||
if (parent && indicator.parentNode !== parent) {
|
||||
parent.appendChild(indicator);
|
||||
}
|
||||
indicator.setAttribute("orientation", "horizontal");
|
||||
indicator.style.setProperty("--indicator-left", `${left}px`);
|
||||
indicator.style.setProperty("--indicator-width", `${width}px`);
|
||||
indicator.style.top = `${Math.round(top)}px`;
|
||||
indicator.style.removeProperty("left");
|
||||
return indicator;
|
||||
}
|
||||
|
||||
get _splitDropReady() {
|
||||
return !!this.#dragOverSplit.canDrop;
|
||||
}
|
||||
|
||||
_handle_tabDragOverToSplit(event) {
|
||||
#handle_tabDragOverToSplit(event) {
|
||||
if (!this._dndSplitEnabled) {
|
||||
return;
|
||||
}
|
||||
@@ -919,7 +831,7 @@
|
||||
}
|
||||
|
||||
if (
|
||||
movingTabsSet.has(this._targetForDrop(dropElement)) ||
|
||||
movingTabsSet.has(dropElement) ||
|
||||
!isTab(draggedTab) ||
|
||||
draggedTab?.group?.hasAttribute("split-view-group") ||
|
||||
draggedTab.hasAttribute("zen-live-folder-item-id") ||
|
||||
@@ -1137,13 +1049,13 @@
|
||||
const placesBefore = new Map(
|
||||
this.#draggedElements(event).map(element => [
|
||||
element,
|
||||
this._placeOf(element),
|
||||
this.#placeOf(element),
|
||||
])
|
||||
);
|
||||
super.handle_drop(event);
|
||||
this.#maybeClearVerticalPinnedGridDragOver();
|
||||
this.#handle_dropSwitchSpace(event);
|
||||
this._handle_dropCreateSplit(event);
|
||||
this.#handle_dropCreateSplit(event);
|
||||
this._clearDragOverSplit();
|
||||
if (!toSplit) {
|
||||
this.#landDragImage(event, placesBefore);
|
||||
@@ -1163,7 +1075,7 @@
|
||||
return (this._dragImageTabs ?? [draggedTab]).map(elementToMove);
|
||||
}
|
||||
|
||||
_placeOf(element) {
|
||||
#placeOf(element) {
|
||||
return `${element.screenX},${element.screenY}`;
|
||||
}
|
||||
|
||||
@@ -1182,20 +1094,8 @@
|
||||
if (gReduceMotion) {
|
||||
return;
|
||||
}
|
||||
this._landDragImageOnElements(this.#draggedElements(event), placesBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks where the drag image lands, one item per element in order, and
|
||||
* keeps those elements out of sight until the drag ends.
|
||||
*
|
||||
* @param {Element[]} elements - Where the dropped items land
|
||||
* @param {Map<Element, string>} [placesBefore] - Where each element was
|
||||
* before the drop; one still there stays in sight under the image
|
||||
*/
|
||||
_landDragImageOnElements(elements, placesBefore = null) {
|
||||
const landing = [];
|
||||
for (const element of elements) {
|
||||
for (const element of this.#draggedElements(event)) {
|
||||
const { width, height } = element.getBoundingClientRect();
|
||||
this.ZenDragAndDropService.addDropLandingRect(
|
||||
Math.round(element.screenX),
|
||||
@@ -1203,7 +1103,7 @@
|
||||
Math.round(width),
|
||||
Math.round(height)
|
||||
);
|
||||
if (placesBefore?.get(element) !== this._placeOf(element)) {
|
||||
if (placesBefore.get(element) !== this.#placeOf(element)) {
|
||||
element.style.visibility = "hidden";
|
||||
landing.push(element);
|
||||
}
|
||||
@@ -1237,16 +1137,9 @@
|
||||
gZenWorkspaces.updateTabsContainers();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DragEvent} event
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.activate] - Whether to select the created split
|
||||
* @returns {boolean} Whether the drop split the dragged tab with the tab
|
||||
* it was held over
|
||||
*/
|
||||
_handle_dropCreateSplit(event, { activate = true } = {}) {
|
||||
#handle_dropCreateSplit(event) {
|
||||
if (!this.#dragOverSplit.canDrop) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const dragData = this.#dragOverSplit.data;
|
||||
@@ -1254,11 +1147,11 @@
|
||||
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||
|
||||
if (!dragData || !draggedTab) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._dontAnimateTabMove = true;
|
||||
const droppedOnTab = this._targetForDrop(dragData.dropElement);
|
||||
const droppedOnTab = dragData.dropElement;
|
||||
const dropSide = dragData.dropSide;
|
||||
|
||||
// Clear any visuals and timer
|
||||
@@ -1268,10 +1161,8 @@
|
||||
gZenViewSplitter.splitTabs(
|
||||
isLeft ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab],
|
||||
"vsep",
|
||||
isLeft ? 0 : 1,
|
||||
{ activate }
|
||||
isLeft ? 0 : 1
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
@@ -1659,17 +1550,33 @@
|
||||
this.#dragShiftableItems.at(-1)
|
||||
);
|
||||
}
|
||||
dropBefore =
|
||||
!showIndicatorUnderNewTabButton && this._dropsBefore(event, rect);
|
||||
const top = Math.round(dropBefore ? rect.top : rect.top + rect.height);
|
||||
if (gZenPinnedTabManager.dragIndicator.style.top !== `${top}px`) {
|
||||
const indicator = gZenPinnedTabManager.dragIndicator;
|
||||
let top = 0;
|
||||
threshold =
|
||||
Services.prefs.getIntPref(
|
||||
"browser.tabs.dragDrop.moveOverThresholdPercent"
|
||||
) / 100;
|
||||
if (overlapPercent > threshold || showIndicatorUnderNewTabButton) {
|
||||
top = Math.round(rect.top + rect.height) + "px";
|
||||
dropBefore = false;
|
||||
} else {
|
||||
top = Math.round(rect.top) + "px";
|
||||
dropBefore = true;
|
||||
}
|
||||
if (indicator.style.top !== top) {
|
||||
shouldPlayHapticFeedback = true;
|
||||
}
|
||||
this._placeDropIndicator({
|
||||
left: rect.left + separation / 2,
|
||||
width: rect.width - separation,
|
||||
top,
|
||||
});
|
||||
indicator.setAttribute("orientation", "horizontal");
|
||||
indicator.style.setProperty(
|
||||
"--indicator-left",
|
||||
rect.left + separation / 2 + "px"
|
||||
);
|
||||
indicator.style.setProperty(
|
||||
"--indicator-width",
|
||||
rect.width - separation + "px"
|
||||
);
|
||||
indicator.style.top = top;
|
||||
indicator.style.removeProperty("left");
|
||||
this.#removeDragOverBackground();
|
||||
if (!isTab(dropElement) && dropElement?.parentElement?.isZenFolder) {
|
||||
dropElement = dropElement.parentElement;
|
||||
|
||||
@@ -100,10 +100,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
||||
|
||||
const folderActionsMenu = document.getElementById("zenFolderActions");
|
||||
folderActionsMenu.addEventListener("popupshowing", event => {
|
||||
const target =
|
||||
event.target === folderActionsMenu
|
||||
? (folderActionsMenu.triggerNode ?? event.explicitOriginalTarget)
|
||||
: event.explicitOriginalTarget;
|
||||
const target = event.explicitOriginalTarget;
|
||||
let folder;
|
||||
if (gBrowser.isTabGroupLabel(target)) {
|
||||
folder = target.group;
|
||||
|
||||
@@ -8,10 +8,7 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root,
|
||||
/* Add zen-folder so that accent color changes done to each
|
||||
* specific space is applied. */
|
||||
zen-folder {
|
||||
:root {
|
||||
--zen-folder-behind-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color) 60%, gray), color-mix(in srgb, var(--zen-primary-color) 60%, #c1c1c1));
|
||||
--zen-folder-front-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color), white 70%), color-mix(in srgb, var(--zen-primary-color), black 40%));
|
||||
--zen-folder-stroke: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
|
||||
|
||||
@@ -63,146 +63,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
.addEventListener("click", this.onOverlayClick.bind(this));
|
||||
}
|
||||
|
||||
#detached = null;
|
||||
|
||||
get detachedBrowser() {
|
||||
return this.#detached?.browser ?? null;
|
||||
}
|
||||
|
||||
#createDetachedHost() {
|
||||
const host = window.MozXULElement.parseXULToFragment(`
|
||||
<html:div id="zen-glance-detached">
|
||||
<html:div class="zen-glance-detached-backdrop"/>
|
||||
<html:div class="browserContainer">
|
||||
<html:div class="browserStack"/>
|
||||
</html:div>
|
||||
</html:div>
|
||||
`).firstElementChild;
|
||||
host
|
||||
.querySelector(".zen-glance-detached-backdrop")
|
||||
.addEventListener("click", () => this.closeDetachedGlance());
|
||||
document.getElementById("zen-main-app-wrapper").appendChild(host);
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a detached glance.
|
||||
*
|
||||
* @param {object} data - The URL, its triggering principal, the point the
|
||||
* glance animates out of as clientX/clientY in window coordinates, and
|
||||
* optionally the userContextId to load in
|
||||
* @returns {Promise<{browser: Element, closed: Promise<string>}|null>}
|
||||
* Once open, the glance's browser and a promise for its closing, which
|
||||
*/
|
||||
async openDetachedGlance(data) {
|
||||
if (this.#detached || !this.#isGlanceLoadAllowed(data)) {
|
||||
return null;
|
||||
}
|
||||
const host = this.#createDetachedHost();
|
||||
const wrapper = host.querySelector(".browserContainer");
|
||||
const browser = document.createXULElement("browser");
|
||||
browser.setAttribute("type", "content");
|
||||
browser.setAttribute("remote", "true");
|
||||
browser.setAttribute("maychangeremoteness", "true");
|
||||
browser.setAttribute("disableglobalhistory", "true");
|
||||
browser.setAttribute("messagemanagergroup", "browsers");
|
||||
browser.setAttribute("nodefaultsrc", "true");
|
||||
if (data.userContextId) {
|
||||
browser.setAttribute("usercontextid", data.userContextId);
|
||||
}
|
||||
host.querySelector(".browserStack").appendChild(browser);
|
||||
wrapper.appendChild(this.#createNewOverlayButtons());
|
||||
for (const selector of [
|
||||
".zen-glance-sidebar-open",
|
||||
".zen-glance-sidebar-split",
|
||||
]) {
|
||||
wrapper.querySelector(selector)?.remove();
|
||||
}
|
||||
|
||||
const hostRect = host.getBoundingClientRect();
|
||||
const startPoint = {
|
||||
clientX: data.clientX - hostRect.left,
|
||||
clientY: data.clientY - hostRect.top,
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
let resolveClosed;
|
||||
const whenClosed = new Promise(resolve => {
|
||||
resolveClosed = resolve;
|
||||
});
|
||||
this.#detached = {
|
||||
host,
|
||||
wrapper,
|
||||
browser,
|
||||
startPoint,
|
||||
closing: false,
|
||||
resolveClosed,
|
||||
};
|
||||
|
||||
browser.loadURI(Services.io.newURI(data.url), {
|
||||
triggeringPrincipal: data.triggeringPrincipal,
|
||||
});
|
||||
wrapper.setAttribute("animate", true);
|
||||
await gZenUIManager.elementAnimate(
|
||||
wrapper,
|
||||
this.#createGlanceArcSequence(startPoint, "opening", null, hostRect),
|
||||
{
|
||||
duration: gZenUIManager.testingEnabled
|
||||
? 0
|
||||
: this.#GLANCE_ANIMATION_DURATION,
|
||||
easing: "ease-in-out",
|
||||
}
|
||||
);
|
||||
wrapper.removeAttribute("animate");
|
||||
wrapper.setAttribute("has-finished-animation", true);
|
||||
return { browser, closed: whenClosed };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.skipPermitUnload] - Close even if the page
|
||||
* would rather not unload
|
||||
*/
|
||||
async closeDetachedGlance({ skipPermitUnload = false } = {}) {
|
||||
const detached = this.#detached;
|
||||
if (!detached || detached.closing) {
|
||||
return;
|
||||
}
|
||||
if (!skipPermitUnload && !detached.browser.permitUnload().permitUnload) {
|
||||
return;
|
||||
}
|
||||
detached.closing = true;
|
||||
const { host, wrapper, browser } = detached;
|
||||
detached.resolveClosed();
|
||||
this.#animateSidebarButtons(
|
||||
wrapper.querySelector(".zen-glance-sidebar-container")
|
||||
);
|
||||
host.setAttribute("fade-out", true);
|
||||
wrapper.removeAttribute("has-finished-animation");
|
||||
wrapper.setAttribute("animate", true);
|
||||
try {
|
||||
await gZenUIManager.elementAnimate(
|
||||
wrapper,
|
||||
this.#createGlanceArcSequence(
|
||||
detached.startPoint,
|
||||
"closing",
|
||||
null,
|
||||
host.getBoundingClientRect()
|
||||
),
|
||||
{
|
||||
duration: gZenUIManager.testingEnabled
|
||||
? 0
|
||||
: this.#GLANCE_ANIMATION_DURATION,
|
||||
easing: "ease-out",
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
browser.destroy();
|
||||
host.remove();
|
||||
this.#detached = null;
|
||||
}
|
||||
}
|
||||
|
||||
#setupPreferences() {
|
||||
XPCOMUtils.defineLazyPreferenceGetter(
|
||||
this._lazyPref,
|
||||
@@ -242,10 +102,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
handleMainCommandSet(event) {
|
||||
const command = event.target;
|
||||
const commandHandlers = {
|
||||
cmd_zenGlanceClose: () =>
|
||||
this.#detached
|
||||
? this.closeDetachedGlance()
|
||||
: this.closeGlance({ onTabClose: true }),
|
||||
cmd_zenGlanceClose: () => this.closeGlance({ onTabClose: true }),
|
||||
cmd_zenGlanceExpand: () => this.fullyOpenGlance(),
|
||||
cmd_zenGlanceSplit: () => this.splitGlance(),
|
||||
};
|
||||
@@ -760,16 +617,9 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
* @param {object} data - Glance data with position and dimensions
|
||||
* @param {string} direction - 'opening' or 'closing'
|
||||
* @param {Element|null} imageDataElement - The image data element for preview (optional)
|
||||
* @param {DOMRect|null} referenceRect - Bounds the glance opens within;
|
||||
* defaults to the tab panels
|
||||
* @returns {object} Animation sequence object
|
||||
*/
|
||||
#createGlanceArcSequence(
|
||||
data,
|
||||
direction,
|
||||
imageDataElement = null,
|
||||
referenceRect = null
|
||||
) {
|
||||
#createGlanceArcSequence(data, direction, imageDataElement = null) {
|
||||
let { clientX, clientY, width, height } = data;
|
||||
if (imageDataElement?.parentElement) {
|
||||
// Since we are animating scale transforms on the wrapper, we need to
|
||||
@@ -799,9 +649,9 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
// Calculate start and end positions based on direction
|
||||
let startPosition, endPosition;
|
||||
|
||||
const tabPanelsRect =
|
||||
referenceRect ??
|
||||
window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
|
||||
const tabPanelsRect = window.windowUtils.getBoundsWithoutFlushing(
|
||||
gBrowser.tabpanels
|
||||
);
|
||||
|
||||
const widthPercent = 0.8;
|
||||
if (direction === "opening") {
|
||||
|
||||
@@ -103,14 +103,16 @@
|
||||
}
|
||||
|
||||
.browserSidebarContainer.zen-glance-background,
|
||||
:is(.browserSidebarContainer.zen-glance-overlay, #zen-glance-detached)
|
||||
.browserContainer:not([fade-out="true"]) {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: round;
|
||||
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
|
||||
.browserSidebarContainer.zen-glance-overlay .browserContainer:not([fade-out="true"]) {
|
||||
border-radius: var(--zen-native-inner-radius);
|
||||
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: round;
|
||||
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
|
||||
}
|
||||
}
|
||||
|
||||
:is(.browserSidebarContainer.zen-glance-overlay, #zen-glance-detached) {
|
||||
.browserSidebarContainer.zen-glance-overlay {
|
||||
box-shadow: none !important;
|
||||
visibility: inherit;
|
||||
z-index: 1;
|
||||
@@ -144,11 +146,14 @@
|
||||
}
|
||||
|
||||
& .browserStack {
|
||||
border-radius: var(--zen-native-inner-radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--zen-big-shadow);
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: round;
|
||||
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
|
||||
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
|
||||
/* stylelint-disable-next-line property-no-unknown */
|
||||
corner-shape: round;
|
||||
border-radius: calc(var(--zen-native-inner-radius) / var(--zen-squircle-value));
|
||||
}
|
||||
}
|
||||
|
||||
& browser {
|
||||
@@ -193,15 +198,3 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-main-app-wrapper > #zen-glance-detached {
|
||||
position: fixed;
|
||||
inset: calc(var(--zen-element-separation) / 2);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
|
||||
& .zen-glance-detached-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,705 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import {
|
||||
html,
|
||||
nothing,
|
||||
repeat,
|
||||
} from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
|
||||
const { ZenLibraryWidget } = ChromeUtils.importESModule(
|
||||
"moz-src:///zen/library/ZenLibraryWidget.sys.mjs"
|
||||
);
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(
|
||||
lazy,
|
||||
{
|
||||
ZenLibraryHistorySection:
|
||||
"moz-src:///zen/library/sections/ZenLibraryHistorySection.mjs",
|
||||
ZenLibraryDownloadsSection:
|
||||
"moz-src:///zen/library/sections/ZenLibraryDownloadsSection.mjs",
|
||||
ZenLibraryBoostsSection:
|
||||
"moz-src:///zen/library/sections/ZenLibraryBoostsSection.mjs",
|
||||
ZenLibraryMediaSection:
|
||||
"moz-src:///zen/library/sections/ZenLibraryMediaSection.mjs",
|
||||
ZenLibrarySpacesSection:
|
||||
"moz-src:///zen/library/sections/ZenLibrarySpacesSection.mjs",
|
||||
},
|
||||
{ global: "current" }
|
||||
);
|
||||
|
||||
const LAST_TAB_PREF = "zen.library.last-tab";
|
||||
const CLEANUP_DELAY_MS = 30000;
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
|
||||
return document.getElementById("zen-appcontent-wrapper");
|
||||
});
|
||||
|
||||
export class ZenLibrary extends MozLitElement {
|
||||
static instance = null;
|
||||
#contentMounted = true;
|
||||
#mounted = new Set();
|
||||
#progress = 0;
|
||||
|
||||
#springControls = null;
|
||||
|
||||
#toolboxWidth = 0;
|
||||
|
||||
#originalButtonsNextSibling = null;
|
||||
|
||||
get #hasAdoptedButtons() {
|
||||
return this.#originalButtonsNextSibling !== null;
|
||||
}
|
||||
|
||||
#canSwipe = false;
|
||||
#isOpen = false;
|
||||
|
||||
#wrapperGestureControl = null;
|
||||
#gestureControl = null;
|
||||
|
||||
#resizeObserver = new ResizeObserver(() => {
|
||||
this.openProgress = this.#progress;
|
||||
});
|
||||
|
||||
static queries = {
|
||||
_content: "#zen-library-content",
|
||||
_header: "#zen-library-header",
|
||||
_footer: "#zen-library-footer",
|
||||
};
|
||||
|
||||
static properties = {
|
||||
_activeTab: { type: String },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.zenLibrarySections = {
|
||||
media: lazy.ZenLibraryMediaSection,
|
||||
downloads: lazy.ZenLibraryDownloadsSection,
|
||||
boosts: lazy.ZenLibraryBoostsSection,
|
||||
...(!window.gZenWorkspaces.privateWindowOrDisabled
|
||||
? {
|
||||
spaces: lazy.ZenLibrarySpacesSection,
|
||||
}
|
||||
: {}),
|
||||
history: lazy.ZenLibraryHistorySection,
|
||||
};
|
||||
const lastTab = Services.prefs.getStringPref(LAST_TAB_PREF, "history");
|
||||
this.activeTab = lastTab in this.zenLibrarySections ? lastTab : "history";
|
||||
this.#mounted.add(this.activeTab);
|
||||
this.#hijackFirefoxCommands();
|
||||
}
|
||||
|
||||
static get isLibraryOpen() {
|
||||
const lib = this.getInstance();
|
||||
return lib.#isOpen;
|
||||
}
|
||||
|
||||
static get isLibrarySlightlyOpen() {
|
||||
const lib = this.getInstance(/* createIfMissing = */ false);
|
||||
if (!lib) {
|
||||
return false;
|
||||
}
|
||||
// Due to calculation inaccuracies assume
|
||||
// that openProgress never goes back to 0
|
||||
return lib.openProgress > 0.001;
|
||||
}
|
||||
|
||||
set isHidden(value) {
|
||||
this.requestUpdate();
|
||||
this.hidden = value;
|
||||
}
|
||||
|
||||
set activeTab(value) {
|
||||
if (this._activeTab === value) {
|
||||
return;
|
||||
}
|
||||
this._activeTab = value;
|
||||
this.#mounted.add(value);
|
||||
Services.prefs.setStringPref(LAST_TAB_PREF, value);
|
||||
}
|
||||
|
||||
get activeTab() {
|
||||
return this._activeTab;
|
||||
}
|
||||
|
||||
get activeSection() {
|
||||
return this.zenLibrarySections[this.activeTab];
|
||||
}
|
||||
|
||||
set openProgress(value) {
|
||||
const p = value;
|
||||
const stealWindowButtonsPastPoint = 0.6;
|
||||
const wasOpen = this.#progress > 0.001;
|
||||
this.#progress = p;
|
||||
const isPastWindowButtonSwitchPoint = p > stealWindowButtonsPastPoint;
|
||||
const isOpen = p > 0.001;
|
||||
|
||||
if (this.#stylesLoaded) {
|
||||
let libraryWidth =
|
||||
window.windowUtils.getBoundsWithoutFlushing(this).width;
|
||||
const compactModeOffsetDirection = this.#libraryOnRight
|
||||
? -this.#toolboxWidth + ZenThemeModifier.elementSeparation
|
||||
: this.#toolboxWidth - ZenThemeModifier.elementSeparation;
|
||||
const compactModeOffset = this.#isCompactMode
|
||||
? compactModeOffsetDirection
|
||||
: 0;
|
||||
|
||||
const leftAligned = this.#libraryOnRight ? -1 : 1;
|
||||
let webOffset =
|
||||
leftAligned * (libraryWidth - this.#toolboxWidth) + compactModeOffset;
|
||||
|
||||
this.style.setProperty(
|
||||
"transform",
|
||||
`translateX(calc(${leftAligned} * -100% * (1 - ${value})))`
|
||||
);
|
||||
lazy.appContentWrapper?.style.setProperty(
|
||||
"transform",
|
||||
`translateX(${value * webOffset}px)`
|
||||
);
|
||||
|
||||
const toolboxProgress = Math.min(1, value * 1.5);
|
||||
if (this.#isCompactMode) {
|
||||
if (this.#libraryOnRight) {
|
||||
gNavToolbox.style.setProperty(
|
||||
"transform",
|
||||
`translateX(calc(100% * ${toolboxProgress}))`
|
||||
);
|
||||
} else {
|
||||
gNavToolbox.style.setProperty(
|
||||
"transform",
|
||||
`translateX(calc(-100% * ${toolboxProgress}))`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const toolboxScale = 1 - toolboxProgress * 0.04;
|
||||
const toolboxOpacity = 1 - toolboxProgress;
|
||||
gNavToolbox?.style.setProperty("transform", `scale(${toolboxScale})`);
|
||||
gNavToolbox?.style.setProperty("opacity", `${toolboxOpacity}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen && !wasOpen) {
|
||||
this.#init();
|
||||
} else if (!isOpen && wasOpen) {
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
if (isPastWindowButtonSwitchPoint && this.#coversWindowButtons) {
|
||||
this.#adoptWindowButtons();
|
||||
} else if (!isPastWindowButtonSwitchPoint) {
|
||||
this.#restoreWindowButtons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the window buttons sit in the sidebar column the library covers.
|
||||
*/
|
||||
get #coversWindowButtons() {
|
||||
if (!gZenVerticalTabsManager.isWindowsStyledButtons) {
|
||||
return !this.#libraryOnRight;
|
||||
}
|
||||
return this.#libraryOnRight && !this.#isCompactMode;
|
||||
}
|
||||
|
||||
get openProgress() {
|
||||
return this.#progress;
|
||||
}
|
||||
|
||||
#hijackFirefoxCommands() {
|
||||
document
|
||||
.getElementById("Browser:ShowAllHistory")
|
||||
.addEventListener("command", event => {
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
ZenLibrary.toggle("history");
|
||||
});
|
||||
}
|
||||
|
||||
#adoptWindowButtons() {
|
||||
if (this.#hasAdoptedButtons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const realButtons = gZenVerticalTabsManager.actualWindowButtons;
|
||||
if (!this.#originalButtonsNextSibling) {
|
||||
this.#originalButtonsNextSibling = {
|
||||
isNext: realButtons.nextSibling,
|
||||
sibling: realButtons.nextSibling || realButtons.previousSibling,
|
||||
clone: realButtons.cloneNode(true),
|
||||
};
|
||||
|
||||
this.#originalButtonsNextSibling.clone.classList.add(
|
||||
"zen-library-window-buttons-clone"
|
||||
);
|
||||
if (this.#originalButtonsNextSibling.isNext) {
|
||||
this.#originalButtonsNextSibling.sibling.before(
|
||||
this.#originalButtonsNextSibling.clone
|
||||
);
|
||||
} else {
|
||||
this.#originalButtonsNextSibling.sibling.after(
|
||||
this.#originalButtonsNextSibling.clone
|
||||
);
|
||||
}
|
||||
|
||||
this._header.appendChild(realButtons);
|
||||
}
|
||||
}
|
||||
|
||||
#restoreWindowButtons() {
|
||||
if (!this.#hasAdoptedButtons) {
|
||||
return;
|
||||
}
|
||||
|
||||
const realButtons = gZenVerticalTabsManager.actualWindowButtons;
|
||||
if (this.#originalButtonsNextSibling) {
|
||||
this.#originalButtonsNextSibling.clone.remove();
|
||||
if (this.#originalButtonsNextSibling.isNext) {
|
||||
this.#originalButtonsNextSibling.sibling.before(realButtons);
|
||||
} else {
|
||||
this.#originalButtonsNextSibling.sibling.after(realButtons);
|
||||
}
|
||||
this.#originalButtonsNextSibling = null;
|
||||
}
|
||||
}
|
||||
|
||||
#stylesLoaded = null;
|
||||
|
||||
#whenStylesLoaded() {
|
||||
this.#stylesLoaded ??= this.updateComplete.then(() => {
|
||||
const link = this.querySelector("link[rel='stylesheet']");
|
||||
if (!link || link.sheet) {
|
||||
return undefined;
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
link.addEventListener("load", resolve, { once: true });
|
||||
link.addEventListener("error", resolve, { once: true });
|
||||
});
|
||||
});
|
||||
return this.#stylesLoaded;
|
||||
}
|
||||
|
||||
#cleanupTimer = null;
|
||||
#idleCleanup = null;
|
||||
|
||||
#scheduleIdleCleanup() {
|
||||
this.#cancelIdleCleanup();
|
||||
this.#cleanupTimer = window.setTimeout(() => {
|
||||
this.#cleanupTimer = null;
|
||||
this.#idleCleanup = window.requestIdleCallback(() => {
|
||||
this.#idleCleanup = null;
|
||||
this.#stylesLoaded = null;
|
||||
|
||||
this.#contentMounted = false;
|
||||
this.#mounted = new Set([this.activeTab]);
|
||||
this.requestUpdate();
|
||||
});
|
||||
}, CLEANUP_DELAY_MS);
|
||||
}
|
||||
|
||||
#cancelIdleCleanup() {
|
||||
if (this.#cleanupTimer) {
|
||||
window.clearTimeout(this.#cleanupTimer);
|
||||
this.#cleanupTimer = null;
|
||||
}
|
||||
if (this.#idleCleanup) {
|
||||
window.cancelIdleCallback(this.#idleCleanup);
|
||||
this.#idleCleanup = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or closes the library. With a tab id, opens the library on that
|
||||
* tab, switches to it if already open on another, or closes if it is
|
||||
* already the open one. Without one, plainly toggles open and closed.
|
||||
*
|
||||
* @param {string?} [tab] - A section id to open on
|
||||
*/
|
||||
static toggle(tab = undefined) {
|
||||
if (!Services.prefs.getBoolPref("zen.library.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lib = this.getInstance();
|
||||
if (tab && tab in lib.zenLibrarySections) {
|
||||
if (lib.#isOpen && lib.activeTab === tab) {
|
||||
this.animateProgress(0);
|
||||
} else if (lib.#isOpen) {
|
||||
lib.activeTab = tab;
|
||||
} else {
|
||||
lib.activeTab = tab;
|
||||
this.animateProgress(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.animateProgress(lib.#isOpen ? 0 : 1);
|
||||
}
|
||||
|
||||
static async animateProgress(target) {
|
||||
const lib = this.getInstance();
|
||||
lib.#cancelIdleCleanup();
|
||||
await lib.#whenStylesLoaded();
|
||||
lib.style.visibility = "";
|
||||
await window.promiseDocumentFlushed(() => {});
|
||||
|
||||
if (lib.#springControls) {
|
||||
lib.#springControls.stop();
|
||||
lib.#springControls = null;
|
||||
}
|
||||
|
||||
if (target === 1) {
|
||||
lib.#onOpenLibrary();
|
||||
lib.#isOpen = true;
|
||||
} else if (target === 0) {
|
||||
lib.#isOpen = false;
|
||||
lib.#canSwipe = false;
|
||||
}
|
||||
|
||||
lib.setAttribute("transitioning", "true");
|
||||
lib.#springControls = gZenUIManager.motion.animate(
|
||||
lib.openProgress,
|
||||
target,
|
||||
{
|
||||
type: "spring",
|
||||
stiffness: 720,
|
||||
damping: 47,
|
||||
mass: 1.2,
|
||||
onUpdate: latest => {
|
||||
lib.openProgress = latest;
|
||||
},
|
||||
onComplete: () => {
|
||||
lib.openProgress = target;
|
||||
lib.#springControls = null;
|
||||
lib.removeAttribute("transitioning");
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static async startSwipe() {
|
||||
const lib = this.getInstance();
|
||||
lib.#cancelIdleCleanup();
|
||||
lib.#canSwipe = true;
|
||||
await lib.#whenStylesLoaded();
|
||||
lib.style.visibility = "";
|
||||
await window.promiseDocumentFlushed(() => {});
|
||||
|
||||
lib.#onOpenLibrary();
|
||||
|
||||
if (lib.#springControls) {
|
||||
lib.#springControls.stop();
|
||||
lib.#springControls = null;
|
||||
}
|
||||
|
||||
lib.style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
static stopSwipe(direction) {
|
||||
const lib = this.getInstance();
|
||||
lib.style.pointerEvents = "";
|
||||
lib.#canSwipe = false;
|
||||
|
||||
if (lib.#libraryOnRight) {
|
||||
direction = direction * -1;
|
||||
}
|
||||
|
||||
if (direction) {
|
||||
const target = Math.max(-direction, 0);
|
||||
this.animateProgress(target);
|
||||
}
|
||||
|
||||
return lib.#isOpen;
|
||||
}
|
||||
|
||||
static swipeProgress(target) {
|
||||
const lib = this.getInstance();
|
||||
if (!lib.#canSwipe) {
|
||||
return;
|
||||
}
|
||||
|
||||
lib.openProgress = target;
|
||||
}
|
||||
|
||||
#attachWrapperToSwipe() {
|
||||
if (!this.#wrapperGestureControl) {
|
||||
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||
this.#wrapperGestureControl =
|
||||
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(
|
||||
appWrapper
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#detachWrapperOfSwipe() {
|
||||
if (this.#wrapperGestureControl) {
|
||||
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||
window.gZenWorkspaces._swipeManager?.detachWorkspaceSwipeGestures(
|
||||
appWrapper,
|
||||
this.#wrapperGestureControl
|
||||
);
|
||||
this.#wrapperGestureControl = null;
|
||||
}
|
||||
}
|
||||
|
||||
#onOpenLibrary() {
|
||||
this.#cancelIdleCleanup();
|
||||
if (!this.#contentMounted) {
|
||||
this.#contentMounted = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
gURLBar.view.close();
|
||||
// Get the width from the css property,
|
||||
// getBoundsWithoutFlushing will fail as it takes the
|
||||
// toolbox transformation during the animation into account
|
||||
this.#toolboxWidth = parseFloat(
|
||||
gNavToolbox.style
|
||||
.getPropertyValue("--actual-zen-sidebar-width")
|
||||
.replace("/\D/g", "")
|
||||
);
|
||||
if (document.documentElement.hasAttribute("zen-sidebar-expanded")) {
|
||||
const splitterWidth = window.windowUtils.getBoundsWithoutFlushing(
|
||||
document.getElementById("zen-sidebar-splitter")
|
||||
).width;
|
||||
this.#toolboxWidth += splitterWidth;
|
||||
}
|
||||
}
|
||||
|
||||
static getInstance(createIfMissing = true) {
|
||||
if (!this.instance && createIfMissing) {
|
||||
this.instance = new ZenLibrary();
|
||||
this.instance.style.visibility = "collapse";
|
||||
const mountAfter = document.getElementById("navigator-toolbox");
|
||||
mountAfter.after(this.instance);
|
||||
}
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
get #libraryOnRight() {
|
||||
return gZenVerticalTabsManager._prefsRightSide;
|
||||
}
|
||||
|
||||
static get libraryOnRight() {
|
||||
const lib = this.getInstance();
|
||||
return lib.#libraryOnRight;
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
#init() {
|
||||
this.#cancelIdleCleanup();
|
||||
if (!this.#contentMounted) {
|
||||
this.#contentMounted = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
this.setAttribute("open", "true");
|
||||
document
|
||||
.getElementById("zen-sidebar-splitter")
|
||||
.setAttribute("zen-library-open", "true");
|
||||
document.addEventListener("keydown", this, true);
|
||||
window.addEventListener("TabOpen", this);
|
||||
|
||||
this.#attachWrapperToSwipe();
|
||||
this.#gestureControl =
|
||||
window.gZenWorkspaces._swipeManager.attachWorkspaceSwipeGestures(this);
|
||||
this.#resizeObserver.observe(this);
|
||||
ZenLibraryWidget.attachLibrary(this);
|
||||
this.isHidden = false;
|
||||
}
|
||||
|
||||
#cleanup() {
|
||||
this.removeAttribute("open");
|
||||
this.#mounted = new Set([this.activeTab]);
|
||||
this.requestUpdate();
|
||||
document
|
||||
.getElementById("zen-sidebar-splitter")
|
||||
.removeAttribute("zen-library-open");
|
||||
|
||||
if (this.#springControls) {
|
||||
this.#springControls.stop();
|
||||
this.#springControls = null;
|
||||
}
|
||||
this.removeAttribute("transitioning");
|
||||
|
||||
this.#detachWrapperOfSwipe();
|
||||
if (this.#gestureControl) {
|
||||
window.gZenWorkspaces._swipeManager.detachWorkspaceSwipeGestures(
|
||||
this,
|
||||
this.#gestureControl
|
||||
);
|
||||
}
|
||||
|
||||
this.#restoreWindowButtons();
|
||||
ZenLibraryWidget.detachLibrary(this);
|
||||
this.#resizeObserver.disconnect();
|
||||
document.removeEventListener("keydown", this, true);
|
||||
window.removeEventListener("TabOpen", this);
|
||||
this.isHidden = true;
|
||||
this.#scheduleIdleCleanup();
|
||||
}
|
||||
|
||||
get #isCompactMode() {
|
||||
return (
|
||||
window.gZenCompactModeManager.preference &&
|
||||
(Services.prefs.getBoolPref("zen.view.compact.hide-tabbar") ||
|
||||
Services.prefs.getBoolPref("zen.view.use-single-toolbar"))
|
||||
);
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
switch (e.type) {
|
||||
case "TabOpen":
|
||||
this.onTabOpen();
|
||||
break;
|
||||
case "keydown":
|
||||
this.onKeyDown(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onTabOpen() {
|
||||
if (this.#isOpen) {
|
||||
ZenLibrary.animateProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (!this.hasAttribute("open")) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Escape") {
|
||||
ZenLibrary.animateProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (super.firstUpdated) {
|
||||
super.firstUpdated();
|
||||
}
|
||||
this.#buildFooterButtons();
|
||||
}
|
||||
|
||||
#buildFooterButtons() {
|
||||
const footer = this.querySelector("#zen-library-footer");
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
image: "chrome://browser/skin/zen-icons/back.svg",
|
||||
l10nId: "library-footer-close-button",
|
||||
command: () => ZenLibrary.animateProgress(0),
|
||||
},
|
||||
{
|
||||
image: "chrome://browser/skin/zen-icons/heart-circle-fill.svg",
|
||||
l10nId: "library-footer-donate-button",
|
||||
command: () => {
|
||||
window.openTrustedLinkIn("https://www.zen-browser.app/donate", "tab");
|
||||
ZenLibrary.animateProgress(0);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const { image, l10nId, command } of buttons) {
|
||||
const button = document.createXULElement("toolbarbutton");
|
||||
button.className = "toolbarbutton-1";
|
||||
button.setAttribute("image", image);
|
||||
button.setAttribute("data-l10n-id", l10nId);
|
||||
button.addEventListener("command", command);
|
||||
footer.appendChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated?.(changedProperties);
|
||||
this.#updateMountedSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the section being looked at and puts the others out of sight. A
|
||||
* section is told which it is, so one that reaches outside itself, such as
|
||||
* spaces setting the library's width, only does so while it is on show.
|
||||
*/
|
||||
#updateMountedSections() {
|
||||
for (const section of this._content?.children ?? []) {
|
||||
const id = section.dataset?.section;
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
const showing = id === this.activeTab;
|
||||
const wasShowing = section.hasAttribute("showing");
|
||||
section.hidden = !showing;
|
||||
section.toggleAttribute("showing", showing);
|
||||
if (showing !== wasShowing) {
|
||||
(showing ? section.onShown : section.onHidden)?.call(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="chrome://browser/content/zen-styles/zen-library.css"
|
||||
/>
|
||||
<hbox id="zen-library-panel">
|
||||
<vbox id="zen-library-side">
|
||||
<vbox id="zen-library-header"></vbox>
|
||||
<vbox id="zen-library-sidebar-tabs">
|
||||
${Object.values(this.zenLibrarySections).map(
|
||||
Section => html`
|
||||
<vbox
|
||||
class="zen-library-tab"
|
||||
?active=${this.activeTab === Section.id}
|
||||
data-section=${Section.id}
|
||||
@click=${event => {
|
||||
if (this.activeTab !== Section.id) {
|
||||
this.activeTab = Section.id;
|
||||
const previousTab =
|
||||
event.currentTarget.parentNode.querySelector(
|
||||
`.zen-library-tab[animate="true"]`
|
||||
);
|
||||
if (previousTab) {
|
||||
previousTab.removeAttribute("animate");
|
||||
}
|
||||
event.currentTarget.setAttribute("animate", "true");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="zen-library-tab-icon">
|
||||
<div class="zen-library-tab-icon-image"></div>
|
||||
</div>
|
||||
<label data-l10n-id=${Section.label}></label>
|
||||
</vbox>
|
||||
`
|
||||
)}
|
||||
</vbox>
|
||||
<toolbar
|
||||
id="zen-library-footer"
|
||||
class="chromeclass-location"
|
||||
mode="icons"
|
||||
fullscreentoolbar="true"
|
||||
></toolbar>
|
||||
</vbox>
|
||||
<vbox id="zen-library-content">
|
||||
${
|
||||
this.#contentMounted
|
||||
? repeat(
|
||||
[...this.#mounted],
|
||||
id => id,
|
||||
id => this.zenLibrarySections[id].render(this)
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
</vbox>
|
||||
</hbox>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("zen-library", ZenLibrary);
|
||||
@@ -1,362 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
|
||||
#section;
|
||||
/** @type {{element: Element, dropBefore: boolean, intoFolder: boolean}|null} */
|
||||
#target = null;
|
||||
#highlightedLabel = null;
|
||||
// The space whose theme the drag image currently wears, so it is only
|
||||
// restyled when the pointer crosses into a different card.
|
||||
#dragImageSpace = null;
|
||||
|
||||
constructor(section) {
|
||||
super(section);
|
||||
this.#section = section;
|
||||
}
|
||||
|
||||
_targetForDrop(element) {
|
||||
return this.#section.realElementFor(element) ?? element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DragEvent} event
|
||||
* @returns {Element|null} The dragged tab, or the label of the dragged
|
||||
* split view, when it comes from this window
|
||||
*/
|
||||
#draggedItem(event) {
|
||||
if (this.getDropEffectForTabDrag(event) !== "move") {
|
||||
return null;
|
||||
}
|
||||
const item = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||
if (!item || item.documentGlobal !== window) {
|
||||
return null;
|
||||
}
|
||||
return gBrowser.isTab(item) || this.#isSplitViewLabel(item) ? item : null;
|
||||
}
|
||||
|
||||
#isSplitViewLabel(item) {
|
||||
return (
|
||||
gBrowser.isTabGroupLabel(item) &&
|
||||
!!item.group?.hasAttribute("split-view-group")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The tabs and split views a drag moves, from the drag data set on the
|
||||
* dragged item.
|
||||
*
|
||||
* @param {Element} item - The dragged tab or split view label
|
||||
* @returns {Element[]} Tabs and split view groups
|
||||
*/
|
||||
#movingElements(item) {
|
||||
const moving = item._dragData?.movingTabs ?? [item];
|
||||
return moving.map(element =>
|
||||
gBrowser.isTabGroupLabel(element) ? element.group : element
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DragEvent} event
|
||||
* @param {Element} copy - The copied tab being dragged
|
||||
*/
|
||||
startTabDrag(event, copy) {
|
||||
let item = this._targetForDrop(copy);
|
||||
if (!gBrowser.isTab(item)) {
|
||||
return;
|
||||
}
|
||||
let shown = copy;
|
||||
if (item.group?.hasAttribute("split-view-group")) {
|
||||
item = item.group.labelElement;
|
||||
shown = copy.group;
|
||||
}
|
||||
const strip = gBrowser.tabContainer.tabDragAndDrop;
|
||||
strip.startTabDrag(event, item, {
|
||||
fromTabList: true,
|
||||
dragImageSource: shown,
|
||||
armLanding: this.#landingSupported,
|
||||
});
|
||||
this.#dragImageSpace = copy.closest(".zen-library-space")?.dataset.uuid;
|
||||
this.#styleDragImage(strip, this.#dragImageSpace);
|
||||
}
|
||||
|
||||
#styleDragImage(strip, uuid) {
|
||||
const workspace = uuid && gZenWorkspaces.getWorkspaceFromId(uuid);
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
for (const clone of strip.originalDragImageArgs?.[0]?.querySelectorAll(
|
||||
"tab"
|
||||
) ?? []) {
|
||||
clone.toggleAttribute("visuallyselected", true);
|
||||
clone.toggleAttribute("selected", true);
|
||||
}
|
||||
strip._recolorDragImage(workspace);
|
||||
}
|
||||
|
||||
handle_dragover(event) {
|
||||
const item = this.#draggedItem(event);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
this.#updateDragImageSpace(event);
|
||||
this._handle_tabDragOverToSplit(event);
|
||||
this.#updateDropTarget(event, item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recolours the drag image to match the card under the pointer, so it reads
|
||||
* as the space it would drop into.
|
||||
*
|
||||
* @param {DragEvent} event
|
||||
*/
|
||||
#updateDragImageSpace(event) {
|
||||
const uuid = event.target.closest(".zen-library-space")?.dataset.uuid;
|
||||
if (!uuid || uuid === this.#dragImageSpace) {
|
||||
return;
|
||||
}
|
||||
this.#dragImageSpace = uuid;
|
||||
const strip = gBrowser.tabContainer.tabDragAndDrop;
|
||||
this.#styleDragImage(strip, uuid);
|
||||
strip._refreshDragImage(event.dataTransfer);
|
||||
}
|
||||
|
||||
handle_dragleave(event) {
|
||||
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||
this.clearDragOverVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
handle_drop(event) {
|
||||
const item = this.#draggedItem(event);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
const target = this.#target;
|
||||
const uuid = event.currentTarget.closest(".zen-library-space").dataset.uuid;
|
||||
const moving = this.#movingElements(item);
|
||||
const tabs = moving.flatMap(element =>
|
||||
gBrowser.isTab(element) ? [element] : element.tabs
|
||||
);
|
||||
const fromElsewhere = tabs.filter(
|
||||
tab => tab.getAttribute("zen-workspace-id") !== uuid
|
||||
);
|
||||
const strip = gBrowser.tabContainer.tabDragAndDrop;
|
||||
const copyBefore = this.#section.copyForTab(tabs[0]);
|
||||
const placeBefore = copyBefore && strip._placeOf(copyBefore);
|
||||
if (fromElsewhere.length) {
|
||||
gZenWorkspaces.moveTabsToWorkspace(fromElsewhere, uuid);
|
||||
}
|
||||
const split = this._handle_dropCreateSplit(event, { activate: false });
|
||||
this.clearDragOverVisuals();
|
||||
if (!split && target) {
|
||||
const { element, dropBefore, intoFolder } = target;
|
||||
if (intoFolder) {
|
||||
element.addTabs(moving);
|
||||
const firstExisting = element.tabs.find(tab => !tabs.includes(tab));
|
||||
if (firstExisting) {
|
||||
gBrowser.moveTabsBefore(moving, firstExisting);
|
||||
}
|
||||
} else {
|
||||
const pinned = this.#pinnedAt(event);
|
||||
for (const tab of tabs) {
|
||||
if (pinned && !tab.pinned) {
|
||||
gBrowser.pinTab(tab);
|
||||
} else if (!pinned && tab.pinned) {
|
||||
gBrowser.unpinTab(tab);
|
||||
}
|
||||
}
|
||||
if (dropBefore) {
|
||||
gBrowser.moveTabsBefore(moving, element);
|
||||
} else {
|
||||
gBrowser.moveTabsAfter(moving, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.#land(tabs[0], placeBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a drop belongs to the pinned part of a card.
|
||||
*
|
||||
* @param {DragEvent} event - The drop
|
||||
* @returns {boolean} Whether the tab should end up pinned
|
||||
*/
|
||||
#pinnedAt(event) {
|
||||
const strip = event.currentTarget
|
||||
.closest(".zen-library-space")
|
||||
?.querySelector(".zen-library-space-tabs");
|
||||
const pinnedRows = [...(strip?.querySelectorAll("tab[pinned]") ?? [])];
|
||||
if (!pinnedRows.length) {
|
||||
return false;
|
||||
}
|
||||
return event.clientY <= pinnedRows.at(-1).getBoundingClientRect().bottom;
|
||||
}
|
||||
|
||||
get #landingSupported() {
|
||||
return AppConstants.platform === "macosx" && !gReduceMotion;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} tab - The dropped tab
|
||||
* @param {string|null} placeBefore - Where its copy was before the drop;
|
||||
* a copy still there stays in sight under the image
|
||||
*/
|
||||
#land(tab, placeBefore) {
|
||||
if (!this.#landingSupported) {
|
||||
return;
|
||||
}
|
||||
const copy = this.#section.beginTabLanding(tab);
|
||||
if (copy) {
|
||||
gBrowser.tabContainer.tabDragAndDrop._landDragImageOnElements(
|
||||
[copy],
|
||||
new Map([[copy, placeBefore]])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
clearDragOverVisuals(options) {
|
||||
super.clearDragOverVisuals(options);
|
||||
gZenFolders.highlightGroupOnDragOver(null);
|
||||
this.#highlightedLabel?.removeAttribute("dragover");
|
||||
this.#highlightedLabel = null;
|
||||
this.#target = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element|null} strip - A card's tab strip
|
||||
* @returns {Element|null} The last tab or folder label shown in it
|
||||
*/
|
||||
#lastRow(strip) {
|
||||
if (!strip) {
|
||||
return null;
|
||||
}
|
||||
const rows = strip.querySelectorAll(
|
||||
"tab:not([zen-empty-tab]):not([zen-glance-tab]), .tab-group-label-container"
|
||||
);
|
||||
for (let i = rows.length - 1; i >= 0; i--) {
|
||||
if (rows[i].getBoundingClientRect().height > 0) {
|
||||
return rows[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works out what a drop at the pointer would do and shows it on the copies.
|
||||
*
|
||||
* @param {DragEvent} event
|
||||
* @param {Element} item - The dragged tab or split view label
|
||||
*/
|
||||
#updateDropTarget(event, item) {
|
||||
if (this._splitDropReady) {
|
||||
return;
|
||||
}
|
||||
const movingTabs = item._dragData?.movingTabs ?? [item];
|
||||
const moving = this.#movingElements(item);
|
||||
let copy = event.target.closest("tab, .tab-group-label-container");
|
||||
if (!copy) {
|
||||
copy = this.#lastRow(event.target.closest(".zen-library-space-tabs"));
|
||||
}
|
||||
if (copy?.hasAttribute("zen-glance-tab")) {
|
||||
copy = copy.parentElement.closest("tab");
|
||||
}
|
||||
if (copy?.classList.contains("tab-group-label-container")) {
|
||||
copy = copy.parentElement;
|
||||
} else if (copy?.group?.hasAttribute("split-view-group")) {
|
||||
copy = copy.group;
|
||||
}
|
||||
const element = copy && this._targetForDrop(copy);
|
||||
if (!element) {
|
||||
// A card with nothing in it takes the drop at its start.
|
||||
const strip = event.target.closest(".zen-library-space-tabs");
|
||||
if (strip && !this.#lastRow(strip)) {
|
||||
this.#target = null;
|
||||
gZenFolders.highlightGroupOnDragOver(null);
|
||||
this.#placeIndicatorIn(strip, 0);
|
||||
return;
|
||||
}
|
||||
this.clearDragOverVisuals();
|
||||
return;
|
||||
}
|
||||
if (moving.some(moved => moved === element || moved.contains(element))) {
|
||||
this.clearDragOverVisuals();
|
||||
return;
|
||||
}
|
||||
const isFolder = !!element.isZenFolder;
|
||||
const box = isFolder ? copy.labelContainerElement : copy;
|
||||
const rect = box.getBoundingClientRect();
|
||||
const overlap = (event.clientY - rect.top) / rect.height;
|
||||
let dropBefore;
|
||||
let intoFolder = false;
|
||||
if (isFolder) {
|
||||
const threshold =
|
||||
Services.prefs.getIntPref(
|
||||
"zen.tabs.folder-dragover-threshold-percent"
|
||||
) / 100;
|
||||
if (overlap < threshold) {
|
||||
dropBefore = true;
|
||||
} else if (
|
||||
overlap > 1 - threshold &&
|
||||
(copy.collapsed || element.childGroupsAndTabs.length < 2)
|
||||
) {
|
||||
dropBefore = false;
|
||||
} else {
|
||||
intoFolder = true;
|
||||
}
|
||||
} else {
|
||||
dropBefore = this._dropsBefore(event, rect);
|
||||
}
|
||||
|
||||
const previous = this.#target;
|
||||
this.#target = { element, dropBefore, intoFolder };
|
||||
const changed =
|
||||
previous?.element !== element ||
|
||||
previous.dropBefore !== dropBefore ||
|
||||
previous.intoFolder !== intoFolder;
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
if (previous) {
|
||||
// eslint-disable-next-line mozilla/valid-services
|
||||
Services.zen.playHapticFeedback();
|
||||
}
|
||||
|
||||
this.#highlightedLabel?.removeAttribute("dragover");
|
||||
this.#highlightedLabel = null;
|
||||
if (intoFolder) {
|
||||
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
||||
gZenFolders.highlightGroupOnDragOver(copy, movingTabs);
|
||||
this.#highlightedLabel = box;
|
||||
box.setAttribute("dragover", "true");
|
||||
return;
|
||||
}
|
||||
gZenFolders.highlightGroupOnDragOver(null);
|
||||
const strip = copy.closest(".zen-library-space-tabs");
|
||||
this.#placeIndicatorIn(
|
||||
strip,
|
||||
(dropBefore ? rect.top : rect.bottom) - strip.getBoundingClientRect().top
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} strip - A card's tab strip
|
||||
* @param {number} offset - Where in the strip's view the drop would go
|
||||
*/
|
||||
#placeIndicatorIn(strip, offset) {
|
||||
const inset = 14;
|
||||
this._placeDropIndicator({
|
||||
parent: strip,
|
||||
left: inset,
|
||||
width: strip.getBoundingClientRect().width - 2 * inset,
|
||||
top: offset + strip.scrollTop,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "mimeService", () =>
|
||||
Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService)
|
||||
);
|
||||
|
||||
/**
|
||||
* Groups the browser cannot name on its own. Pictures, video and sound come
|
||||
* from the content type instead, so they are not listed here.
|
||||
*/
|
||||
const GROUP_EXTENSIONS = {
|
||||
documents:
|
||||
"pdf doc docx xls xlsx ppt pptx txt md rtf odt ods odp csv epub pages numbers key",
|
||||
archives: "zip rar 7z tar gz bz2 xz tgz zst",
|
||||
apps: "dmg pkg exe msi app deb rpm appimage apk jar",
|
||||
};
|
||||
|
||||
/**
|
||||
* The few the browser has no type for, or types differently from how these
|
||||
* lists read them. Matroska is unknown to it, and Ogg is reported as a
|
||||
* container rather than as sound.
|
||||
*/
|
||||
const EXTRA_KINDS = new Map([
|
||||
["mkv", "video"],
|
||||
["ogg", "audio"],
|
||||
]);
|
||||
|
||||
const GROUP_BY_EXTENSION = new Map();
|
||||
for (const [group, extensions] of Object.entries(GROUP_EXTENSIONS)) {
|
||||
for (const extension of extensions.split(" ")) {
|
||||
GROUP_BY_EXTENSION.set(extension, group);
|
||||
}
|
||||
}
|
||||
|
||||
/** The groups a file can be filtered by, in the order they are offered. */
|
||||
export const FILE_GROUPS = [
|
||||
"images",
|
||||
"video",
|
||||
"audio",
|
||||
...Object.keys(GROUP_EXTENSIONS),
|
||||
];
|
||||
|
||||
/**
|
||||
* A picture the platform renders as a document rather than a bitmap. It is
|
||||
* left out of thumbnails, which are drawn inside the browser's own chrome.
|
||||
*/
|
||||
const NOT_DRAWN = "image/svg+xml";
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name, with or without its path
|
||||
* @returns {string} Its extension, lowercased, or "" when it has none
|
||||
*/
|
||||
export function extensionOf(fileName) {
|
||||
const name = fileName?.split(/[/\\]/).pop() ?? "";
|
||||
const dot = name.lastIndexOf(".");
|
||||
return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {string} What the browser believes the file holds, or "" when it
|
||||
* has no idea
|
||||
*/
|
||||
export function contentTypeOf(fileName) {
|
||||
const extension = extensionOf(fileName);
|
||||
if (!extension) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return lazy.mimeService.getTypeFromExtension(extension);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {string|null} "image", "video" or "audio", or null for anything
|
||||
* that is none of those
|
||||
*/
|
||||
export function mediaKindOf(fileName) {
|
||||
const type = contentTypeOf(fileName);
|
||||
for (const kind of ["image", "video", "audio"]) {
|
||||
if (type.startsWith(`${kind}/`)) {
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
return EXTRA_KINDS.get(extensionOf(fileName)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {string|null} The group it belongs to, of {@link FILE_GROUPS}
|
||||
*/
|
||||
export function fileGroupOf(fileName) {
|
||||
const kind = mediaKindOf(fileName);
|
||||
if (kind) {
|
||||
return kind === "image" ? "images" : kind;
|
||||
}
|
||||
return GROUP_BY_EXTENSION.get(extensionOf(fileName)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {boolean} Whether the file itself can stand in for its icon
|
||||
*/
|
||||
export function canDrawThumbnail(fileName) {
|
||||
const type = contentTypeOf(fileName);
|
||||
return type.startsWith("image/") && type !== NOT_DRAWN;
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"relativeDayFormat",
|
||||
() =>
|
||||
new Intl.RelativeTimeFormat(undefined, {
|
||||
numeric: "auto",
|
||||
style: "short",
|
||||
})
|
||||
);
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const MS_PER_DAY = 86400000;
|
||||
const MORPH_MS = 250;
|
||||
// How far in the picture gives way to the dialog around it.
|
||||
// Matches the corner the grid's pictures have.
|
||||
const MEDIA_RADIUS = 8;
|
||||
const HANDOVER = 0.3;
|
||||
// How soon the dialog's surfaces are gone on the way back.
|
||||
const CHROME_OUT = 0.25;
|
||||
// How long to hold the dialog back while the picture readies itself.
|
||||
const IMAGE_WAIT_MS = 400;
|
||||
const VIDEO_WAIT_MS = 900;
|
||||
const MORPH_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||||
|
||||
/**
|
||||
* The media tab's full view. It lives beside the library rather than inside
|
||||
* it, so it can sit in the middle of the window while the library slides.
|
||||
*/
|
||||
export class ZenLibraryMediaPreview {
|
||||
#window;
|
||||
#document;
|
||||
#root = null;
|
||||
#parts = {};
|
||||
#menu = null;
|
||||
/** @type {object|null} The media the open menu acts on */
|
||||
#menuItem = null;
|
||||
#items = [];
|
||||
#index = -1;
|
||||
#origin = null;
|
||||
#onKeyDown = event => this.#handleKey(event);
|
||||
|
||||
constructor(win) {
|
||||
this.#window = win;
|
||||
this.#document = win.document;
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return !!this.#root?.isConnected;
|
||||
}
|
||||
|
||||
get #item() {
|
||||
return this.#items[this.#index] ?? null;
|
||||
}
|
||||
|
||||
// Building
|
||||
|
||||
#build() {
|
||||
const doc = this.#document;
|
||||
const make = (tag, className, into) => {
|
||||
const element = doc.createElementNS(HTML_NS, tag);
|
||||
if (className) {
|
||||
element.className = className;
|
||||
}
|
||||
into?.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
const root = make("div", "zen-library-media-preview");
|
||||
const backdrop = make("div", "zen-library-media-preview-backdrop", root);
|
||||
backdrop.addEventListener("click", () => this.close());
|
||||
|
||||
const panel = make("div", "zen-library-media-preview-panel", root);
|
||||
const header = doc.createXULElement("toolbar");
|
||||
header.className = "zen-library-media-preview-header";
|
||||
panel.appendChild(header);
|
||||
|
||||
const button = (className, icon, l10nId, onClick) => {
|
||||
const element = doc.createXULElement("toolbarbutton");
|
||||
element.className = `toolbarbutton-1 ${className}`;
|
||||
header.appendChild(element);
|
||||
const image = doc.createElementNS(HTML_NS, "img");
|
||||
image.className = "toolbarbutton-icon";
|
||||
image.src = icon;
|
||||
image.alt = "";
|
||||
element.appendChild(image);
|
||||
doc.l10n.setAttributes(element, l10nId);
|
||||
element.addEventListener("command", onClick);
|
||||
element.addEventListener("click", onClick);
|
||||
return element;
|
||||
};
|
||||
|
||||
button(
|
||||
"close",
|
||||
"chrome://browser/skin/zen-icons/close.svg",
|
||||
"library-media-close",
|
||||
() => this.close()
|
||||
);
|
||||
const prev = button(
|
||||
"prev",
|
||||
"chrome://browser/skin/zen-icons/arrow-left.svg",
|
||||
"library-media-previous",
|
||||
() => this.#step(-1)
|
||||
);
|
||||
const next = button(
|
||||
"next",
|
||||
"chrome://browser/skin/zen-icons/arrow-right.svg",
|
||||
"library-media-next",
|
||||
() => this.#step(1)
|
||||
);
|
||||
|
||||
const nameLabel = make("span", "zen-library-media-preview-name", header);
|
||||
const date = make("span", "zen-library-media-preview-date", header);
|
||||
const more = button(
|
||||
"more",
|
||||
"chrome://global/skin/icons/more.svg",
|
||||
"library-media-more",
|
||||
event => this.#openMenu(event)
|
||||
);
|
||||
const openButton = doc.createXULElement("toolbarbutton");
|
||||
openButton.className = "toolbarbutton-1 zen-library-media-preview-open";
|
||||
header.appendChild(openButton);
|
||||
const openText = doc.createXULElement("label");
|
||||
openText.className = "toolbarbutton-text";
|
||||
openButton.appendChild(openText);
|
||||
openButton.addEventListener("command", () => this.#launch());
|
||||
openButton.addEventListener("click", () => this.#launch());
|
||||
|
||||
const stage = make("div", "zen-library-media-preview-stage", panel);
|
||||
stage.addEventListener("contextmenu", event =>
|
||||
this.openContextMenu(event, this.#item)
|
||||
);
|
||||
|
||||
this.#parts = {
|
||||
panel,
|
||||
backdrop,
|
||||
header,
|
||||
openText,
|
||||
stage,
|
||||
nameLabel,
|
||||
date,
|
||||
openButton,
|
||||
prev,
|
||||
next,
|
||||
more,
|
||||
};
|
||||
this.#root = root;
|
||||
this.#mountRoot.appendChild(root);
|
||||
}
|
||||
|
||||
get #mountRoot() {
|
||||
return (
|
||||
this.#document.getElementById("zen-main-app-wrapper") ??
|
||||
this.#document.documentElement
|
||||
);
|
||||
}
|
||||
|
||||
// Opening and closing
|
||||
|
||||
/**
|
||||
* @param {object} options - Where to start
|
||||
* @param {object[]} options.items - Everything the grid is showing
|
||||
* @param {number} options.index - The item that was clicked
|
||||
* @param {Element} options.from - The thumbnail it grows out of
|
||||
*/
|
||||
open({ items, index, from }) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (!this.#root) {
|
||||
this.#build();
|
||||
}
|
||||
this.#items = items;
|
||||
this.#index = index;
|
||||
this.#origin = from;
|
||||
this.#window.addEventListener("keydown", this.#onKeyDown, true);
|
||||
this.#show();
|
||||
this.#root.style.visibility = "hidden";
|
||||
this.#whenMediaReady().then(() => {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.#root.style.visibility = "";
|
||||
this.#morph(from, /* opening = */ true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise} Settles once the picture has a size to animate from,
|
||||
* or right away when it already has one.
|
||||
*/
|
||||
#whenMediaReady() {
|
||||
const media = this.#parts.stage.querySelector(
|
||||
".zen-library-media-preview-media"
|
||||
);
|
||||
if (!media) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const isVideo = media.localName === "video";
|
||||
const ready = () =>
|
||||
isVideo ? media.readyState >= media.HAVE_CURRENT_DATA : media.complete;
|
||||
if (ready()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const done = () => resolve();
|
||||
media.addEventListener("load", done, { once: true });
|
||||
media.addEventListener("loadeddata", done, { once: true });
|
||||
media.addEventListener("error", done, { once: true });
|
||||
this.#window.setTimeout(done, isVideo ? VIDEO_WAIT_MS : IMAGE_WAIT_MS);
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.#window.removeEventListener("keydown", this.#onKeyDown, true);
|
||||
const done = this.#morph(this.#origin, /* opening = */ false);
|
||||
const finish = () => {
|
||||
this.#root?.remove();
|
||||
this.#root = null;
|
||||
this.#parts = {};
|
||||
this.#origin = null;
|
||||
};
|
||||
if (done) {
|
||||
done.finished.then(finish, finish);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#window.removeEventListener("keydown", this.#onKeyDown, true);
|
||||
this.#menu?.remove();
|
||||
this.#menu = null;
|
||||
this.#root?.remove();
|
||||
this.#root = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog straight on top of the card's picture, matching its box
|
||||
* exactly, and grows it into the middle of the window. Closing is the same
|
||||
* run played backwards.
|
||||
*
|
||||
* @param {Element} thumbnail - The card's picture
|
||||
* @param {boolean} opening - Which way round
|
||||
* @returns {Animation} The dialog's animation
|
||||
*/
|
||||
#morph(thumbnail, opening) {
|
||||
const { panel, backdrop, header, stage } = this.#parts;
|
||||
const base = {
|
||||
duration: MORPH_MS,
|
||||
easing: MORPH_EASING,
|
||||
fill: "both",
|
||||
};
|
||||
const steps = keyframes =>
|
||||
opening
|
||||
? keyframes
|
||||
: [...keyframes]
|
||||
.reverse()
|
||||
.map(frame =>
|
||||
frame.offset === undefined
|
||||
? frame
|
||||
: { ...frame, offset: 1 - frame.offset }
|
||||
);
|
||||
const chrome = { ...base, easing: "linear" };
|
||||
const fadeOut = arriving =>
|
||||
opening
|
||||
? arriving
|
||||
: [
|
||||
{ ...arriving.at(-1), offset: 0 },
|
||||
{ ...arriving[0], offset: CHROME_OUT },
|
||||
{ ...arriving[0], offset: 1 },
|
||||
];
|
||||
backdrop.animate(fadeOut([{ opacity: 0 }, { opacity: 1 }]), chrome);
|
||||
const source = thumbnail?.querySelector(".zen-library-media-thumb");
|
||||
const from = source?.getBoundingClientRect();
|
||||
const to = panel.getBoundingClientRect();
|
||||
if (!from?.width || !to.width) {
|
||||
return backdrop.animate(
|
||||
fadeOut([{ opacity: 0 }, { opacity: 1 }]),
|
||||
chrome
|
||||
);
|
||||
}
|
||||
const media = stage?.querySelector(".zen-library-media-preview-media");
|
||||
const shown = media?.getBoundingClientRect();
|
||||
const target = shown?.width ? shown : to;
|
||||
const scale = from.width / target.width;
|
||||
const centre = box => ({
|
||||
x: box.left + box.width / 2,
|
||||
y: box.top + box.height / 2,
|
||||
});
|
||||
const panelCentre = centre(to);
|
||||
const shownCentre = centre(target);
|
||||
const cardCentre = centre(from);
|
||||
const atCard =
|
||||
`translate(${cardCentre.x - panelCentre.x - scale * (shownCentre.x - panelCentre.x)}px, ` +
|
||||
`${cardCentre.y - panelCentre.y - scale * (shownCentre.y - panelCentre.y)}px) ` +
|
||||
`scale(${scale})`;
|
||||
const panelStyle = this.#window.getComputedStyle(panel);
|
||||
const late = (values, property) => {
|
||||
const hidden = property === "boxShadow" ? "none" : "transparent";
|
||||
return opening
|
||||
? [
|
||||
{ [property]: hidden, offset: 0 },
|
||||
{ [property]: hidden, offset: HANDOVER },
|
||||
{ [property]: values, offset: 1 },
|
||||
]
|
||||
: [
|
||||
{ [property]: values, offset: 0 },
|
||||
{ [property]: hidden, offset: CHROME_OUT },
|
||||
{ [property]: hidden, offset: 1 },
|
||||
];
|
||||
};
|
||||
panel.animate(late(panelStyle.backgroundColor, "backgroundColor"), chrome);
|
||||
panel.animate(late(panelStyle.boxShadow, "boxShadow"), chrome);
|
||||
if (stage) {
|
||||
const stageStyle = this.#window.getComputedStyle(stage);
|
||||
stage.animate(
|
||||
late(stageStyle.backgroundColor, "backgroundColor"),
|
||||
chrome
|
||||
);
|
||||
stage.animate(late(stageStyle.borderTopColor, "borderTopColor"), chrome);
|
||||
}
|
||||
header.animate(
|
||||
fadeOut([
|
||||
{ opacity: 0, offset: 0 },
|
||||
{ opacity: 0, offset: HANDOVER },
|
||||
{ opacity: 1, offset: 1 },
|
||||
]),
|
||||
chrome
|
||||
);
|
||||
if (media && shown?.width) {
|
||||
media.animate(
|
||||
steps([
|
||||
{ borderRadius: `${Math.round(MEDIA_RADIUS / scale)}px` },
|
||||
{ borderRadius: `${MEDIA_RADIUS}px` },
|
||||
]),
|
||||
base
|
||||
);
|
||||
}
|
||||
return panel.animate(
|
||||
steps([{ transform: atCard }, { transform: "none" }]),
|
||||
base
|
||||
);
|
||||
}
|
||||
|
||||
#step(delta) {
|
||||
const next = this.#index + delta;
|
||||
if (next < 0 || next >= this.#items.length) {
|
||||
return;
|
||||
}
|
||||
this.#index = next;
|
||||
this.#origin = this.#thumbFor(this.#item);
|
||||
this.#show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} item - The media being shown
|
||||
* @returns {Element|null} Its card in the grid, so closing lands back on it
|
||||
*/
|
||||
#thumbFor(item) {
|
||||
const section = this.#document.querySelector("zen-library-media-section");
|
||||
const cards = [
|
||||
...(section?.querySelectorAll(".zen-library-media-item") ?? []),
|
||||
];
|
||||
return (
|
||||
cards
|
||||
.find(card => card.title === item?.name)
|
||||
?.querySelector(".zen-library-media-frame") ?? this.#origin
|
||||
);
|
||||
}
|
||||
|
||||
#handleKey(event) {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
this.close();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
this.#step(-1);
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.#step(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Contents
|
||||
|
||||
#show() {
|
||||
const item = this.#item;
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const { stage, nameLabel, date, openText, prev, next } = this.#parts;
|
||||
const doc = this.#document;
|
||||
stage.replaceChildren();
|
||||
const media = doc.createElementNS(
|
||||
HTML_NS,
|
||||
item.kind === "video" ? "video" : "img"
|
||||
);
|
||||
media.className = "zen-library-media-preview-media";
|
||||
media.src = item.url;
|
||||
if (item.kind !== "video") {
|
||||
media.draggable = true;
|
||||
media.addEventListener("dragstart", event =>
|
||||
this.#onDragStart(event, item)
|
||||
);
|
||||
}
|
||||
if (item.kind === "video") {
|
||||
media.controls = true;
|
||||
media.autoplay = true;
|
||||
media.preload = "auto";
|
||||
} else {
|
||||
media.alt = "";
|
||||
}
|
||||
stage.appendChild(media);
|
||||
|
||||
nameLabel.textContent = item.name;
|
||||
date.textContent = this.#relativeDate(item.modified);
|
||||
doc.l10n
|
||||
.formatValue("library-media-open-in", { app: this.#openerName(item) })
|
||||
.then(text => {
|
||||
openText.textContent = text;
|
||||
});
|
||||
prev.disabled = this.#index <= 0;
|
||||
next.disabled = this.#index >= this.#items.length - 1;
|
||||
}
|
||||
|
||||
#relativeDate(modified) {
|
||||
const days = Math.round((modified - Date.now()) / MS_PER_DAY);
|
||||
if (Math.abs(days) >= 7) {
|
||||
return lazy.relativeDayFormat.format(Math.round(days / 7), "week");
|
||||
}
|
||||
return lazy.relativeDayFormat.format(days, "day");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} item - The media being shown
|
||||
* @returns {string} What the system opens this kind of file with
|
||||
*/
|
||||
#openerName(item) {
|
||||
try {
|
||||
const mime = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
|
||||
const info = mime.getFromTypeAndExtension(null, item.extension);
|
||||
return info.defaultDescription || info.description || "";
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
#onDragStart(event, item) {
|
||||
const file = new lazy.FileUtils.File(item.path);
|
||||
if (!file.exists()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.dataTransfer.mozSetDataAt("application/x-moz-file", file, 0);
|
||||
event.dataTransfer.effectAllowed = "copyMove";
|
||||
event.dataTransfer.setData("text/uri-list", item.url);
|
||||
}
|
||||
|
||||
#launch(item = this.#item) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new lazy.FileUtils.File(item.path).launch();
|
||||
} catch (error) {
|
||||
this.#window.openTrustedLinkIn(item.url, "tab");
|
||||
}
|
||||
}
|
||||
|
||||
#reveal(item = this.#item) {
|
||||
if (item) {
|
||||
new lazy.FileUtils.File(item.path).reveal();
|
||||
}
|
||||
}
|
||||
|
||||
#copy(item = this.#item) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const transferable = Cc[
|
||||
"@mozilla.org/widget/transferable;1"
|
||||
].createInstance(Ci.nsITransferable);
|
||||
transferable.init(this.#window.docShell.QueryInterface(Ci.nsILoadContext));
|
||||
transferable.addDataFlavor("application/x-moz-file");
|
||||
transferable.setTransferData(
|
||||
"application/x-moz-file",
|
||||
new lazy.FileUtils.File(item.path)
|
||||
);
|
||||
Services.clipboard.setData(
|
||||
transferable,
|
||||
null,
|
||||
Services.clipboard.kGlobalClipboard
|
||||
);
|
||||
}
|
||||
|
||||
#openMenu(event) {
|
||||
this.#menuItem = this.#item;
|
||||
this.#ensureMenu().openPopup(
|
||||
event.currentTarget,
|
||||
"after_end",
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the same menu as the more button, at the pointer, for any media
|
||||
* in the grid or the one on show.
|
||||
*
|
||||
* @param {MouseEvent} event - The right click
|
||||
* @param {object} item - The media it was on
|
||||
*/
|
||||
openContextMenu(event, item) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
this.#menuItem = item;
|
||||
this.#ensureMenu().openPopupAtScreen(
|
||||
event.screenX,
|
||||
event.screenY,
|
||||
true,
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
#ensureMenu() {
|
||||
if (!this.#menu) {
|
||||
this.#menu = this.#window.MozXULElement.parseXULToFragment(`
|
||||
<menupopup class="zen-library-media-menu">
|
||||
<menuitem data-action="open" data-l10n-id="library-media-menu-open"/>
|
||||
<menuitem data-action="show" data-l10n-id="downloads-cmd-show-menuitem-2"/>
|
||||
<menuseparator/>
|
||||
<menuitem data-action="copy" data-l10n-id="library-media-menu-copy"/>
|
||||
</menupopup>
|
||||
`).firstElementChild;
|
||||
this.#menu.addEventListener("command", menuEvent => {
|
||||
const item = this.#menuItem;
|
||||
switch (menuEvent.target.dataset.action) {
|
||||
case "open":
|
||||
this.#launch(item);
|
||||
break;
|
||||
case "show":
|
||||
this.#reveal(item);
|
||||
break;
|
||||
case "copy":
|
||||
this.#copy(item);
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.#document.getElementById("mainPopupSet").appendChild(this.#menu);
|
||||
}
|
||||
return this.#menu;
|
||||
}
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
DownloadsCommon:
|
||||
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||
DownloadsViewUI:
|
||||
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
});
|
||||
|
||||
const ENTRIES = 4;
|
||||
const CLOSE_DELAY_MS = 200;
|
||||
const FILE_MIME = "application/x-moz-file";
|
||||
const BADGE_MARKUP = `
|
||||
<span class="zen-library-download-badge no-squircles">
|
||||
<span class="zen-library-download-progress no-squircles"></span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
class ZenLibraryDownloadStack {
|
||||
#button;
|
||||
#badge;
|
||||
#list;
|
||||
#library = null;
|
||||
#entries = [];
|
||||
#data;
|
||||
#downloads = [];
|
||||
#inBatch = false;
|
||||
#loaded = false;
|
||||
#recentNewDownload = false;
|
||||
#closeTimer = null;
|
||||
#contextMenuOpen = false;
|
||||
#secondsLeft = new WeakMap();
|
||||
|
||||
/**
|
||||
* @param {Element} button - The library toolbar button
|
||||
*/
|
||||
constructor(button) {
|
||||
this.#button = button;
|
||||
button.setAttribute("command", "cmd_zenToggleLibrary");
|
||||
button.classList.add("toolbarbutton-badge-stack-host");
|
||||
button.appendChild(
|
||||
this.#parse(
|
||||
`<box class="toolbarbutton-badge-stack">${BADGE_MARKUP}</box>`
|
||||
)
|
||||
);
|
||||
this.#badge = button.querySelector(".zen-library-download-badge");
|
||||
this.#badge.id = "library-button-badge";
|
||||
this.#list = this.#buildList();
|
||||
|
||||
button.addEventListener("mouseenter", this);
|
||||
button.addEventListener("mouseleave", this);
|
||||
this.#list.addEventListener("mouseenter", this);
|
||||
this.#list.addEventListener("mouseleave", this);
|
||||
|
||||
this.#data = lazy.DownloadsCommon.getData(this.#window, true);
|
||||
this.#data.addView(this);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#data.removeView(this);
|
||||
this.#window.clearTimeout(this.#closeTimer);
|
||||
this.#list.remove();
|
||||
}
|
||||
|
||||
get #window() {
|
||||
return this.#button.documentGlobal;
|
||||
}
|
||||
|
||||
get #footButtons() {
|
||||
return this.#list.parentElement;
|
||||
}
|
||||
|
||||
/** The tab strip, once the window has it. */
|
||||
get #tabs() {
|
||||
return this.#window.gBrowser?.tabContainer ?? null;
|
||||
}
|
||||
|
||||
#parse(markup) {
|
||||
return this.#window.MozXULElement.parseXULToFragment(markup);
|
||||
}
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "mouseenter":
|
||||
this.#window.clearTimeout(this.#closeTimer);
|
||||
if (event.currentTarget === this.#button) {
|
||||
this.#open();
|
||||
}
|
||||
break;
|
||||
case "mouseleave":
|
||||
this.#scheduleClose();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#scheduleClose() {
|
||||
this.#window.clearTimeout(this.#closeTimer);
|
||||
this.#closeTimer = this.#window.setTimeout(() => {
|
||||
if (!this.#contextMenuOpen) {
|
||||
this.#close();
|
||||
}
|
||||
}, CLOSE_DELAY_MS);
|
||||
}
|
||||
|
||||
#open() {
|
||||
if (this.#footButtons.hasAttribute("zen-library-stack-open")) {
|
||||
return;
|
||||
}
|
||||
this.#aimBadge();
|
||||
this.#tabs?.removeAttribute("zen-library-stack-closing");
|
||||
for (const host of [this.#footButtons, this.#tabs]) {
|
||||
host?.setAttribute("zen-library-stack-open", "true");
|
||||
}
|
||||
}
|
||||
|
||||
#close() {
|
||||
if (!this.#footButtons.hasAttribute("zen-library-stack-open")) {
|
||||
return;
|
||||
}
|
||||
this.#recentNewDownload = false;
|
||||
this.#updateBadgeShowing();
|
||||
for (const host of [this.#footButtons, this.#tabs]) {
|
||||
host?.removeAttribute("zen-library-stack-open");
|
||||
}
|
||||
// The strip's fade stays until its progress is back at zero.
|
||||
const tabs = this.#tabs;
|
||||
if (!tabs) {
|
||||
return;
|
||||
}
|
||||
tabs.setAttribute("zen-library-stack-closing", "true");
|
||||
tabs.addEventListener("transitionend", function onEnd(event) {
|
||||
if (event.propertyName === "--zen-library-progress") {
|
||||
tabs.removeEventListener("transitionend", onEnd);
|
||||
tabs.removeAttribute("zen-library-stack-closing");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Points the button's badge at the newest entry's badge, where it flies
|
||||
* to as the list opens, from wherever it is right now.
|
||||
*/
|
||||
#aimBadge() {
|
||||
const entry = this.#entries.at(-1);
|
||||
if (entry.hidden) {
|
||||
return;
|
||||
}
|
||||
const target = entry.querySelector(".zen-library-download-badge");
|
||||
const from = this.#badge.getBoundingClientRect();
|
||||
const to = target.getBoundingClientRect();
|
||||
const style = this.#window.getComputedStyle(this.#badge);
|
||||
// The entry is still translated down while closed; land where it ends.
|
||||
const entryTransform = this.#window.getComputedStyle(entry).transform;
|
||||
const rise =
|
||||
entryTransform === "none"
|
||||
? 0
|
||||
: new this.#window.DOMMatrixReadOnly(entryTransform).f;
|
||||
this.#badge.style.setProperty(
|
||||
"--zen-library-badge-to-x",
|
||||
`${parseFloat(style.left) + to.left - from.left}px`
|
||||
);
|
||||
this.#badge.style.setProperty(
|
||||
"--zen-library-badge-to-y",
|
||||
`${parseFloat(style.top) + to.top - from.top - rise}px`
|
||||
);
|
||||
}
|
||||
|
||||
#buildList() {
|
||||
const document = this.#window.document;
|
||||
const footButtons = document.getElementById("zen-sidebar-foot-buttons");
|
||||
// Not a widget of the toolbar, so its customization leaves it alone.
|
||||
footButtons.appendChild(
|
||||
this.#parse(
|
||||
`<box id="zen-library-download-list" skipintoolbarset="true"></box>`
|
||||
)
|
||||
);
|
||||
const list = document.getElementById("zen-library-download-list");
|
||||
for (let i = 0; i < ENTRIES; i++) {
|
||||
list.appendChild(
|
||||
this.#parse(`
|
||||
<div class="zen-library-download-list-download">
|
||||
${BADGE_MARKUP}
|
||||
<vbox class="zen-library-download-list-title-container">
|
||||
<span class="zen-library-download-list-title"></span>
|
||||
<span class="zen-library-download-list-subtitle"></span>
|
||||
</vbox>
|
||||
<toolbarbutton class="toolbarbutton-1 zen-library-download-action no-squircles" />
|
||||
</div>
|
||||
`)
|
||||
);
|
||||
}
|
||||
this.#entries = [...list.children];
|
||||
for (const entry of this.#entries) {
|
||||
entry.setAttribute("draggable", "true");
|
||||
entry.addEventListener("dragstart", event =>
|
||||
this.#onDragStart(event, entry.download)
|
||||
);
|
||||
entry.addEventListener("click", event => {
|
||||
if (event.button === 0) {
|
||||
this.#openDownload(entry.download);
|
||||
}
|
||||
});
|
||||
entry.addEventListener("contextmenu", event =>
|
||||
this.#showContextMenu(event, entry.download)
|
||||
);
|
||||
const action = entry.querySelector(".zen-library-download-action");
|
||||
action.addEventListener("click", event => event.stopPropagation());
|
||||
action.addEventListener("command", () =>
|
||||
this.#cancelDownload(entry.download)
|
||||
);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a download under way, partial file and all.
|
||||
*
|
||||
* @param {Download} download
|
||||
*/
|
||||
#cancelDownload(download) {
|
||||
if (!download || download.stopped) {
|
||||
return;
|
||||
}
|
||||
download.cancel().catch(() => {});
|
||||
download
|
||||
.removePartialData()
|
||||
.catch(console.error)
|
||||
.finally(() => download.target.refresh());
|
||||
}
|
||||
|
||||
#openDownload(download) {
|
||||
if (download.succeeded) {
|
||||
lazy.DownloadsCommon.openDownload(download).catch(console.error);
|
||||
} else if (download.source?.url) {
|
||||
this.#window.openTrustedLinkIn(download.source.url, "tab");
|
||||
}
|
||||
}
|
||||
|
||||
#onDragStart(event, download) {
|
||||
if (!download?.succeeded || download.deleted || !download.target?.exists) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const file = new lazy.FileUtils.File(download.target.path);
|
||||
const { dataTransfer } = event;
|
||||
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
|
||||
dataTransfer.effectAllowed = "copyMove";
|
||||
dataTransfer.setData("text/uri-list", Services.io.newFileURI(file).spec);
|
||||
dataTransfer.addElement(event.currentTarget);
|
||||
// eslint-disable-next-line mozilla/valid-services
|
||||
Services.zen.playHapticFeedback();
|
||||
}
|
||||
|
||||
#updateList() {
|
||||
const shown = this.#downloads.slice(-ENTRIES);
|
||||
const unused = ENTRIES - shown.length;
|
||||
this.#entries.forEach((entry, i) => {
|
||||
const download = shown[i - unused];
|
||||
entry.hidden = !download;
|
||||
entry.download = download ?? null;
|
||||
if (!download) {
|
||||
return;
|
||||
}
|
||||
this.#updateBadge(
|
||||
entry.querySelector(".zen-library-download-badge"),
|
||||
download
|
||||
);
|
||||
entry.querySelector(".zen-library-download-list-title").textContent =
|
||||
this.#fileName(download);
|
||||
entry.querySelector(".zen-library-download-list-subtitle").textContent =
|
||||
this.#statusText(download);
|
||||
entry.toggleAttribute("downloading", !download.stopped);
|
||||
});
|
||||
this.#tabs?.style.setProperty(
|
||||
"--zen-library-stack-height",
|
||||
`${this.#list.getBoundingClientRect().height}px`
|
||||
);
|
||||
}
|
||||
|
||||
#updateBadge(badge, download) {
|
||||
const pending = this.#isPending(download);
|
||||
badge.toggleAttribute("downloading", pending);
|
||||
if (badge === this.#badge) {
|
||||
badge.parentElement.toggleAttribute("downloading", pending);
|
||||
}
|
||||
if (download) {
|
||||
const preview = this.#previewUrl(download);
|
||||
badge.style.setProperty(
|
||||
"--download-image",
|
||||
`${preview ? `url('${preview}'), ` : ""}url('${this.#iconUrl(download)}')`
|
||||
);
|
||||
}
|
||||
badge
|
||||
.querySelector(".zen-library-download-progress")
|
||||
.style.setProperty(
|
||||
"--value",
|
||||
download?.hasProgress ? download.progress : 0
|
||||
);
|
||||
}
|
||||
|
||||
#updateBadgeShowing() {
|
||||
this.#footButtons.toggleAttribute(
|
||||
"zen-library-badge",
|
||||
this.#isPending(this.#downloads.at(-1)) || this.#recentNewDownload
|
||||
);
|
||||
}
|
||||
|
||||
// DownloadList view
|
||||
|
||||
onDownloadBatchStarting() {
|
||||
this.#inBatch = true;
|
||||
}
|
||||
|
||||
onDownloadBatchEnded() {
|
||||
this.#inBatch = false;
|
||||
this.#loaded = true;
|
||||
this.#update();
|
||||
}
|
||||
|
||||
onDownloadAdded(download, { insertBefore } = {}) {
|
||||
const index = insertBefore ? this.#downloads.indexOf(insertBefore) : -1;
|
||||
if (index === -1) {
|
||||
this.#downloads.push(download);
|
||||
} else {
|
||||
this.#downloads.splice(index, 0, download);
|
||||
}
|
||||
if (this.#loaded) {
|
||||
this.#recentNewDownload = true;
|
||||
}
|
||||
this.#update();
|
||||
}
|
||||
|
||||
onDownloadChanged() {
|
||||
this.#update();
|
||||
}
|
||||
|
||||
onDownloadRemoved(download) {
|
||||
const index = this.#downloads.indexOf(download);
|
||||
if (index !== -1) {
|
||||
this.#downloads.splice(index, 1);
|
||||
}
|
||||
this.#update();
|
||||
}
|
||||
|
||||
#update() {
|
||||
if (this.#inBatch) {
|
||||
return;
|
||||
}
|
||||
const newest = this.#downloads.at(-1);
|
||||
this.#updateList();
|
||||
this.#updateBadge(this.#badge, newest);
|
||||
this.#updateBadgeShowing();
|
||||
this.#applyDownloadState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the newest download's state on the library while it is open, for
|
||||
* its downloads tab to show.
|
||||
*
|
||||
* @param {Element} library
|
||||
*/
|
||||
attachLibrary(library) {
|
||||
this.#library = library;
|
||||
this.#applyDownloadState();
|
||||
}
|
||||
|
||||
detachLibrary(library) {
|
||||
if (this.#library === library) {
|
||||
this.#library = null;
|
||||
}
|
||||
}
|
||||
|
||||
#applyDownloadState() {
|
||||
if (!this.#library) {
|
||||
return;
|
||||
}
|
||||
const newest = this.#downloads.at(-1);
|
||||
this.#library.toggleAttribute(
|
||||
"zen-library-downloading",
|
||||
this.#isPending(newest)
|
||||
);
|
||||
this.#library.style.setProperty(
|
||||
"--zen-library-download-progress",
|
||||
`${newest?.hasProgress ? Math.round(newest.progress) : 0}%`
|
||||
);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
#fileName(download) {
|
||||
return download.target.path
|
||||
? PathUtils.filename(download.target.path)
|
||||
: download.source.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} download - The download an entry stands for
|
||||
* @returns {string|null} The finished file itself, when it is a picture
|
||||
*/
|
||||
#previewUrl(download) {
|
||||
const path = download.succeeded && download.target.path;
|
||||
if (!path || !lazy.canDrawThumbnail(path)) {
|
||||
return null;
|
||||
}
|
||||
return PathUtils.toFileURI(path);
|
||||
}
|
||||
|
||||
#isPending(download) {
|
||||
return (
|
||||
!!download &&
|
||||
(!download.stopped || (download.canceled && download.hasPartialData))
|
||||
);
|
||||
}
|
||||
|
||||
#joinStatus(...parts) {
|
||||
return parts
|
||||
.filter(Boolean)
|
||||
.reduce((a, b) => lazy.DownloadsCommon.strings.statusSeparator(a, b));
|
||||
}
|
||||
|
||||
#iconUrl(download) {
|
||||
if (!download.target.path) {
|
||||
return "moz-icon://.unknown?size=32";
|
||||
}
|
||||
return `moz-icon://${download.target.path}?size=32${
|
||||
download.succeeded ? "&state=normal" : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
#statusText(download) {
|
||||
const strings = lazy.DownloadsCommon.strings;
|
||||
const totalBytes = download.hasProgress ? download.totalBytes : -1;
|
||||
if (!download.stopped) {
|
||||
const [statusText, secondsLeft] = lazy.DownloadUtils.getDownloadStatus(
|
||||
download.currentBytes,
|
||||
totalBytes,
|
||||
download.speed,
|
||||
this.#secondsLeft.get(download) ?? Infinity
|
||||
);
|
||||
this.#secondsLeft.set(download, secondsLeft);
|
||||
return statusText;
|
||||
}
|
||||
this.#secondsLeft.delete(download);
|
||||
if (download.deleted) {
|
||||
return strings.fileDeleted;
|
||||
}
|
||||
if (download.succeeded) {
|
||||
if (!download.target.exists) {
|
||||
return strings.fileMovedOrMissing;
|
||||
}
|
||||
const parsed = URL.parse(download.source.url);
|
||||
const uri = parsed && Services.io.newURI(parsed.href);
|
||||
const host = uri
|
||||
? lazy.BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true })
|
||||
: "";
|
||||
const [date] = lazy.DownloadUtils.getReadableDates(
|
||||
new Date(download.endTime)
|
||||
);
|
||||
return this.#joinStatus(
|
||||
lazy.DownloadsViewUI.getSizeWithUnits(download),
|
||||
host,
|
||||
date
|
||||
);
|
||||
}
|
||||
if (download.canceled && download.hasPartialData) {
|
||||
return this.#joinStatus(
|
||||
strings.statePaused,
|
||||
lazy.DownloadUtils.getTransferTotal(download.currentBytes, totalBytes)
|
||||
);
|
||||
}
|
||||
if (download.error?.becauseBlockedByParentalControls) {
|
||||
return strings.stateBlockedParentalControls;
|
||||
}
|
||||
if (download.error?.becauseBlockedByReputationCheck) {
|
||||
return strings.blockedMalware;
|
||||
}
|
||||
return download.canceled ? strings.stateCanceled : strings.stateFailed;
|
||||
}
|
||||
|
||||
#contextMenuItems(download) {
|
||||
const C = lazy.DownloadsCommon;
|
||||
const state = C.stateOfDownload(download);
|
||||
const isActive =
|
||||
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_PAUSED;
|
||||
const fileExists =
|
||||
state === C.DOWNLOAD_FINISHED &&
|
||||
download.target?.exists !== false &&
|
||||
!download.deleted;
|
||||
const sourceUrl = download.source?.originalUrl || download.source?.url;
|
||||
const items = [];
|
||||
if (state === C.DOWNLOAD_DOWNLOADING) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-pause",
|
||||
onClick: () => download.cancel().catch(() => {}),
|
||||
});
|
||||
} else if (state === C.DOWNLOAD_PAUSED) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-resume",
|
||||
onClick: () => download.start?.().catch(() => {}),
|
||||
});
|
||||
}
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-show-menuitem-2",
|
||||
onClick: () =>
|
||||
C.showDownloadedFile(new lazy.FileUtils.File(download.target.path)),
|
||||
});
|
||||
}
|
||||
if (sourceUrl) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-go-to-download-page",
|
||||
onClick: () => this.#window.openTrustedLinkIn(sourceUrl, "tab"),
|
||||
});
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-copy-download-link",
|
||||
onClick: () =>
|
||||
Cc["@mozilla.org/widget/clipboardhelper;1"]
|
||||
.getService(Ci.nsIClipboardHelper)
|
||||
.copyString(sourceUrl),
|
||||
});
|
||||
}
|
||||
items.push({ separator: true });
|
||||
if (fileExists) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-delete-file",
|
||||
onClick: () =>
|
||||
C.deleteDownloadFiles(
|
||||
download,
|
||||
lazy.DownloadsViewUI.clearHistoryOnDelete
|
||||
).catch(console.error),
|
||||
});
|
||||
}
|
||||
if (!isActive) {
|
||||
items.push({
|
||||
l10nId: "downloads-cmd-remove-from-history",
|
||||
onClick: () => C.deleteDownload(download).catch(console.error),
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
#showContextMenu(event, download) {
|
||||
if (!download) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const document = this.#window.document;
|
||||
const popup = document.createXULElement("menupopup");
|
||||
for (const item of this.#contextMenuItems(download)) {
|
||||
if (item.separator) {
|
||||
if (popup.lastChild && popup.lastChild.tagName !== "menuseparator") {
|
||||
popup.appendChild(document.createXULElement("menuseparator"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const menuitem = document.createXULElement("menuitem");
|
||||
menuitem.setAttribute("data-l10n-id", item.l10nId);
|
||||
menuitem.addEventListener(
|
||||
"command",
|
||||
() => {
|
||||
try {
|
||||
item.onClick();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
popup.appendChild(menuitem);
|
||||
}
|
||||
if (popup.lastChild?.tagName === "menuseparator") {
|
||||
popup.lastChild.remove();
|
||||
}
|
||||
if (!popup.childElementCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#contextMenuOpen = true;
|
||||
popup.addEventListener(
|
||||
"popuphidden",
|
||||
() => {
|
||||
this.#contextMenuOpen = false;
|
||||
popup.remove();
|
||||
this.#scheduleClose();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
document.getElementById("mainPopupSet").appendChild(popup);
|
||||
popup.openPopupAtScreen(event.screenX, event.screenY, true);
|
||||
}
|
||||
}
|
||||
|
||||
const stacks = new WeakMap();
|
||||
|
||||
export const ZenLibraryWidget = {
|
||||
id: "zen-library-button",
|
||||
l10nId: "zen-library-button",
|
||||
_introducedByPref: "zen.library.enabled",
|
||||
|
||||
onCreated(node) {
|
||||
stacks.set(node.ownerDocument, new ZenLibraryDownloadStack(node));
|
||||
},
|
||||
|
||||
onDestroyed(document) {
|
||||
stacks.get(document)?.destroy();
|
||||
stacks.delete(document);
|
||||
},
|
||||
|
||||
attachLibrary(library) {
|
||||
stacks.get(library.ownerDocument)?.attachLibrary(library);
|
||||
},
|
||||
|
||||
detachLibrary(library) {
|
||||
stacks.get(library.ownerDocument)?.detachLibrary(library);
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
content/browser/zen-styles/zen-library.css (../../zen/library/zen-library.css)
|
||||
@@ -1,18 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
MOZ_SRC_FILES += [
|
||||
"sections/ZenLibraryBoostsSection.mjs",
|
||||
"sections/ZenLibraryDownloadsSection.mjs",
|
||||
"sections/ZenLibraryHistorySection.mjs",
|
||||
"sections/ZenLibraryMediaSection.mjs",
|
||||
"sections/ZenLibrarySearchSection.mjs",
|
||||
"sections/ZenLibrarySpacesSection.mjs",
|
||||
|
||||
"ZenLibrary.mjs",
|
||||
"ZenLibraryDragAndDrop.mjs",
|
||||
"ZenLibraryFileTypes.sys.mjs",
|
||||
"ZenLibraryMediaPreview.mjs",
|
||||
"ZenLibraryWidget.sys.mjs",
|
||||
]
|
||||
@@ -1,278 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html, repeat } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { ZenLibrarySearchSection } from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
|
||||
});
|
||||
|
||||
const BOOST_TOPICS = ["zen-boosts-update", "zen-boosts-active-change"];
|
||||
|
||||
const boostKey = boost => `${boost.domain}/${boost.id}`;
|
||||
|
||||
export class ZenLibraryBoostsSection extends ZenLibrarySearchSection {
|
||||
static id = "boosts";
|
||||
static label = "library-boosts-section-title";
|
||||
|
||||
static render(library) {
|
||||
return html`
|
||||
<zen-library-boosts-section
|
||||
class="zen-library-section"
|
||||
data-section="boosts"
|
||||
.library=${library}
|
||||
></zen-library-boosts-section>
|
||||
`;
|
||||
}
|
||||
|
||||
#observer = { observe: () => this.requestUpdate() };
|
||||
#glanceBrowser = null;
|
||||
#editor = null;
|
||||
#menu = null;
|
||||
#menuBoost = null;
|
||||
#menuRow = null;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
for (const topic of BOOST_TOPICS) {
|
||||
Services.obs.addObserver(this.#observer, topic);
|
||||
}
|
||||
this.#menu = this.#buildMenu();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
for (const topic of BOOST_TOPICS) {
|
||||
Services.obs.removeObserver(this.#observer, topic);
|
||||
}
|
||||
this.#menu?.hidePopup();
|
||||
this.#menu?.remove();
|
||||
this.#menu = null;
|
||||
this.#glanceBrowser = null;
|
||||
}
|
||||
|
||||
get searchPlaceholderL10nId() {
|
||||
return "library-boosts-search-placeholder";
|
||||
}
|
||||
|
||||
onSearchChanged() {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
#boosts() {
|
||||
const boosts = [];
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
for (const [domain, entry] of lazy.gZenBoostsManager.registeredDomains) {
|
||||
for (const [id, boostEntry] of entry.boostEntries) {
|
||||
const { boostData } = boostEntry;
|
||||
if (!boostData.changeWasMade) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
query &&
|
||||
!boostData.boostName.toLowerCase().includes(query) &&
|
||||
!domain.toLowerCase().includes(query)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
boosts.push({
|
||||
id,
|
||||
domain,
|
||||
name: boostData.boostName,
|
||||
enabled: entry.activeBoostId === id,
|
||||
});
|
||||
}
|
||||
}
|
||||
return boosts.sort(
|
||||
(a, b) => a.name.localeCompare(b.name) || a.domain.localeCompare(b.domain)
|
||||
);
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
#toggle(boost) {
|
||||
lazy.gZenBoostsManager.toggleBoostActiveForDomain(boost.domain, boost.id);
|
||||
}
|
||||
|
||||
#onRowClick(boost, row) {
|
||||
if (boost.enabled) {
|
||||
this.#edit(boost, row);
|
||||
} else {
|
||||
this.#toggle(boost);
|
||||
}
|
||||
}
|
||||
|
||||
async #edit(boost, row) {
|
||||
if (this.#glanceBrowser) {
|
||||
return;
|
||||
}
|
||||
const url = `https://${boost.domain}/`;
|
||||
const uri = Services.io.newURI(url);
|
||||
if (!lazy.gZenBoostsManager.canBoostSite(uri)) {
|
||||
return;
|
||||
}
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const glance = await gZenGlanceManager.openDetachedGlance({
|
||||
url,
|
||||
clientX: rowRect.left + rowRect.width / 2,
|
||||
clientY: rowRect.top + rowRect.height / 2,
|
||||
userContextId: gZenWorkspaces.getActiveWorkspace()?.containerTabId,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
if (!glance) {
|
||||
return;
|
||||
}
|
||||
const { browser } = glance;
|
||||
this.#glanceBrowser = browser;
|
||||
glance.closed.then(() => {
|
||||
this.#glanceBrowser = null;
|
||||
this.#editor?.close();
|
||||
this.#editor = null;
|
||||
});
|
||||
await this.#whenNavigated(browser);
|
||||
if (this.#glanceBrowser !== browser) {
|
||||
return;
|
||||
}
|
||||
const stored = lazy.gZenBoostsManager.loadBoostFromStore(
|
||||
boost.domain,
|
||||
boost.id
|
||||
);
|
||||
this.#editor = lazy.gZenBoostsManager.openBoostWindow(window, stored, uri, {
|
||||
browser,
|
||||
});
|
||||
}
|
||||
|
||||
#whenNavigated(browser) {
|
||||
if (browser.currentURI?.spec !== "about:blank") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const listener = {
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIWebProgressListener",
|
||||
"nsISupportsWeakReference",
|
||||
]),
|
||||
onLocationChange(webProgress) {
|
||||
if (webProgress.isTopLevel) {
|
||||
browser.removeProgressListener(listener);
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
};
|
||||
browser.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_LOCATION);
|
||||
});
|
||||
}
|
||||
|
||||
async #export(boost) {
|
||||
const { boostEntry } = lazy.gZenBoostsManager.loadBoostFromStore(
|
||||
boost.domain,
|
||||
boost.id
|
||||
);
|
||||
await lazy.gZenBoostsManager.exportBoost(window, boostEntry.boostData);
|
||||
}
|
||||
|
||||
#delete(boost) {
|
||||
lazy.gZenBoostsManager.deleteBoost({ domain: boost.domain, id: boost.id });
|
||||
}
|
||||
|
||||
// Context menu
|
||||
|
||||
#buildMenu() {
|
||||
const menu = window.MozXULElement.parseXULToFragment(`
|
||||
<menupopup class="zen-library-boosts-menu">
|
||||
<menuitem data-action="edit" data-l10n-id="library-boosts-menu-edit"/>
|
||||
<menuitem data-action="export" data-l10n-id="zen-boost-save"/>
|
||||
<menuseparator/>
|
||||
<menuitem data-action="delete" data-l10n-id="zen-boost-edit-delete"/>
|
||||
</menupopup>
|
||||
`).firstElementChild;
|
||||
menu.addEventListener("command", event => {
|
||||
const boost = this.#menuBoost;
|
||||
const row = this.#menuRow;
|
||||
if (!boost) {
|
||||
return;
|
||||
}
|
||||
switch (event.target.dataset.action) {
|
||||
case "edit":
|
||||
this.#edit(boost, row);
|
||||
break;
|
||||
case "export":
|
||||
this.#export(boost);
|
||||
break;
|
||||
case "delete":
|
||||
this.#delete(boost);
|
||||
break;
|
||||
}
|
||||
});
|
||||
menu.addEventListener("popuphidden", () => {
|
||||
this.#menuRow?.removeAttribute("menu-open");
|
||||
this.#menuRow = null;
|
||||
this.#menuBoost = null;
|
||||
});
|
||||
document.getElementById("mainPopupSet").appendChild(menu);
|
||||
return menu;
|
||||
}
|
||||
|
||||
#openMenu(boost, row, event) {
|
||||
this.#menuRow?.removeAttribute("menu-open");
|
||||
this.#menuBoost = boost;
|
||||
this.#menuRow = row;
|
||||
row.setAttribute("menu-open", "true");
|
||||
this.#menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
#renderBoost(boost) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-row zen-library-boost-row"
|
||||
?disabled=${!boost.enabled}
|
||||
@click=${event => this.#onRowClick(boost, event.currentTarget)}
|
||||
@contextmenu=${event => {
|
||||
event.preventDefault();
|
||||
this.#openMenu(boost, event.currentTarget, event);
|
||||
}}
|
||||
>
|
||||
<div class="zen-library-boost-icon zen-squircle-before">
|
||||
<img src="page-icon:https://${boost.domain}/" alt="" />
|
||||
</div>
|
||||
<div class="zen-library-row-text">
|
||||
<span class="zen-library-row-title">${boost.name}</span>
|
||||
<span class="zen-library-row-subtitle">${boost.domain}</span>
|
||||
</div>
|
||||
<div class="zen-library-row-actions">
|
||||
<moz-toggle
|
||||
?pressed=${boost.enabled}
|
||||
data-l10n-id="library-boosts-toggle"
|
||||
@click=${event => event.stopPropagation()}
|
||||
@toggle=${() => this.#toggle(boost)}
|
||||
></moz-toggle>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
const boosts = this.#boosts();
|
||||
if (!boosts.length) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-empty"
|
||||
data-l10n-id="library-boosts-empty"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="zen-library-group">
|
||||
${repeat(boosts, boostKey, boost => this.#renderBoost(boost))}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("zen-library-boosts-section", ZenLibraryBoostsSection);
|
||||
@@ -1,666 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html, repeat, when } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import {
|
||||
MS_PER_DAY,
|
||||
PAGE_SIZE,
|
||||
ZenLibrarySearchSection,
|
||||
whenFilterGroup,
|
||||
} from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
FILE_GROUPS: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
fileGroupOf: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||
DownloadsCommon:
|
||||
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||
DownloadsViewUI:
|
||||
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
});
|
||||
|
||||
const FILE_MIME = "application/x-moz-file";
|
||||
const OPENING_FEEDBACK_MS = 1500;
|
||||
|
||||
export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||
static id = "downloads";
|
||||
static label = "library-downloads-section-title";
|
||||
|
||||
static render(library) {
|
||||
return html`
|
||||
<zen-library-downloads-section
|
||||
class="zen-library-section"
|
||||
data-section="downloads"
|
||||
.library=${library}
|
||||
></zen-library-downloads-section>
|
||||
`;
|
||||
}
|
||||
|
||||
// Oldest to newest, mirroring the order of the underlying download list.
|
||||
#downloads = [];
|
||||
#data = null;
|
||||
#loaded = false;
|
||||
#inBatch = false;
|
||||
#limit = PAGE_SIZE;
|
||||
#visible = [];
|
||||
#refreshed = new WeakSet();
|
||||
#secondsLeft = new WeakMap();
|
||||
|
||||
#menu = null;
|
||||
#menuDownload = null;
|
||||
#menuRow = null;
|
||||
#openingDownload = null;
|
||||
#openingTimer = null;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#data = lazy.DownloadsCommon.getData(window, true);
|
||||
this.#data.addView(this);
|
||||
this.#menu = this.#buildMenu();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#data?.removeView(this);
|
||||
this.#data = null;
|
||||
this.#menu?.hidePopup();
|
||||
this.#menu?.remove();
|
||||
this.#menu = null;
|
||||
clearTimeout(this.#openingTimer);
|
||||
this.#openingTimer = null;
|
||||
this.#openingDownload = null;
|
||||
}
|
||||
|
||||
get searchPlaceholderL10nId() {
|
||||
return "places-search-downloads";
|
||||
}
|
||||
|
||||
get filterTitleL10nId() {
|
||||
return "library-downloads-filter-title";
|
||||
}
|
||||
|
||||
get filterGroups() {
|
||||
return [
|
||||
{
|
||||
id: "type",
|
||||
titleL10nId: "library-downloads-filter-type",
|
||||
options: lazy.FILE_GROUPS.map(id => ({
|
||||
id,
|
||||
l10nId: `library-downloads-type-${id}`,
|
||||
})),
|
||||
},
|
||||
whenFilterGroup("library-downloads-filter-when"),
|
||||
];
|
||||
}
|
||||
|
||||
onSearchChanged() {
|
||||
this.#limit = PAGE_SIZE;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
onFiltersChanged() {
|
||||
this.#limit = PAGE_SIZE;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
onListScrolledToEnd() {
|
||||
if (this.#visible.length < this.#limit) {
|
||||
return;
|
||||
}
|
||||
this.#limit += PAGE_SIZE;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
// DownloadList view
|
||||
|
||||
onDownloadBatchStarting() {
|
||||
this.#inBatch = true;
|
||||
}
|
||||
|
||||
onDownloadBatchEnded() {
|
||||
this.#inBatch = false;
|
||||
this.#loaded = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
onDownloadAdded(download, { insertBefore } = {}) {
|
||||
const index = insertBefore ? this.#downloads.indexOf(insertBefore) : -1;
|
||||
if (index === -1) {
|
||||
this.#downloads.push(download);
|
||||
} else {
|
||||
this.#downloads.splice(index, 0, download);
|
||||
}
|
||||
this.#scheduleUpdate();
|
||||
}
|
||||
|
||||
onDownloadChanged() {
|
||||
this.#scheduleUpdate();
|
||||
}
|
||||
|
||||
onDownloadRemoved(download) {
|
||||
const index = this.#downloads.indexOf(download);
|
||||
if (index !== -1) {
|
||||
this.#downloads.splice(index, 1);
|
||||
}
|
||||
if (this.#menuDownload === download) {
|
||||
this.#menu?.hidePopup();
|
||||
}
|
||||
this.#scheduleUpdate();
|
||||
}
|
||||
|
||||
#scheduleUpdate() {
|
||||
if (!this.#inBatch) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
// Check that the files of rendered downloads still exist, like the
|
||||
// downloads panel does when it opens.
|
||||
for (const download of this.#visible) {
|
||||
if (download.succeeded && !this.#refreshed.has(download)) {
|
||||
this.#refreshed.add(download);
|
||||
download.refresh().catch(console.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
#fileName(download) {
|
||||
return download.target.path
|
||||
? PathUtils.filename(download.target.path)
|
||||
: download.source.url;
|
||||
}
|
||||
|
||||
#hasFile(download) {
|
||||
return download.succeeded && !download.deleted && download.target.exists;
|
||||
}
|
||||
|
||||
#file(download) {
|
||||
return new lazy.FileUtils.File(download.target.path);
|
||||
}
|
||||
|
||||
#formatUrl(url) {
|
||||
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
#matchesQuery(download) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
return (
|
||||
this.#fileName(download).toLowerCase().includes(query) ||
|
||||
download.source.url.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
#fileType(download) {
|
||||
return lazy.fileGroupOf(this.#fileName(download)) ?? undefined;
|
||||
}
|
||||
|
||||
#activeTypes() {
|
||||
return lazy.FILE_GROUPS.filter(id => this.isFilterActive("type", id));
|
||||
}
|
||||
|
||||
#whenCutoff() {
|
||||
const days = this.activeWhenDays;
|
||||
return days ? Date.now() - days * MS_PER_DAY : 0;
|
||||
}
|
||||
|
||||
#computeVisible() {
|
||||
const visible = [];
|
||||
const types = this.#activeTypes();
|
||||
const cutoff = this.#whenCutoff();
|
||||
for (let i = this.#downloads.length - 1; i >= 0; i--) {
|
||||
const download = this.#downloads[i];
|
||||
if (types.length && !types.includes(this.#fileType(download))) {
|
||||
continue;
|
||||
}
|
||||
if (cutoff && !(download.endTime >= cutoff)) {
|
||||
continue;
|
||||
}
|
||||
if (!this.searchQuery || this.#matchesQuery(download)) {
|
||||
visible.push(download);
|
||||
if (visible.length >= this.#limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
#iconUrl(download) {
|
||||
if (!download.target.path) {
|
||||
return "moz-icon://.unknown?size=32";
|
||||
}
|
||||
return `moz-icon://${download.target.path}?size=32${
|
||||
download.succeeded ? "&state=normal" : ""
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} download - The download a row stands for
|
||||
* @returns {string|null} The finished file itself, when it is a picture
|
||||
* worth showing in place of a file icon
|
||||
*/
|
||||
#previewUrl(download) {
|
||||
const path = download.succeeded && download.target.path;
|
||||
if (!path || !lazy.canDrawThumbnail(path)) {
|
||||
return null;
|
||||
}
|
||||
return PathUtils.toFileURI(path);
|
||||
}
|
||||
|
||||
#isPending(download) {
|
||||
return !download.stopped || (download.canceled && download.hasPartialData);
|
||||
}
|
||||
|
||||
#joinStatus(...parts) {
|
||||
return parts
|
||||
.filter(Boolean)
|
||||
.reduce((a, b) => lazy.DownloadsCommon.strings.statusSeparator(a, b));
|
||||
}
|
||||
|
||||
#statusText(download) {
|
||||
const strings = lazy.DownloadsCommon.strings;
|
||||
const totalBytes = download.hasProgress ? download.totalBytes : -1;
|
||||
if (!download.stopped) {
|
||||
const [statusText, secondsLeft] = lazy.DownloadUtils.getDownloadStatus(
|
||||
download.currentBytes,
|
||||
totalBytes,
|
||||
download.speed,
|
||||
this.#secondsLeft.get(download) ?? Infinity
|
||||
);
|
||||
this.#secondsLeft.set(download, secondsLeft);
|
||||
return statusText;
|
||||
}
|
||||
this.#secondsLeft.delete(download);
|
||||
if (download.deleted) {
|
||||
return strings.fileDeleted;
|
||||
}
|
||||
if (download.succeeded) {
|
||||
if (!download.target.exists) {
|
||||
return strings.fileMovedOrMissing;
|
||||
}
|
||||
const parsed = URL.parse(download.source.url);
|
||||
const uri = parsed && Services.io.newURI(parsed.href);
|
||||
const host = uri
|
||||
? lazy.BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true })
|
||||
: "";
|
||||
const [date] = lazy.DownloadUtils.getReadableDates(
|
||||
new Date(download.endTime)
|
||||
);
|
||||
return this.#joinStatus(
|
||||
lazy.DownloadsViewUI.getSizeWithUnits(download),
|
||||
host,
|
||||
date
|
||||
);
|
||||
}
|
||||
if (download.canceled && download.hasPartialData) {
|
||||
return this.#joinStatus(
|
||||
strings.statePaused,
|
||||
lazy.DownloadUtils.getTransferTotal(download.currentBytes, totalBytes)
|
||||
);
|
||||
}
|
||||
if (download.error?.becauseBlockedByParentalControls) {
|
||||
return strings.stateBlockedParentalControls;
|
||||
}
|
||||
if (download.error?.becauseBlockedByReputationCheck) {
|
||||
return strings.blockedMalware;
|
||||
}
|
||||
return download.canceled ? strings.stateCanceled : strings.stateFailed;
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
#showInFolder(download) {
|
||||
if (!this.#hasFile(download)) {
|
||||
return;
|
||||
}
|
||||
lazy.DownloadsCommon.showDownloadedFile(this.#file(download));
|
||||
}
|
||||
|
||||
#onRowClick(download) {
|
||||
if (!this.#hasFile(download)) {
|
||||
return;
|
||||
}
|
||||
this.#showInFolder(download);
|
||||
clearTimeout(this.#openingTimer);
|
||||
this.#openingDownload = download;
|
||||
this.#openingTimer = setTimeout(() => {
|
||||
this.#openingTimer = null;
|
||||
this.#openingDownload = null;
|
||||
this.requestUpdate();
|
||||
}, OPENING_FEEDBACK_MS);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
#canRetry(download) {
|
||||
return (
|
||||
download.stopped &&
|
||||
!download.succeeded &&
|
||||
(download.canceled || !!download.error)
|
||||
);
|
||||
}
|
||||
|
||||
#retryDownload(download) {
|
||||
if (download.start) {
|
||||
// Errors when retrying are already reported as download failures.
|
||||
download.start().catch(() => {});
|
||||
return;
|
||||
}
|
||||
// History-only entries have no session object, so download the URL again.
|
||||
const targetName = download.target.path
|
||||
? PathUtils.filename(download.target.path)
|
||||
: null;
|
||||
window.DownloadURL(download.source.url, targetName, document);
|
||||
}
|
||||
|
||||
#cancelDownload(download) {
|
||||
download.cancel().catch(() => {});
|
||||
download
|
||||
.removePartialData()
|
||||
.catch(console.error)
|
||||
.finally(() => download.target.refresh());
|
||||
}
|
||||
|
||||
#openDownload(download) {
|
||||
if (!this.#hasFile(download)) {
|
||||
return;
|
||||
}
|
||||
lazy.DownloadsCommon.openDownload(download, { openWhere: "tab" });
|
||||
}
|
||||
|
||||
#copyFile(download) {
|
||||
if (!this.#hasFile(download)) {
|
||||
return;
|
||||
}
|
||||
const transferable = Cc[
|
||||
"@mozilla.org/widget/transferable;1"
|
||||
].createInstance(Ci.nsITransferable);
|
||||
transferable.init(window.docShell.QueryInterface(Ci.nsILoadContext));
|
||||
transferable.addDataFlavor(FILE_MIME);
|
||||
transferable.setTransferData(FILE_MIME, this.#file(download));
|
||||
Services.clipboard.setData(
|
||||
transferable,
|
||||
null,
|
||||
Services.clipboard.kGlobalClipboard
|
||||
);
|
||||
}
|
||||
|
||||
#hideDownload(download) {
|
||||
lazy.DownloadsCommon.deleteDownload(download).catch(console.error);
|
||||
}
|
||||
|
||||
#trashDownload(download) {
|
||||
lazy.DownloadsCommon.deleteDownloadFiles(
|
||||
download,
|
||||
lazy.DownloadsViewUI.clearHistoryOnDelete
|
||||
).catch(console.error);
|
||||
}
|
||||
|
||||
#isActionEnabled(action, download) {
|
||||
switch (action) {
|
||||
case "open":
|
||||
case "copy":
|
||||
case "show":
|
||||
return this.#hasFile(download);
|
||||
case "copy-link":
|
||||
return !download.source.isDataURICleared;
|
||||
case "trash":
|
||||
return (
|
||||
this.#hasFile(download) ||
|
||||
!download.stopped ||
|
||||
download.hasPartialData
|
||||
);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
#doAction(action, download) {
|
||||
switch (action) {
|
||||
case "open":
|
||||
this.#openDownload(download);
|
||||
break;
|
||||
case "copy":
|
||||
this.#copyFile(download);
|
||||
break;
|
||||
case "copy-link":
|
||||
lazy.DownloadsCommon.copyDownloadLink(download);
|
||||
break;
|
||||
case "show":
|
||||
this.#showInFolder(download);
|
||||
break;
|
||||
case "hide":
|
||||
this.#hideDownload(download);
|
||||
break;
|
||||
case "trash":
|
||||
this.#trashDownload(download);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#onDragStart(event, download) {
|
||||
if (!this.#hasFile(download) || !this.#file(download).exists()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const file = this.#file(download);
|
||||
const { dataTransfer } = event;
|
||||
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
|
||||
dataTransfer.effectAllowed = "copyMove";
|
||||
dataTransfer.setData("text/uri-list", Services.io.newFileURI(file).spec);
|
||||
dataTransfer.addElement(event.currentTarget);
|
||||
// eslint-disable-next-line mozilla/valid-services
|
||||
Services.zen.playHapticFeedback();
|
||||
}
|
||||
|
||||
// Context menu
|
||||
|
||||
#buildMenu() {
|
||||
const menu = window.MozXULElement.parseXULToFragment(`
|
||||
<menupopup class="zen-library-downloads-menu">
|
||||
<menuitem data-action="open" data-l10n-id="library-downloads-menu-open"/>
|
||||
<menuitem data-action="show" data-l10n-id="downloads-cmd-show-menuitem-2"/>
|
||||
<menuseparator/>
|
||||
<menuitem data-action="copy" data-l10n-id="library-downloads-menu-copy"/>
|
||||
<menuitem data-action="copy-link" data-l10n-id="downloads-cmd-copy-download-link"/>
|
||||
<menuseparator/>
|
||||
<menuitem data-action="hide" data-l10n-id="library-downloads-menu-hide"/>
|
||||
<menuitem data-action="trash" data-l10n-id="library-downloads-menu-trash"/>
|
||||
</menupopup>
|
||||
`).firstElementChild;
|
||||
menu.addEventListener("command", event => {
|
||||
if (this.#menuDownload) {
|
||||
this.#doAction(event.target.dataset.action, this.#menuDownload);
|
||||
}
|
||||
});
|
||||
menu.addEventListener("popuphidden", () => {
|
||||
this.#menuRow?.removeAttribute("menu-open");
|
||||
this.#menuRow = null;
|
||||
this.#menuDownload = null;
|
||||
});
|
||||
document.getElementById("mainPopupSet").appendChild(menu);
|
||||
return menu;
|
||||
}
|
||||
|
||||
#openMenu(download, row, anchor, event) {
|
||||
const fileName = this.#fileName(download);
|
||||
for (const item of this.#menu.querySelectorAll("menuitem")) {
|
||||
const { action } = item.dataset;
|
||||
if (action === "open" || action === "copy") {
|
||||
document.l10n.setAttributes(item, item.dataset.l10nId, {
|
||||
name: fileName,
|
||||
});
|
||||
}
|
||||
item.disabled = !this.#isActionEnabled(action, download);
|
||||
}
|
||||
this.#menuRow?.removeAttribute("menu-open");
|
||||
this.#menuRow = row;
|
||||
this.#menuDownload = download;
|
||||
row.setAttribute("menu-open", "true");
|
||||
if (anchor) {
|
||||
this.#menu.openPopup(anchor, "after_end", 0, 4, false, false, event);
|
||||
} else {
|
||||
this.#menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
|
||||
}
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
#renderSubtitle(download) {
|
||||
const statusNode =
|
||||
download === this.#openingDownload
|
||||
? html`<span
|
||||
class="zen-library-download-status"
|
||||
data-l10n-id="library-downloads-opening-in"
|
||||
></span>`
|
||||
: html`<span class="zen-library-download-status"
|
||||
>${this.#statusText(download)}</span
|
||||
>`;
|
||||
return html`
|
||||
<span class="zen-library-row-subtitle">
|
||||
${statusNode}
|
||||
<span class="zen-library-download-url"
|
||||
>${this.#formatUrl(download.source.url)}</span
|
||||
>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderCancelButton(download) {
|
||||
return html`
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1"
|
||||
data-l10n-id="library-downloads-cancel-button"
|
||||
@click=${event => {
|
||||
event.stopPropagation();
|
||||
this.#cancelDownload(download);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://browser/skin/zen-icons/close.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderRetryButton(download) {
|
||||
return html`
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1"
|
||||
data-l10n-id="library-downloads-retry-button"
|
||||
@click=${event => {
|
||||
event.stopPropagation();
|
||||
this.#retryDownload(download);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://browser/skin/zen-icons/reload.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderDownload(download) {
|
||||
const pending = this.#isPending(download);
|
||||
const progress = download.hasProgress ? download.progress : 0;
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-row"
|
||||
draggable="true"
|
||||
style="--zen-library-download-progress: ${progress}%"
|
||||
?pending=${pending}
|
||||
?indeterminate=${pending && !download.hasProgress}
|
||||
?paused=${pending && download.stopped}
|
||||
?opening=${download === this.#openingDownload}
|
||||
@click=${() => this.#onRowClick(download)}
|
||||
@contextmenu=${event => {
|
||||
event.preventDefault();
|
||||
this.#openMenu(download, event.currentTarget, null, event);
|
||||
}}
|
||||
@dragstart=${event => this.#onDragStart(event, download)}
|
||||
>
|
||||
<img
|
||||
class="zen-library-row-icon"
|
||||
?preview=${!!this.#previewUrl(download)}
|
||||
src=${this.#previewUrl(download) ?? this.#iconUrl(download)}
|
||||
@error=${event => {
|
||||
event.target.removeAttribute("preview");
|
||||
event.target.src = this.#iconUrl(download);
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
<div class="zen-library-row-text">
|
||||
<span class="zen-library-row-title">${this.#fileName(download)}</span>
|
||||
${this.#renderSubtitle(download)}
|
||||
</div>
|
||||
<div class="zen-library-row-actions">
|
||||
${when(!download.stopped, () => this.#renderCancelButton(download))}
|
||||
${when(this.#canRetry(download), () =>
|
||||
this.#renderRetryButton(download)
|
||||
)}
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1"
|
||||
data-l10n-id="library-downloads-more-button"
|
||||
@click=${event => {
|
||||
event.stopPropagation();
|
||||
this.#openMenu(
|
||||
download,
|
||||
event.currentTarget.closest(".zen-library-row"),
|
||||
event.currentTarget,
|
||||
event
|
||||
);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://global/skin/icons/more.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
if (!this.#loaded) {
|
||||
return null;
|
||||
}
|
||||
this.#visible = this.#computeVisible();
|
||||
if (!this.#visible.length) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-empty"
|
||||
data-l10n-id="library-downloads-empty"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="zen-library-group">
|
||||
${repeat(
|
||||
this.#visible,
|
||||
download => download,
|
||||
download => this.#renderDownload(download)
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(
|
||||
"zen-library-downloads-section",
|
||||
ZenLibraryDownloadsSection
|
||||
);
|
||||
@@ -1,361 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html, repeat } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import {
|
||||
MS_PER_DAY,
|
||||
PAGE_SIZE,
|
||||
ZenLibrarySearchSection,
|
||||
whenFilterGroup,
|
||||
} from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
PlacesQuery: "resource://gre/modules/PlacesQuery.sys.mjs",
|
||||
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"relativeDayFormat",
|
||||
() => new Intl.RelativeTimeFormat(undefined, { numeric: "auto" })
|
||||
);
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"dateFormat",
|
||||
() => new Intl.DateTimeFormat(undefined, { dateStyle: "medium" })
|
||||
);
|
||||
|
||||
const HISTORY_DAYS_OLD = 120;
|
||||
const SORT_OPTIONS = ["date", "site", "mostvisited", "lastvisited"];
|
||||
|
||||
const visitKey = visit => `${visit.guid}-${visit.date.getTime()}`;
|
||||
|
||||
export class ZenLibraryHistorySection extends ZenLibrarySearchSection {
|
||||
static id = "history";
|
||||
static label = "library-history-section-title";
|
||||
|
||||
static render(library) {
|
||||
return html`
|
||||
<zen-library-history-section
|
||||
class="zen-library-section"
|
||||
data-section="history"
|
||||
.library=${library}
|
||||
></zen-library-history-section>
|
||||
`;
|
||||
}
|
||||
|
||||
static properties = {
|
||||
visits: { state: true },
|
||||
};
|
||||
|
||||
#placesQuery = null;
|
||||
#limit = PAGE_SIZE;
|
||||
#fetchGeneration = 0;
|
||||
#exhausted = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.visits = null;
|
||||
this.activeFilters.add("sort:date");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.#placesQuery = new lazy.PlacesQuery();
|
||||
this.#placesQuery.observeHistory(() => this.#fetch());
|
||||
this.#fetch();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#placesQuery?.close();
|
||||
this.#placesQuery = null;
|
||||
}
|
||||
|
||||
get searchPlaceholderL10nId() {
|
||||
return "library-history-search-placeholder";
|
||||
}
|
||||
|
||||
get filterTitleL10nId() {
|
||||
return "library-history-filter-title";
|
||||
}
|
||||
|
||||
get filterGroups() {
|
||||
return [
|
||||
whenFilterGroup("library-history-filter-when"),
|
||||
{
|
||||
id: "sort",
|
||||
titleL10nId: "library-history-filter-sort",
|
||||
exclusive: true,
|
||||
options: [
|
||||
{ id: "date", l10nId: "library-history-sort-date" },
|
||||
{ id: "site", l10nId: "library-history-sort-site" },
|
||||
{ id: "mostvisited", l10nId: "library-history-sort-most-visited" },
|
||||
{ id: "lastvisited", l10nId: "library-history-sort-last-visited" },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get #activeDaysOld() {
|
||||
return this.activeWhenDays ?? HISTORY_DAYS_OLD;
|
||||
}
|
||||
|
||||
get #activeSort() {
|
||||
return SORT_OPTIONS.find(id => this.isFilterActive("sort", id)) ?? "date";
|
||||
}
|
||||
|
||||
onSearchChanged() {
|
||||
this.#resetAndFetch();
|
||||
}
|
||||
|
||||
onFiltersChanged() {
|
||||
if (!SORT_OPTIONS.some(id => this.isFilterActive("sort", id))) {
|
||||
this.activeFilters.add("sort:date");
|
||||
this.requestUpdate();
|
||||
}
|
||||
this.#resetAndFetch();
|
||||
}
|
||||
|
||||
onListScrolledToEnd() {
|
||||
if (this.#exhausted || !this.#placesQuery) {
|
||||
return;
|
||||
}
|
||||
this.#limit += PAGE_SIZE;
|
||||
this.#fetch();
|
||||
}
|
||||
|
||||
#resetAndFetch() {
|
||||
this.#limit = PAGE_SIZE;
|
||||
this.#exhausted = false;
|
||||
this.#fetch();
|
||||
}
|
||||
|
||||
async #fetch() {
|
||||
if (!this.#placesQuery) {
|
||||
return;
|
||||
}
|
||||
const generation = ++this.#fetchGeneration;
|
||||
const daysOld = this.#activeDaysOld;
|
||||
let visits;
|
||||
if (this.searchQuery) {
|
||||
visits =
|
||||
(await this.#placesQuery.searchHistory(
|
||||
this.searchQuery,
|
||||
this.#limit
|
||||
)) ?? [];
|
||||
} else if (this.#activeSort === "mostvisited") {
|
||||
visits = this.#fetchMostVisited(daysOld, this.#limit);
|
||||
} else {
|
||||
visits = await this.#placesQuery.getHistory({
|
||||
daysOld,
|
||||
limit: this.#limit,
|
||||
sortBy: this.#activeSort,
|
||||
});
|
||||
}
|
||||
if (generation !== this.#fetchGeneration || !this.#placesQuery) {
|
||||
return;
|
||||
}
|
||||
this.#exhausted = this.#countVisits(visits) < this.#limit;
|
||||
if (this.searchQuery && daysOld !== HISTORY_DAYS_OLD) {
|
||||
const cutoff = Date.now() - daysOld * MS_PER_DAY;
|
||||
visits = visits.filter(visit => visit.date.getTime() >= cutoff);
|
||||
}
|
||||
this.visits = visits;
|
||||
// The cached Map is mutated in place, so its identity may not change.
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
#countVisits(visits) {
|
||||
if (Array.isArray(visits)) {
|
||||
return visits.length;
|
||||
}
|
||||
let count = 0;
|
||||
for (const groupVisits of visits.values()) {
|
||||
count += groupVisits.length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
#fetchMostVisited(daysOld, limit) {
|
||||
const query = lazy.PlacesUtils.history.getNewQuery();
|
||||
query.beginTime = lazy.PlacesUtils.toPRTime(
|
||||
Date.now() - daysOld * MS_PER_DAY
|
||||
);
|
||||
const options = lazy.PlacesUtils.history.getNewQueryOptions();
|
||||
options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
|
||||
options.maxResults = limit;
|
||||
const root = lazy.PlacesUtils.history.executeQuery(query, options).root;
|
||||
root.containerOpen = true;
|
||||
const visits = [];
|
||||
for (let i = 0; i < root.childCount; i++) {
|
||||
const node = root.getChild(i);
|
||||
visits.push({
|
||||
url: node.uri,
|
||||
title: node.title,
|
||||
date: lazy.PlacesUtils.toDate(node.time),
|
||||
guid: node.pageGuid,
|
||||
});
|
||||
}
|
||||
root.containerOpen = false;
|
||||
return visits;
|
||||
}
|
||||
|
||||
#openVisit(visit) {
|
||||
window.openTrustedLinkIn(visit.url, "tab");
|
||||
this.library?.constructor.toggle();
|
||||
}
|
||||
|
||||
#forgetVisit(visit) {
|
||||
lazy.PlacesUtils.history.remove(visit.url);
|
||||
this.visits = this.#withoutUrl(this.visits, visit.url);
|
||||
}
|
||||
|
||||
#withoutUrl(container, url) {
|
||||
if (Array.isArray(container)) {
|
||||
return container.filter(visit => visit.url !== url);
|
||||
}
|
||||
const remaining = new Map();
|
||||
for (const [key, groupVisits] of container) {
|
||||
const filtered = groupVisits.filter(visit => visit.url !== url);
|
||||
if (filtered.length) {
|
||||
remaining.set(key, filtered);
|
||||
}
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
#formatDay(day) {
|
||||
const daysAgo = Math.round(
|
||||
(this.#placesQuery.getStartOfDayTimestamp(new Date()) - day) / MS_PER_DAY
|
||||
);
|
||||
const formatted =
|
||||
daysAgo <= 6
|
||||
? lazy.relativeDayFormat.format(-daysAgo, "day")
|
||||
: lazy.dateFormat.format(day);
|
||||
return formatted.charAt(0).toLocaleUpperCase() + formatted.slice(1);
|
||||
}
|
||||
|
||||
#formatUrl(url) {
|
||||
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* A visit drags as a link, so it can be dropped onto content, the tab
|
||||
* strip or another app.
|
||||
*
|
||||
* @param {DragEvent} event
|
||||
* @param {object} visit
|
||||
*/
|
||||
#onDragStart(event, visit) {
|
||||
const { dataTransfer } = event;
|
||||
const title = visit.title || visit.url;
|
||||
dataTransfer.setData("text/x-moz-url", `${visit.url}\n${title}`);
|
||||
dataTransfer.setData("text/uri-list", visit.url);
|
||||
dataTransfer.setData("text/plain", visit.url);
|
||||
dataTransfer.effectAllowed = "copyLink";
|
||||
dataTransfer.addElement(event.currentTarget);
|
||||
// eslint-disable-next-line mozilla/valid-services
|
||||
Services.zen.playHapticFeedback();
|
||||
}
|
||||
|
||||
#renderVisit(visit) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-row"
|
||||
draggable="true"
|
||||
@click=${() => this.#openVisit(visit)}
|
||||
@dragstart=${event => this.#onDragStart(event, visit)}
|
||||
>
|
||||
<img class="zen-library-row-icon" src="page-icon:${visit.url}" alt="" />
|
||||
<div class="zen-library-row-text">
|
||||
<span class="zen-library-row-title">${visit.title || visit.url}</span>
|
||||
<span class="zen-library-row-subtitle"
|
||||
>${this.#formatUrl(visit.url)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="zen-library-row-actions">
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1"
|
||||
data-l10n-id="library-history-forget-button"
|
||||
@click=${event => {
|
||||
event.stopPropagation();
|
||||
this.#forgetVisit(visit);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://browser/skin/zen-icons/trash.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1"
|
||||
data-l10n-id="library-history-reopen-button"
|
||||
@click=${event => {
|
||||
event.stopPropagation();
|
||||
this.#openVisit(visit);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://browser/skin/zen-icons/u-turn-to-left.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderVisits(visits) {
|
||||
return repeat(visits, visitKey, visit => this.#renderVisit(visit));
|
||||
}
|
||||
|
||||
#renderGroupHeader(key) {
|
||||
if (typeof key === "number") {
|
||||
return html`<h3>${this.#formatDay(key)}</h3>`;
|
||||
}
|
||||
return key
|
||||
? html`<h3>${key}</h3>`
|
||||
: html`<h3 data-l10n-id="library-history-site-other"></h3>`;
|
||||
}
|
||||
|
||||
#renderEmpty() {
|
||||
return html`
|
||||
<div class="zen-library-empty" data-l10n-id="library-history-empty"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
if (!this.visits) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(this.visits)) {
|
||||
if (!this.visits.length) {
|
||||
return this.#renderEmpty();
|
||||
}
|
||||
return html`
|
||||
<div class="zen-library-group">${this.#renderVisits(this.visits)}</div>
|
||||
`;
|
||||
}
|
||||
if (!this.visits.size) {
|
||||
return this.#renderEmpty();
|
||||
}
|
||||
return repeat(
|
||||
this.visits.entries(),
|
||||
([key]) => key,
|
||||
([key, groupVisits]) => html`
|
||||
<div class="zen-library-group">
|
||||
${this.#renderGroupHeader(key)} ${this.#renderVisits(groupVisits)}
|
||||
</div>
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("zen-library-history-section", ZenLibraryHistorySection);
|
||||
@@ -1,474 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { ZenLibrarySearchSection } from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||
import { ZenLibraryMediaPreview } from "moz-src:///zen/library/ZenLibraryMediaPreview.mjs";
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
mediaKindOf: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
});
|
||||
|
||||
const FILE_MIME = "application/x-moz-file";
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const FOLDERS_PREF = "zen.library.media.folders";
|
||||
const ENABLED_PREF = "zen.library.media.enabled";
|
||||
|
||||
const FOLDERS = {
|
||||
downloads: { key: "DfltDwnld", home: "Downloads" },
|
||||
documents: { key: "Docs", home: "Documents" },
|
||||
desktop: { key: "Desk", home: "Desktop" },
|
||||
pictures: { home: "Pictures" },
|
||||
music: { home: "Music" },
|
||||
videos: { home: AppConstants.platform === "macosx" ? "Movies" : "Videos" },
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file found while looking through a folder
|
||||
* @returns {string|null} How the grid shows it
|
||||
*/
|
||||
function kindFor(fileName) {
|
||||
const kind = lazy.mediaKindOf(fileName);
|
||||
if (kind === "video") {
|
||||
return "video";
|
||||
}
|
||||
if (kind === "image") {
|
||||
return lazy.canDrawThumbnail(fileName) ? "image" : "file";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const MAX_DEPTH = 2;
|
||||
const MAX_FOLDERS = 60;
|
||||
const MAX_ITEMS = 300;
|
||||
|
||||
export class ZenLibraryMediaSection extends ZenLibrarySearchSection {
|
||||
static id = "media";
|
||||
static label = "library-media-section-title";
|
||||
|
||||
static properties = {
|
||||
items: { state: true },
|
||||
loading: { state: true },
|
||||
};
|
||||
|
||||
#preview = null;
|
||||
#scanId = 0;
|
||||
#scanned = [];
|
||||
#scanHandle = null;
|
||||
#thumbObserver = null;
|
||||
#cardItems = new WeakMap();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
static render(library) {
|
||||
return html`
|
||||
<zen-library-media-section
|
||||
class="zen-library-section"
|
||||
data-section="media"
|
||||
.library=${library}
|
||||
></zen-library-media-section>
|
||||
`;
|
||||
}
|
||||
|
||||
get searchPlaceholderL10nId() {
|
||||
return "library-media-search-placeholder";
|
||||
}
|
||||
|
||||
get filterTitleL10nId() {
|
||||
return "library-media-filter-title";
|
||||
}
|
||||
|
||||
get filterGroups() {
|
||||
if (!this.#enabled || this.#scanned.length < 2) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "folder",
|
||||
titleL10nId: "library-media-filter-folders",
|
||||
options: this.#scanned.map(path => ({
|
||||
id: path,
|
||||
label: PathUtils.filename(path),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
onFiltersChanged() {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.#enabled) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.#scanHandle = window.requestIdleCallback(() => this.#scan(), {
|
||||
timeout: 600,
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {boolean} Whether looking through the folders was agreed to */
|
||||
get #enabled() {
|
||||
return Services.prefs.getBoolPref(ENABLED_PREF, false);
|
||||
}
|
||||
|
||||
#enable() {
|
||||
Services.prefs.setBoolPref(ENABLED_PREF, true);
|
||||
this.loading = true;
|
||||
this.requestUpdate();
|
||||
this.#scan();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#scanId++;
|
||||
if (this.#scanHandle) {
|
||||
window.cancelIdleCallback(this.#scanHandle);
|
||||
this.#scanHandle = null;
|
||||
}
|
||||
this.#thumbObserver?.disconnect();
|
||||
this.#thumbObserver = null;
|
||||
this.#preview?.destroy();
|
||||
this.#preview = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]} Absolute paths of the folders to look through. The
|
||||
* pref takes named locations such as "downloads" and absolute paths
|
||||
* alike, and anything that cannot be found is passed over.
|
||||
*/
|
||||
#folders() {
|
||||
const home = Services.dirsvc.get("Home", Ci.nsIFile).path;
|
||||
const paths = [];
|
||||
for (const raw of Services.prefs
|
||||
.getStringPref(FOLDERS_PREF, "")
|
||||
.split(",")) {
|
||||
const entry = raw.trim();
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
if (PathUtils.isAbsolute(entry)) {
|
||||
paths.push(entry);
|
||||
continue;
|
||||
}
|
||||
const folder = FOLDERS[entry.toLowerCase()];
|
||||
if (!folder) {
|
||||
continue;
|
||||
}
|
||||
let path = null;
|
||||
if (folder.key) {
|
||||
try {
|
||||
path = Services.dirsvc.get(folder.key, Ci.nsIFile).path;
|
||||
} catch (error) {
|
||||
path = null;
|
||||
}
|
||||
}
|
||||
paths.push(path ?? PathUtils.join(home, folder.home));
|
||||
}
|
||||
return [...new Set(paths)];
|
||||
}
|
||||
|
||||
async #scan() {
|
||||
const scanId = ++this.#scanId;
|
||||
this.#scanHandle = null;
|
||||
this.loading = true;
|
||||
const found = [];
|
||||
const budget = { folders: MAX_FOLDERS };
|
||||
this.#scanned = this.#folders();
|
||||
for (const folder of this.#scanned) {
|
||||
budget.root = folder;
|
||||
await this.#walk(folder, 0, found, budget);
|
||||
if (scanId !== this.#scanId) {
|
||||
return;
|
||||
}
|
||||
// Let the window breathe between folders.
|
||||
await new Promise(resolve =>
|
||||
window.requestIdleCallback(resolve, { timeout: 100 })
|
||||
);
|
||||
}
|
||||
found.sort((a, b) => b.modified - a.modified);
|
||||
this.items = found.slice(0, MAX_ITEMS);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path - Folder to look through
|
||||
* @param {number} depth - How far below the named folder this is
|
||||
* @param {object[]} found - Collects the media
|
||||
* @param {object} budget - Caps how many folders one scan opens
|
||||
*/
|
||||
async #walk(path, depth, found, budget) {
|
||||
if (depth > MAX_DEPTH || budget.folders <= 0) {
|
||||
return;
|
||||
}
|
||||
budget.folders--;
|
||||
let children = [];
|
||||
try {
|
||||
children = await IOUtils.getChildren(path);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
const candidates = [];
|
||||
for (const child of children) {
|
||||
const fileName = PathUtils.filename(child);
|
||||
if (fileName.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
const dot = fileName.lastIndexOf(".");
|
||||
const extension = dot > 0 ? fileName.slice(dot + 1).toLowerCase() : "";
|
||||
const kind = kindFor(fileName);
|
||||
// A name with a suffix that is not media is a file to pass over, or a
|
||||
// bundle on macOS. Either way there is nothing to ask the disk about.
|
||||
if (!kind && extension) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({ child, fileName, extension, kind });
|
||||
}
|
||||
const stats = await Promise.all(
|
||||
candidates.map(candidate =>
|
||||
IOUtils.stat(candidate.child).catch(() => null)
|
||||
)
|
||||
);
|
||||
const folders = [];
|
||||
for (const [index, candidate] of candidates.entries()) {
|
||||
const info = stats[index];
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
if (info.type === "directory") {
|
||||
if (!candidate.extension) {
|
||||
folders.push(candidate.child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!candidate.kind) {
|
||||
continue;
|
||||
}
|
||||
found.push({
|
||||
folder: budget.root,
|
||||
path: candidate.child,
|
||||
name: candidate.fileName,
|
||||
extension: candidate.extension,
|
||||
kind: candidate.kind,
|
||||
modified: info.lastModified,
|
||||
size: info.size,
|
||||
url: PathUtils.toFileURI(candidate.child),
|
||||
});
|
||||
}
|
||||
for (const folder of folders) {
|
||||
await this.#walk(folder, depth + 1, found, budget);
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction
|
||||
|
||||
#onDragStart(event, item) {
|
||||
const file = new lazy.FileUtils.File(item.path);
|
||||
if (!file.exists()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const { dataTransfer } = event;
|
||||
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
|
||||
dataTransfer.effectAllowed = "copyMove";
|
||||
dataTransfer.setData("text/uri-list", item.url);
|
||||
dataTransfer.addElement(event.currentTarget);
|
||||
}
|
||||
|
||||
#onItemContextMenu(event, item) {
|
||||
this.#preview ??= new ZenLibraryMediaPreview(window);
|
||||
this.#preview.openContextMenu(event, item);
|
||||
}
|
||||
|
||||
#onItemClick(event, item) {
|
||||
this.#preview ??= new ZenLibraryMediaPreview(window);
|
||||
const shown = this.#shown;
|
||||
this.#preview.open({
|
||||
items: shown,
|
||||
index: shown.indexOf(item),
|
||||
from: event.currentTarget.querySelector(".zen-library-media-frame"),
|
||||
});
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
/**
|
||||
* Builds one card, empty. Its picture is fetched only once the card comes
|
||||
* into view, so opening the tab does not read every file at once.
|
||||
*
|
||||
* @param {object} item - The media the card stands for
|
||||
* @returns {Element} Its card
|
||||
*/
|
||||
#buildCard(item) {
|
||||
const card = document.createElementNS(HTML_NS, "div");
|
||||
card.className = "zen-library-media-item";
|
||||
card.draggable = true;
|
||||
card.title = item.name;
|
||||
card.addEventListener("dragstart", event => this.#onDragStart(event, item));
|
||||
card.addEventListener("click", event => this.#onItemClick(event, item));
|
||||
card.addEventListener("contextmenu", event =>
|
||||
this.#onItemContextMenu(event, item)
|
||||
);
|
||||
|
||||
const frame = document.createElementNS(HTML_NS, "div");
|
||||
frame.className = "zen-library-media-frame";
|
||||
frame.dataset.kind = item.kind;
|
||||
card.appendChild(frame);
|
||||
|
||||
this.#cardItems.set(card, item);
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} card - A card that has come into view
|
||||
*/
|
||||
#loadThumb(card) {
|
||||
const item = this.#cardItems.get(card);
|
||||
const frame = card.firstElementChild;
|
||||
if (!item || !frame || frame.childElementCount) {
|
||||
return;
|
||||
}
|
||||
const make = tag => {
|
||||
const element = document.createElementNS(HTML_NS, tag);
|
||||
frame.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
if (item.kind === "video") {
|
||||
const video = make("video");
|
||||
video.className = "zen-library-media-thumb";
|
||||
video.src = `${item.url}#t=0.1`;
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.tabIndex = -1;
|
||||
const badge = make("img");
|
||||
badge.className = "zen-library-media-play no-squircles";
|
||||
badge.src = "chrome://browser/skin/zen-icons/media-play.svg";
|
||||
badge.alt = "";
|
||||
return;
|
||||
}
|
||||
const image = make("img");
|
||||
image.className =
|
||||
item.kind === "image"
|
||||
? "zen-library-media-thumb"
|
||||
: "zen-library-media-thumb zen-library-media-file";
|
||||
image.src =
|
||||
item.kind === "image" ? item.url : `moz-icon://${item.name}?size=128`;
|
||||
image.alt = "";
|
||||
}
|
||||
|
||||
#fillGrid() {
|
||||
const grid = this.querySelector(".zen-library-media-grid");
|
||||
if (!grid || !this.#enabled) {
|
||||
return;
|
||||
}
|
||||
const shown = this.#shown;
|
||||
const signature = shown.map(item => item.path).join("\n");
|
||||
if (grid.dataset.signature === signature) {
|
||||
return;
|
||||
}
|
||||
grid.dataset.signature = signature;
|
||||
this.#thumbObserver?.disconnect();
|
||||
this.#thumbObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
this.#loadThumb(entry.target);
|
||||
this.#thumbObserver.unobserve(entry.target);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: this.querySelector(".zen-library-search-results"),
|
||||
rootMargin: "300px",
|
||||
}
|
||||
);
|
||||
const fragment = document.createDocumentFragment();
|
||||
const cards = shown.map(item => this.#buildCard(item));
|
||||
for (const card of cards) {
|
||||
fragment.appendChild(card);
|
||||
}
|
||||
grid.replaceChildren(fragment);
|
||||
for (const card of cards) {
|
||||
this.#thumbObserver.observe(card);
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {object[]} What the grid shows, once the search has its say */
|
||||
get #shown() {
|
||||
const folders = this.#scanned.filter(path =>
|
||||
this.isFilterActive("folder", path)
|
||||
);
|
||||
const query = this.searchQuery.trim().toLowerCase();
|
||||
return this.items.filter(
|
||||
item =>
|
||||
(!folders.length || folders.includes(item.folder)) &&
|
||||
(!query || item.name.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
if (!this.#enabled) {
|
||||
return this.#renderOptIn();
|
||||
}
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-empty"
|
||||
data-l10n-id="library-media-loading"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
const shown = this.#shown;
|
||||
if (!shown.length) {
|
||||
return html`
|
||||
<div class="zen-library-empty" data-l10n-id="library-media-empty"></div>
|
||||
`;
|
||||
}
|
||||
return html`<div class="zen-library-media-grid"></div>`;
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated?.(changedProperties);
|
||||
this.toggleAttribute("needs-opt-in", !this.#enabled);
|
||||
this.#fillGrid();
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty grid behind a note saying what turning this on would do. The
|
||||
* cards are only shapes: nothing has been looked at yet.
|
||||
*/
|
||||
#renderOptIn() {
|
||||
return html`
|
||||
<div class="zen-library-media-skeleton" behind></div>
|
||||
<div class="zen-library-media-opt-in">
|
||||
<div class="zen-library-media-opt-in-icon">
|
||||
<div class="empty-state-icon-image"></div>
|
||||
</div>
|
||||
<h3 data-l10n-id="library-media-opt-in-title"></h3>
|
||||
<p data-l10n-id="library-media-opt-in-body"></p>
|
||||
<button
|
||||
class="zen-big-accent-button primary zen-library-media-opt-in-button"
|
||||
data-l10n-id="library-media-opt-in-button"
|
||||
@click=${() => this.#enable()}
|
||||
></button>
|
||||
<div class="zen-library-media-opt-in-note">
|
||||
<img src="chrome://global/skin/icons/security.svg" alt="" />
|
||||
<span data-l10n-id="library-media-opt-in-note"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("zen-library-media-section", ZenLibraryMediaSection);
|
||||
@@ -1,265 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html, when } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
|
||||
const SEARCH_DEBOUNCE_MS = 250;
|
||||
|
||||
export const PAGE_SIZE = 100;
|
||||
export const MS_PER_DAY = 86400000;
|
||||
const WHEN_DAYS = { today: 1, week: 7, month: 30 };
|
||||
|
||||
/**
|
||||
* Builds the exclusive "when" filter group shared by time-based sections.
|
||||
*
|
||||
* @param {string} titleL10nId - Fluent id of the group title
|
||||
*/
|
||||
export function whenFilterGroup(titleL10nId) {
|
||||
return {
|
||||
id: "when",
|
||||
titleL10nId,
|
||||
exclusive: true,
|
||||
options: Object.keys(WHEN_DAYS).map(id => ({
|
||||
id,
|
||||
l10nId: `library-filter-${id}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export class ZenLibrarySearchSection extends MozLitElement {
|
||||
static properties = {
|
||||
searchQuery: { type: String, state: true },
|
||||
filtersOpen: { type: Boolean, state: true },
|
||||
};
|
||||
|
||||
#searchDebounce = null;
|
||||
#sentinelObserver = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.searchQuery = "";
|
||||
this.filtersOpen = false;
|
||||
this.activeFilters = new Set();
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
get searchPlaceholderL10nId() {
|
||||
return "";
|
||||
}
|
||||
|
||||
get filterTitleL10nId() {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter groups shown in the filter panel. Sections without filters keep
|
||||
* this empty, which also hides the filter button. An `exclusive` group
|
||||
* behaves like a radio group where the active option can also be toggled
|
||||
* back off.
|
||||
*
|
||||
* @returns {{id: string, titleL10nId: string, exclusive?: boolean,
|
||||
* options: {id: string, l10nId?: string, label?: string,
|
||||
* icon?: string}[]}[]}
|
||||
*/
|
||||
get filterGroups() {
|
||||
return [];
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
return null;
|
||||
}
|
||||
|
||||
onSearchChanged() {}
|
||||
onFiltersChanged() {}
|
||||
onListScrolledToEnd() {}
|
||||
|
||||
isFilterActive(groupId, optionId) {
|
||||
return this.activeFilters.has(`${groupId}:${optionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of days selected in the "when" filter group, or null when no
|
||||
* option is active.
|
||||
*/
|
||||
get activeWhenDays() {
|
||||
for (const [id, days] of Object.entries(WHEN_DAYS)) {
|
||||
if (this.isFilterActive("when", id)) {
|
||||
return days;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.#searchDebounce) {
|
||||
clearTimeout(this.#searchDebounce);
|
||||
this.#searchDebounce = null;
|
||||
}
|
||||
this.#sentinelObserver?.disconnect();
|
||||
this.#sentinelObserver = null;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (super.firstUpdated) {
|
||||
super.firstUpdated();
|
||||
}
|
||||
|
||||
const results = this.querySelector(".zen-library-search-results");
|
||||
this.#sentinelObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
if (entries.some(entry => entry.isIntersecting)) {
|
||||
this.onListScrolledToEnd();
|
||||
}
|
||||
},
|
||||
{ root: results, rootMargin: "200px" }
|
||||
);
|
||||
this.#sentinelObserver.observe(
|
||||
this.querySelector(".zen-library-search-sentinel")
|
||||
);
|
||||
|
||||
results.addEventListener(
|
||||
"scroll",
|
||||
() => results.toggleAttribute("scrolled", results.scrollTop > 0),
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
if (super.updated) {
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
if (changedProperties.has("filtersOpen") && this.filtersOpen) {
|
||||
const inner = this.querySelector(".zen-library-filter-panel-inner");
|
||||
this.style.setProperty(
|
||||
"--zen-library-filter-height",
|
||||
`${inner.scrollHeight + 8}px`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#onSearchInput(event) {
|
||||
const { value } = event.target;
|
||||
if (this.#searchDebounce) {
|
||||
clearTimeout(this.#searchDebounce);
|
||||
}
|
||||
this.#searchDebounce = setTimeout(() => {
|
||||
this.#searchDebounce = null;
|
||||
const query = value.trim();
|
||||
if (query !== this.searchQuery) {
|
||||
this.searchQuery = query;
|
||||
this.onSearchChanged();
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
#toggleFilter(group, optionId) {
|
||||
const key = `${group.id}:${optionId}`;
|
||||
const wasActive = this.activeFilters.has(key);
|
||||
if (group.exclusive) {
|
||||
for (const option of group.options) {
|
||||
this.activeFilters.delete(`${group.id}:${option.id}`);
|
||||
}
|
||||
}
|
||||
if (wasActive) {
|
||||
this.activeFilters.delete(key);
|
||||
} else {
|
||||
this.activeFilters.add(key);
|
||||
}
|
||||
this.requestUpdate();
|
||||
this.onFiltersChanged();
|
||||
}
|
||||
|
||||
#renderFilterOption(group, option) {
|
||||
return html`
|
||||
<button
|
||||
class="zen-library-filter-chip"
|
||||
?active=${this.isFilterActive(group.id, option.id)}
|
||||
@click=${() => this.#toggleFilter(group, option.id)}
|
||||
>
|
||||
${when(option.icon, () => html`<img src=${option.icon} alt="" />`)}
|
||||
${
|
||||
option.l10nId
|
||||
? html`<span data-l10n-id=${option.l10nId}></span>`
|
||||
: html`<span>${option.label}</span>`
|
||||
}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderFilterPanel() {
|
||||
return html`
|
||||
<div class="zen-library-filter-header" ?inert=${!this.filtersOpen}>
|
||||
<h2 data-l10n-id=${this.filterTitleL10nId}></h2>
|
||||
<button
|
||||
class="zen-library-filter-done"
|
||||
data-l10n-id="library-filter-done"
|
||||
@click=${() => {
|
||||
this.filtersOpen = false;
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
<div class="zen-library-filter-panel">
|
||||
<div class="zen-library-filter-panel-inner" ?inert=${!this.filtersOpen}>
|
||||
${this.filterGroups.map(
|
||||
group => html`
|
||||
<div class="zen-library-filter-group">
|
||||
<h3 data-l10n-id=${group.titleL10nId}></h3>
|
||||
<div class="zen-library-filter-options">
|
||||
${group.options.map(option =>
|
||||
this.#renderFilterOption(group, option)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const hasFilters = !!this.filterGroups.length;
|
||||
return html`
|
||||
<div class="zen-library-search-top" ?open=${this.filtersOpen}>
|
||||
<div class="zen-library-search-header" ?inert=${this.filtersOpen}>
|
||||
<div class="zen-library-search-box">
|
||||
<img
|
||||
src="chrome://browser/skin/zen-icons/search-glass.svg"
|
||||
alt=""
|
||||
/>
|
||||
<input
|
||||
type="search"
|
||||
data-l10n-id=${this.searchPlaceholderL10nId}
|
||||
@input=${this.#onSearchInput}
|
||||
/>
|
||||
</div>
|
||||
${when(
|
||||
hasFilters,
|
||||
() => html`
|
||||
<button
|
||||
class="zen-library-filter-button"
|
||||
@click=${() => {
|
||||
this.filtersOpen = true;
|
||||
}}
|
||||
>
|
||||
<img src="chrome://browser/skin/zen-icons/sliders.svg" alt="" />
|
||||
<span data-l10n-id="library-filter-button"></span>
|
||||
</button>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${when(hasFilters, () => this.#renderFilterPanel())}
|
||||
</div>
|
||||
<div class="zen-library-search-results">
|
||||
${this.renderItems()}
|
||||
<div class="zen-library-search-sentinel"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,934 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import {
|
||||
html,
|
||||
repeat,
|
||||
styleMap,
|
||||
} from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
import { ZenLibraryDragAndDrop } from "moz-src:///zen/library/ZenLibraryDragAndDrop.mjs";
|
||||
|
||||
const GRADIENT_TOPIC = "zen-space-gradient-update";
|
||||
const SIZING_FALLBACK_MS = 600;
|
||||
const SCROLL_EDGE_PX = 48;
|
||||
const SCROLL_STEP_PX = 12;
|
||||
|
||||
// Events the copies fire while being built that the rest of the browser must
|
||||
// not mistake for real tab strip changes.
|
||||
// Changes to the real strips that the copies must follow. Attribute changes
|
||||
// count only for what a copy shows.
|
||||
const STRIP_EVENTS = [
|
||||
"TabOpen",
|
||||
"TabClose",
|
||||
"TabMove",
|
||||
"TabPinned",
|
||||
"TabUnpinned",
|
||||
"TabHide",
|
||||
"TabShow",
|
||||
"TabAttrModified",
|
||||
"TabGrouped",
|
||||
"TabUngrouped",
|
||||
"TabGroupCreate",
|
||||
"TabGroupRemoved",
|
||||
"TabGroupMoved",
|
||||
];
|
||||
const SHOWN_ATTRIBUTES = ["label", "image", "pending", "muted", "soundplaying"];
|
||||
|
||||
const CONTAINED_EVENTS = [
|
||||
"TabGrouped",
|
||||
"TabUngrouped",
|
||||
"TabGroupCreate",
|
||||
"TabGroupRemovedFromDOM",
|
||||
"FolderGrouped",
|
||||
"FolderUngrouped",
|
||||
|
||||
"TabGroupCollapse",
|
||||
"TabGroupExpand",
|
||||
];
|
||||
const GROUP_TAGS = ["tab-group", "zen-folder", "tab-split-view-wrapper"];
|
||||
|
||||
export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
static id = "spaces";
|
||||
static label = "library-spaces-section-title";
|
||||
|
||||
static render(library) {
|
||||
return html`
|
||||
<zen-library-spaces-section
|
||||
class="zen-library-section"
|
||||
data-section="spaces"
|
||||
.library=${library}
|
||||
></zen-library-spaces-section>
|
||||
`;
|
||||
}
|
||||
|
||||
static properties = {
|
||||
renaming: { state: true },
|
||||
};
|
||||
|
||||
#observer = { observe: () => this.requestUpdate() };
|
||||
#onDataChanged = () => this.requestUpdate();
|
||||
#onStripEvent = event => {
|
||||
if (this.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
event.type === "TabAttrModified" &&
|
||||
!event.detail?.changed?.some(attr => SHOWN_ATTRIBUTES.includes(attr))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.#scheduleStripsRefresh(
|
||||
event.type === "TabAttrModified"
|
||||
? event.target.getAttribute("zen-workspace-id")
|
||||
: null
|
||||
);
|
||||
};
|
||||
#refreshTimer = null;
|
||||
/** @type {Set<string>|null} The spaces to rebuild, or null for every card */
|
||||
#pendingSpaces = new Set();
|
||||
#resizeObserver = new ResizeObserver(() => this.#updateLibraryWidth());
|
||||
#onScroll = event => {
|
||||
if (event.target.classList.contains("zen-library-space-body")) {
|
||||
this.#updateScrollBorders(event.target);
|
||||
}
|
||||
};
|
||||
#onDragEnd = () => {
|
||||
this.#landing = false;
|
||||
this.#dnd.clearDragOverVisuals();
|
||||
this.#refreshStrips();
|
||||
};
|
||||
#onCopyDragEnd = event => {
|
||||
gBrowser.tabContainer.tabDragAndDrop.handle_dragend(event);
|
||||
this.#onDragEnd();
|
||||
};
|
||||
#landing = false;
|
||||
#sizingTimer = null;
|
||||
#pendingGroups = [];
|
||||
/** @type {WeakMap<Element, Element>} copied tab or group to the real one */
|
||||
#realElements = new WeakMap();
|
||||
/**
|
||||
* Folders are opened and closed here without touching the real ones, so
|
||||
* rebuilt copies keep what was chosen here.
|
||||
*
|
||||
* @type {WeakMap<Element, boolean>} real group to its copy's collapsed state
|
||||
*/
|
||||
#collapsedCopies = new WeakMap();
|
||||
#dnd = new ZenLibraryDragAndDrop(this);
|
||||
#dragIndex = -1;
|
||||
#dropIndex = -1;
|
||||
#dragStartX = 0;
|
||||
#dragCenterX = 0;
|
||||
#dragStartScroll = 0;
|
||||
#lastPointerX = 0;
|
||||
#scrollFrame = null;
|
||||
#slotCenters = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.renaming = null;
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("ZenWorkspaceDataChanged", this.#onDataChanged);
|
||||
window.addEventListener("ZenWorkspacesUIUpdate", this.#onDataChanged);
|
||||
Services.obs.addObserver(this.#observer, GRADIENT_TOPIC);
|
||||
this.#resizeObserver.observe(this);
|
||||
window.addEventListener("dragend", this.#onDragEnd);
|
||||
this.addEventListener("scroll", this.#onScroll, true);
|
||||
for (const type of CONTAINED_EVENTS) {
|
||||
this.addEventListener(type, this.#containEvent, true);
|
||||
}
|
||||
for (const type of STRIP_EVENTS) {
|
||||
window.addEventListener(type, this.#onStripEvent);
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("ZenWorkspaceDataChanged", this.#onDataChanged);
|
||||
window.removeEventListener("ZenWorkspacesUIUpdate", this.#onDataChanged);
|
||||
Services.obs.removeObserver(this.#observer, GRADIENT_TOPIC);
|
||||
this.#resizeObserver.disconnect();
|
||||
window.removeEventListener("dragend", this.#onDragEnd);
|
||||
this.removeEventListener("scroll", this.#onScroll, true);
|
||||
for (const type of CONTAINED_EVENTS) {
|
||||
this.removeEventListener(type, this.#containEvent, true);
|
||||
}
|
||||
for (const type of STRIP_EVENTS) {
|
||||
window.removeEventListener(type, this.#onStripEvent);
|
||||
}
|
||||
clearTimeout(this.#refreshTimer);
|
||||
this.#refreshTimer = null;
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
this.library?.removeAttribute("sizing");
|
||||
this.library?.style.removeProperty("--zen-library-content-width");
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated(changedProperties);
|
||||
this.#fillStrips();
|
||||
this.#updateLibraryWidth();
|
||||
for (const card of this.#cards) {
|
||||
this.#updateScrollBorders(card.querySelector(".zen-library-space-body"));
|
||||
}
|
||||
if (changedProperties.has("renaming") && this.renaming) {
|
||||
const input = this.querySelector(".zen-library-space-name-input");
|
||||
input?.focus();
|
||||
input?.select();
|
||||
}
|
||||
}
|
||||
|
||||
onShown() {
|
||||
this.#updateLibraryWidth();
|
||||
}
|
||||
|
||||
onHidden() {
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
this.library?.removeAttribute("sizing");
|
||||
this.library?.style.removeProperty("--zen-library-content-width");
|
||||
}
|
||||
|
||||
#updateLibraryWidth() {
|
||||
const side = this.library?.querySelector("#zen-library-side");
|
||||
const list = this.querySelector(".zen-library-spaces");
|
||||
if (!side || !list || this.hidden) {
|
||||
return;
|
||||
}
|
||||
const sideWidth = window.windowUtils.getBoundsWithoutFlushing(side).width;
|
||||
const width = `${sideWidth + this.#cardsWidth(list)}px`;
|
||||
if (
|
||||
this.library.style.getPropertyValue("--zen-library-content-width") ===
|
||||
width
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.library.style.setProperty("--zen-library-content-width", width);
|
||||
this.#markResizing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the cards' scrollbars back while the library grows or shrinks
|
||||
* around them, where they would otherwise flash into view for the length
|
||||
* of the animation.
|
||||
*/
|
||||
#markResizing() {
|
||||
const library = this.library;
|
||||
library.setAttribute("sizing", "true");
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
const done = () => {
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
library.removeAttribute("sizing");
|
||||
library.removeEventListener("transitionend", onEnd);
|
||||
};
|
||||
const onEnd = event => {
|
||||
if (event.target === library && event.propertyName === "width") {
|
||||
done();
|
||||
}
|
||||
};
|
||||
library.addEventListener("transitionend", onEnd);
|
||||
this.#sizingTimer = window.setTimeout(done, SIZING_FALLBACK_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* The list stretches to the library, so its scroll width never gets
|
||||
* smaller than the library already is. The cards' own extent does.
|
||||
*
|
||||
* @param {Element} list - The cards' scroller
|
||||
* @returns {number} The width the cards need, the list's padding included
|
||||
*/
|
||||
#cardsWidth(list) {
|
||||
const cards = this.#cards;
|
||||
const { paddingLeft, paddingRight } = window.getComputedStyle(list);
|
||||
const padding = parseFloat(paddingLeft) + parseFloat(paddingRight);
|
||||
if (!cards.length) {
|
||||
return padding;
|
||||
}
|
||||
const first = cards[0];
|
||||
const last = cards.at(-1);
|
||||
return last.offsetLeft + last.offsetWidth - first.offsetLeft + padding;
|
||||
}
|
||||
|
||||
get #cards() {
|
||||
return [...this.querySelectorAll(".zen-library-space")];
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a strip as scrolled past its top or bottom edge so the matching
|
||||
* edge border shows.
|
||||
*
|
||||
* @param {Element|null} body - A card's scrolling body
|
||||
*/
|
||||
#updateScrollBorders(body) {
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const max = body.scrollHeight - body.clientHeight;
|
||||
body.toggleAttribute("scrolled-top", body.scrollTop > 1);
|
||||
body.toggleAttribute(
|
||||
"scrolled-bottom",
|
||||
max > 1 && body.scrollTop < max - 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds copies shortly, once a burst of changes has settled.
|
||||
*
|
||||
* @param {string|null} uuid - The space that changed, or null for all
|
||||
*/
|
||||
#scheduleStripsRefresh(uuid) {
|
||||
if (this.#pendingSpaces) {
|
||||
if (uuid) {
|
||||
this.#pendingSpaces.add(uuid);
|
||||
} else {
|
||||
this.#pendingSpaces = null;
|
||||
}
|
||||
}
|
||||
if (this.#refreshTimer) {
|
||||
return;
|
||||
}
|
||||
this.#refreshTimer = setTimeout(() => {
|
||||
this.#refreshTimer = null;
|
||||
const uuids = this.#pendingSpaces;
|
||||
this.#pendingSpaces = new Set();
|
||||
this.#refreshStrips(uuids);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds copies right away, unless a dropped tab is mid-landing.
|
||||
*
|
||||
* @param {Set<string>|null} uuids - The spaces to rebuild, or null for every
|
||||
* card
|
||||
*/
|
||||
#refreshStrips(uuids = null) {
|
||||
if (this.#landing) {
|
||||
return;
|
||||
}
|
||||
this.#fillStrips(true, uuids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} rebuild - Redo copies that already exist, not only
|
||||
* fill in cards that have none yet
|
||||
* @param {Set<string>|null} uuids - When rebuilding, limit to these spaces.
|
||||
*/
|
||||
#fillStrips(rebuild = false, uuids = null) {
|
||||
for (const card of this.#cards) {
|
||||
if (rebuild && uuids && !uuids.has(card.dataset.uuid)) {
|
||||
continue;
|
||||
}
|
||||
const strip = card.querySelector(".zen-library-space-tabs");
|
||||
if (!rebuild && strip.childElementCount) {
|
||||
continue;
|
||||
}
|
||||
const before = rebuild ? this.#rowPositions(strip) : null;
|
||||
const space = gZenWorkspaces.workspaceElement(card.dataset.uuid);
|
||||
if (!space) {
|
||||
strip.textContent = "";
|
||||
continue;
|
||||
}
|
||||
// Built away from the document, so a card's rows land in one go
|
||||
// rather than one reflow at a time.
|
||||
const rows = document.createDocumentFragment();
|
||||
this.#pendingGroups = [];
|
||||
for (const section of [space.pinnedTabsContainer, space.tabsContainer]) {
|
||||
this.#appendCopy(rows, section);
|
||||
}
|
||||
strip.replaceChildren(rows);
|
||||
this.#fillGroups();
|
||||
// Rows slide to their new places, unless the drag image is landing on
|
||||
// the moved one, which is motion enough.
|
||||
if (before && !this.#landing) {
|
||||
this.#animateRows(strip, before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills every copied group now that the strip is in the document. A group
|
||||
* nested inside another is connected as soon as its parent takes it, so
|
||||
* this list drains in order.
|
||||
*/
|
||||
#fillGroups() {
|
||||
while (this.#pendingGroups.length) {
|
||||
const { copy, inner } = this.#pendingGroups.shift();
|
||||
for (const child of inner.children) {
|
||||
if (!child.classList.contains("zen-tab-group-start")) {
|
||||
this.#appendCopy(copy, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-left of every row in a strip, keyed by the copy's stable id, so a
|
||||
* row can be matched across a rebuild.
|
||||
*
|
||||
* @param {Element} strip
|
||||
* @returns {Map<string, {left: number, top: number}>}
|
||||
*/
|
||||
#rowPositions(strip) {
|
||||
const positions = new Map();
|
||||
for (const row of strip.querySelectorAll(
|
||||
"tab, .tab-group-label-container"
|
||||
)) {
|
||||
const id = row.id || row.closest("[id]")?.id;
|
||||
if (id) {
|
||||
const rect = row.getBoundingClientRect();
|
||||
positions.set(id, { left: rect.left, top: rect.top });
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slides each rebuilt row from where its old copy was to where it is now.
|
||||
*
|
||||
* @param {Element} strip
|
||||
* @param {Map<string, {left: number, top: number}>} before - Old positions
|
||||
*/
|
||||
#animateRows(strip, before) {
|
||||
if (gReduceMotion) {
|
||||
return;
|
||||
}
|
||||
for (const row of strip.querySelectorAll(
|
||||
"tab, .tab-group-label-container"
|
||||
)) {
|
||||
const id = row.id || row.closest("[id]")?.id;
|
||||
const old = id && before.get(id);
|
||||
if (!old) {
|
||||
continue;
|
||||
}
|
||||
const rect = row.getBoundingClientRect();
|
||||
const dx = old.left - rect.left;
|
||||
const dy = old.top - rect.top;
|
||||
if (dx || dy) {
|
||||
gZenUIManager.elementAnimate(
|
||||
row,
|
||||
{ x: [dx, 0], y: [dy, 0] },
|
||||
{ duration: 180, easing: "ease-out" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies must never be mistaken for the originals by id lookups, so every
|
||||
* id in a copy gets a suffix.
|
||||
*
|
||||
* @param {Element} copy - The copied element, before it is connected
|
||||
*/
|
||||
#renameIds(copy) {
|
||||
for (const element of [copy, ...copy.querySelectorAll("[id]")]) {
|
||||
if (element.id) {
|
||||
element.id = `${element.id}-copy`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#appendCopy(container, node) {
|
||||
// A closing tab stays in the strip through its animation, but is gone.
|
||||
if (
|
||||
node.nodeType !== Node.ELEMENT_NODE ||
|
||||
node.hasAttribute("hidden") ||
|
||||
node.closing
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (gBrowser.isTab(node)) {
|
||||
const copy = node.cloneNode(false);
|
||||
this.#renameIds(copy);
|
||||
for (const attribute of [
|
||||
"selected",
|
||||
"visuallyselected",
|
||||
"multiselected",
|
||||
]) {
|
||||
copy.removeAttribute(attribute);
|
||||
}
|
||||
container.appendChild(copy);
|
||||
this.#realElements.set(copy, node);
|
||||
if (node.glanceTab) {
|
||||
this.#appendCopy(copy.querySelector(".tab-content"), node.glanceTab);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (GROUP_TAGS.includes(node.localName)) {
|
||||
const copy = node.cloneNode(false);
|
||||
this.#renameIds(copy);
|
||||
if (this.#collapsedCopies.has(node)) {
|
||||
copy.toggleAttribute("collapsed", this.#collapsedCopies.get(node));
|
||||
}
|
||||
container.appendChild(copy);
|
||||
this.#realElements.set(copy, node);
|
||||
const icon = copy.querySelector(".tab-group-folder-icon");
|
||||
const realIcon = node.querySelector(".tab-group-folder-icon");
|
||||
if (icon && realIcon) {
|
||||
icon.replaceChildren(
|
||||
...[...realIcon.children].map(child => child.cloneNode(true))
|
||||
);
|
||||
}
|
||||
// A group empties itself and builds its own scaffolding the moment it
|
||||
// joins the document, so its rows are put in afterwards.
|
||||
this.#pendingGroups.push({
|
||||
copy,
|
||||
inner: node.querySelector(":scope > .tab-group-container") ?? node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!node.querySelector("tab, tab-group, zen-folder")) {
|
||||
const copy = node.cloneNode(true);
|
||||
this.#renameIds(copy);
|
||||
container.appendChild(copy);
|
||||
return;
|
||||
}
|
||||
const copy = node.cloneNode(false);
|
||||
this.#renameIds(copy);
|
||||
container.appendChild(copy);
|
||||
for (const child of node.children) {
|
||||
this.#appendCopy(copy, child);
|
||||
}
|
||||
}
|
||||
|
||||
#containEvent = event => {
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
#containHover = {
|
||||
handleEvent: event => event.stopPropagation(),
|
||||
capture: true,
|
||||
};
|
||||
|
||||
#onStripClick = event => {
|
||||
const label = event.target.closest(".tab-group-label-container");
|
||||
if (label) {
|
||||
const copy = label.closest(GROUP_TAGS.join());
|
||||
const group = this.#realElements.get(copy);
|
||||
if (group) {
|
||||
event.stopPropagation();
|
||||
this.#collapsedCopies.set(group, copy.collapsed);
|
||||
if (copy.collapsed) {
|
||||
gZenFolders.animateCollapse(copy);
|
||||
} else {
|
||||
gZenFolders.animateExpand(copy);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
const tab = this.#realElements.get(event.target.closest("tab"));
|
||||
if (!tab) {
|
||||
return;
|
||||
}
|
||||
event.stopPropagation();
|
||||
if (event.target.closest(".tab-close-button")) {
|
||||
gBrowser.removeTab(tab, { animate: true });
|
||||
return;
|
||||
}
|
||||
gBrowser.selectedTab = tab;
|
||||
this.library?.constructor.toggle();
|
||||
};
|
||||
|
||||
/**
|
||||
* A copy carries the real element's context menu, which would then act on
|
||||
* the copy. The menu opens for the real tab or folder instead.
|
||||
*
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
#onStripContextMenu = event => {
|
||||
let real = null;
|
||||
const label = event.target.closest(".tab-group-label-container");
|
||||
if (label) {
|
||||
const group = this.#realElements.get(label.closest(GROUP_TAGS.join()));
|
||||
real = group?.labelElement?.parentElement;
|
||||
} else {
|
||||
real = this.#realElements.get(event.target.closest("tab"));
|
||||
}
|
||||
const menu = real && document.getElementById(real.getAttribute("context"));
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (!menu) {
|
||||
return;
|
||||
}
|
||||
const trigger = new MouseEvent("ZenLibraryContextMenu", {
|
||||
screenX: event.screenX,
|
||||
screenY: event.screenY,
|
||||
});
|
||||
real.dispatchEvent(trigger);
|
||||
menu.openPopupAtScreen(event.screenX, event.screenY, true, trigger);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Element} copy - A copied tab, group or folder
|
||||
* @returns {Element|undefined} The real element it stands for
|
||||
*/
|
||||
realElementFor(copy) {
|
||||
return this.#realElements.get(copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds every card's copies immediately, so a just-moved tab's new copy
|
||||
* exists to read or animate.
|
||||
*/
|
||||
refreshStripsNow() {
|
||||
this.#fillStrips(true);
|
||||
}
|
||||
|
||||
beginTabLanding(tab) {
|
||||
this.#landing = true;
|
||||
this.refreshStripsNow();
|
||||
const copy = this.copyForTab(tab);
|
||||
if (!copy?.isConnected) {
|
||||
return null;
|
||||
}
|
||||
const rect = copy.getBoundingClientRect();
|
||||
return rect.width && rect.height ? copy : null;
|
||||
}
|
||||
|
||||
copyForTab(tab) {
|
||||
for (const card of this.#cards) {
|
||||
for (const copy of card.querySelectorAll(".zen-library-space-tabs tab")) {
|
||||
if (this.#realElements.get(copy) === tab) {
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#onStripDragStart = event => {
|
||||
const target =
|
||||
event.target.nodeType === Node.TEXT_NODE
|
||||
? event.target.parentElement
|
||||
: event.target;
|
||||
const copy = target.closest("tab");
|
||||
if (copy) {
|
||||
this.#dnd.startTabDrag(event, copy);
|
||||
copy.addEventListener("dragend", this.#onCopyDragEnd, { once: true });
|
||||
}
|
||||
};
|
||||
|
||||
#onStripDrop = event => {
|
||||
this.#dnd.handle_drop(event);
|
||||
this.#refreshStrips();
|
||||
};
|
||||
|
||||
#renderStrip() {
|
||||
return html`
|
||||
<div class="zen-library-space-body" zen-sidebar-expanded="true">
|
||||
<div
|
||||
class="zen-library-space-tabs"
|
||||
id="tabbrowser-tabs"
|
||||
orient="vertical"
|
||||
expanded="true"
|
||||
@click=${this.#onStripClick}
|
||||
@contextmenu=${this.#onStripContextMenu}
|
||||
@mouseover=${this.#containHover}
|
||||
@mouseout=${this.#containHover}
|
||||
@dragstart=${this.#onStripDragStart}
|
||||
@dragover=${event => this.#dnd.handle_dragover(event)}
|
||||
@dragleave=${event => this.#dnd.handle_dragleave(event)}
|
||||
@drop=${this.#onStripDrop}
|
||||
></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Theme
|
||||
|
||||
#themeStyles(workspace) {
|
||||
const { gradient, isDarkMode, isExplicitMode, toolbarColor, primaryColor } =
|
||||
gZenThemePicker.getGradientForWorkspace(workspace);
|
||||
let colorScheme = "";
|
||||
if (isExplicitMode) {
|
||||
colorScheme = isDarkMode ? "dark" : "light";
|
||||
}
|
||||
return {
|
||||
"--zen-library-space-gradient": gradient,
|
||||
"--zen-primary-color": primaryColor,
|
||||
"--toolbox-textcolor": `rgba(${toolbarColor.join(",")})`,
|
||||
colorScheme,
|
||||
};
|
||||
}
|
||||
|
||||
#openThemePicker(workspace, event) {
|
||||
gZenThemePicker.openThemePickerForWorkspace(
|
||||
workspace,
|
||||
event.currentTarget,
|
||||
event
|
||||
);
|
||||
}
|
||||
|
||||
#openActions(event) {
|
||||
document
|
||||
.getElementById("zenWorkspaceMoreActions")
|
||||
.openPopup(event.currentTarget, "after_end");
|
||||
}
|
||||
|
||||
// Icon and name
|
||||
|
||||
#changeIcon(workspace, event) {
|
||||
gZenEmojiPicker.open(event.currentTarget, {
|
||||
closeOnSelect: false,
|
||||
allowNone: gZenWorkspaces.workspaceHasIcon(workspace),
|
||||
onSelect: async icon => {
|
||||
workspace.icon = icon;
|
||||
await gZenWorkspaces.saveWorkspace(workspace);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
#commitRename(workspace, event) {
|
||||
if (this.renaming !== workspace.uuid) {
|
||||
return;
|
||||
}
|
||||
const newName = event.target.value.trim();
|
||||
this.renaming = null;
|
||||
if (newName && newName !== workspace.name) {
|
||||
workspace.name = newName;
|
||||
gZenWorkspaces.saveWorkspace(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
#onRenameKeyDown(workspace, event) {
|
||||
if (event.key === "Enter") {
|
||||
this.#commitRename(workspace, event);
|
||||
} else if (event.key === "Escape") {
|
||||
this.renaming = null;
|
||||
}
|
||||
}
|
||||
|
||||
#onPointerDown(event) {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const cards = this.#cards;
|
||||
const card = event.currentTarget.closest(".zen-library-space");
|
||||
this.#dragIndex = cards.indexOf(card);
|
||||
this.#dropIndex = this.#dragIndex;
|
||||
this.#dragStartX = event.clientX;
|
||||
this.#lastPointerX = event.clientX;
|
||||
this.#dragStartScroll = this.#list.scrollLeft;
|
||||
const cardRect = window.windowUtils.getBoundsWithoutFlushing(card);
|
||||
this.#dragCenterX = cardRect.left + cardRect.width / 2;
|
||||
this.#slotCenters = cards
|
||||
.filter((other, i) => i !== this.#dragIndex)
|
||||
.map(other => {
|
||||
const rect = window.windowUtils.getBoundsWithoutFlushing(other);
|
||||
return rect.left + rect.width / 2;
|
||||
});
|
||||
card.setAttribute("dragging", "true");
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
event.preventDefault();
|
||||
this.#autoScroll();
|
||||
}
|
||||
|
||||
get #list() {
|
||||
return this.querySelector(".zen-library-spaces");
|
||||
}
|
||||
|
||||
#onPointerMove(event) {
|
||||
if (this.#dragIndex === -1) {
|
||||
return;
|
||||
}
|
||||
this.#lastPointerX = event.clientX;
|
||||
this.#updateDrag();
|
||||
}
|
||||
|
||||
#updateDrag() {
|
||||
const cards = this.#cards;
|
||||
const scrolled = this.#list.scrollLeft - this.#dragStartScroll;
|
||||
const travel = this.#lastPointerX - this.#dragStartX + scrolled;
|
||||
cards[this.#dragIndex].style.translate = `${travel}px 0`;
|
||||
const centerX = this.#dragCenterX + travel;
|
||||
const index = this.#slotCenters.filter(center => centerX > center).length;
|
||||
if (index !== this.#dropIndex) {
|
||||
this.#dropIndex = index;
|
||||
this.#shiftCards(cards);
|
||||
}
|
||||
}
|
||||
|
||||
#autoScroll() {
|
||||
if (this.#dragIndex === -1) {
|
||||
return;
|
||||
}
|
||||
const list = this.#list;
|
||||
const rect = window.windowUtils.getBoundsWithoutFlushing(list);
|
||||
let step = 0;
|
||||
if (this.#lastPointerX < rect.left + SCROLL_EDGE_PX) {
|
||||
step = -SCROLL_STEP_PX;
|
||||
} else if (this.#lastPointerX > rect.right - SCROLL_EDGE_PX) {
|
||||
step = SCROLL_STEP_PX;
|
||||
}
|
||||
if (step) {
|
||||
const before = list.scrollLeft;
|
||||
list.scrollLeft += step;
|
||||
if (list.scrollLeft !== before) {
|
||||
this.#updateDrag();
|
||||
}
|
||||
}
|
||||
this.#scrollFrame = requestAnimationFrame(() => this.#autoScroll());
|
||||
}
|
||||
|
||||
#shiftCards(cards) {
|
||||
cards.forEach((card, i) => {
|
||||
let shift = "";
|
||||
if (this.#dragIndex < i && i <= this.#dropIndex) {
|
||||
shift = "left";
|
||||
} else if (this.#dropIndex <= i && i < this.#dragIndex) {
|
||||
shift = "right";
|
||||
}
|
||||
if (shift) {
|
||||
card.setAttribute("shift", shift);
|
||||
} else {
|
||||
card.removeAttribute("shift");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async #onPointerUp(event, workspace) {
|
||||
if (this.#dragIndex === -1) {
|
||||
return;
|
||||
}
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
cancelAnimationFrame(this.#scrollFrame);
|
||||
this.#scrollFrame = null;
|
||||
const moved =
|
||||
this.#dropIndex !== this.#dragIndex && event.type === "pointerup";
|
||||
const dropIndex = this.#dropIndex;
|
||||
this.#dragIndex = -1;
|
||||
this.#dropIndex = -1;
|
||||
|
||||
const list = this.#list;
|
||||
list.setAttribute("no-transition", "true");
|
||||
if (moved) {
|
||||
gZenWorkspaces.reorderWorkspace(workspace.uuid, dropIndex);
|
||||
await this.updateComplete;
|
||||
}
|
||||
for (const card of this.#cards) {
|
||||
card.removeAttribute("dragging");
|
||||
card.removeAttribute("shift");
|
||||
card.style.translate = "";
|
||||
}
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
list.removeAttribute("no-transition");
|
||||
}
|
||||
|
||||
#renderIcon(workspace) {
|
||||
const hasIcon = gZenWorkspaces.workspaceHasIcon(workspace);
|
||||
let content = "";
|
||||
if (hasIcon) {
|
||||
const icon = gZenWorkspaces.getWorkspaceIcon(workspace);
|
||||
content = icon.endsWith(".svg") ? html`<img src=${icon} alt="" />` : icon;
|
||||
}
|
||||
return html`
|
||||
<button
|
||||
class="zen-library-space-icon"
|
||||
?no-icon=${!hasIcon}
|
||||
data-l10n-id="library-spaces-icon-button"
|
||||
@click=${event => this.#changeIcon(workspace, event)}
|
||||
>
|
||||
${content}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderName(workspace) {
|
||||
if (this.renaming === workspace.uuid) {
|
||||
return html`
|
||||
<input
|
||||
class="zen-library-space-name-input"
|
||||
.value=${workspace.name}
|
||||
@keydown=${event => this.#onRenameKeyDown(workspace, event)}
|
||||
@blur=${event => this.#commitRename(workspace, event)}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<button
|
||||
class="zen-library-space-name"
|
||||
data-l10n-id="library-spaces-name-button"
|
||||
@click=${() => {
|
||||
this.renaming = workspace.uuid;
|
||||
}}
|
||||
>
|
||||
${workspace.name}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderSpace(workspace) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-space zen-squircle-before"
|
||||
data-uuid=${workspace.uuid}
|
||||
style=${styleMap(this.#themeStyles(workspace))}
|
||||
?active=${workspace.uuid === gZenWorkspaces.activeWorkspace}
|
||||
>
|
||||
<div class="zen-library-space-header zen-squircle-before">
|
||||
${this.#renderIcon(workspace)} ${this.#renderName(workspace)}
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1"
|
||||
data-l10n-id="library-spaces-theme-button"
|
||||
@click=${event => this.#openThemePicker(workspace, event)}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://browser/skin/zen-icons/paintbrush-fill.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
</div>
|
||||
${this.#renderStrip()}
|
||||
<div class="zen-library-space-footer">
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1 zen-library-space-handle"
|
||||
data-l10n-id="library-spaces-move-button"
|
||||
@pointerdown=${this.#onPointerDown}
|
||||
@pointermove=${this.#onPointerMove}
|
||||
@pointerup=${event => this.#onPointerUp(event, workspace)}
|
||||
@pointercancel=${event => this.#onPointerUp(event, workspace)}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://browser/skin/zen-icons/drag-indicator.svg"
|
||||
draggable="false"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
<toolbarbutton
|
||||
class="toolbarbutton-1 zen-library-space-actions"
|
||||
zen-workspace-id=${workspace.uuid}
|
||||
data-l10n-id="library-spaces-actions-button"
|
||||
@click=${this.#openActions}
|
||||
>
|
||||
<img
|
||||
class="toolbarbutton-icon"
|
||||
src="chrome://global/skin/icons/more.svg"
|
||||
alt=""
|
||||
/>
|
||||
</toolbarbutton>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="zen-library-spaces">
|
||||
${repeat(
|
||||
gZenWorkspaces.getWorkspaces(),
|
||||
workspace => workspace.uuid,
|
||||
workspace => this.#renderSpace(workspace)
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("zen-library-spaces-section", ZenLibrarySpacesSection);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ DIRS += [
|
||||
"compact-mode",
|
||||
"drag-and-drop",
|
||||
"glance",
|
||||
"library",
|
||||
"live-folders",
|
||||
"mods",
|
||||
"tests",
|
||||
|
||||
@@ -383,12 +383,6 @@ class nsZenWindowSync {
|
||||
|
||||
handleEvent(aEvent) {
|
||||
const window = aEvent.currentTarget.documentGlobal ?? aEvent.currentTarget;
|
||||
// The library builds copies of tabs and folders that fire these same
|
||||
// events while being built. Their ids are suffixed with "-copy" (see
|
||||
// ZenLibrarySpacesSection#renameIds), so never sync anything from one.
|
||||
if (aEvent.target?.id?.endsWith("-copy")) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!window.gZenStartup.isReady ||
|
||||
!window.gZenWorkspaces?.shouldHaveWorkspaces ||
|
||||
|
||||
@@ -1542,21 +1542,14 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
browser.gZenThemePicker.invalidateGradientCache(uuid);
|
||||
}
|
||||
|
||||
// Only rebuild for the workspace the window shows, or the one its
|
||||
// picker is editing without showing it.
|
||||
// Do not rebuild if the workspace is not the same as the current one
|
||||
const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace();
|
||||
const appliesToWindow = windowWorkspace.uuid === uuid;
|
||||
if (
|
||||
!appliesToWindow &&
|
||||
browser.gZenThemePicker.editingWorkspaceId !== uuid
|
||||
) {
|
||||
if (windowWorkspace.uuid !== uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get the theme from the window
|
||||
workspaceTheme = this.fixTheme(
|
||||
theme || (appliesToWindow ? windowWorkspace : workspace).theme
|
||||
);
|
||||
workspaceTheme = this.fixTheme(theme || windowWorkspace.theme);
|
||||
const docElement = browser.document.documentElement;
|
||||
|
||||
if (!skipUpdate) {
|
||||
@@ -1567,7 +1560,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
}
|
||||
}
|
||||
|
||||
if (theme && appliesToWindow) {
|
||||
if (theme) {
|
||||
const workspaceElement = browser.gZenWorkspaces.workspaceElement(
|
||||
windowWorkspace.uuid
|
||||
);
|
||||
@@ -1576,7 +1569,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipUpdate && appliesToWindow) {
|
||||
if (!skipUpdate) {
|
||||
let backgroundElement =
|
||||
browser.gZenThemePicker.browserBackgroundElement;
|
||||
let toolbarElement = browser.gZenThemePicker.toolbarBackgroundElement;
|
||||
@@ -1731,9 +1724,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
workspaceTheme.gradientColors,
|
||||
true
|
||||
);
|
||||
if (appliesToWindow) {
|
||||
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
|
||||
}
|
||||
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
|
||||
|
||||
browser.gZenThemePicker.customColorList.innerHTML = "";
|
||||
for (const dot of workspaceTheme.gradientColors) {
|
||||
@@ -1742,66 +1733,59 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
}
|
||||
}
|
||||
|
||||
if (appliesToWindow) {
|
||||
browser.gZenThemePicker.toolbarBackgroundElement.style.setProperty(
|
||||
"--zen-main-browser-background-toolbar",
|
||||
gradientToolbar
|
||||
);
|
||||
browser.gZenThemePicker.browserBackgroundElement.style.setProperty(
|
||||
"--zen-main-browser-background",
|
||||
gradient
|
||||
);
|
||||
const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
|
||||
if (isDefaultTheme) {
|
||||
docElement.setAttribute("zen-default-theme", "true");
|
||||
browser.gZenThemePicker.toolbarBackgroundElement.style.setProperty(
|
||||
"--zen-main-browser-background-toolbar",
|
||||
gradientToolbar
|
||||
);
|
||||
browser.gZenThemePicker.browserBackgroundElement.style.setProperty(
|
||||
"--zen-main-browser-background",
|
||||
gradient
|
||||
);
|
||||
const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
|
||||
if (isDefaultTheme) {
|
||||
docElement.setAttribute("zen-default-theme", "true");
|
||||
} else {
|
||||
docElement.removeAttribute("zen-default-theme");
|
||||
}
|
||||
if (dominantColor) {
|
||||
// Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
|
||||
// so instead get the pref value directly.
|
||||
browser.gZenThemePicker.isLegacyVersion =
|
||||
Services.prefs.getIntPref("zen.theme.gradient-legacy-version", 1) ===
|
||||
0;
|
||||
|
||||
let isDarkMode = isDarkModeWindow;
|
||||
if (!isDefaultTheme && !this.isLegacyVersion) {
|
||||
// Check for the primary color
|
||||
isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
|
||||
docElement.setAttribute("zen-should-be-dark-mode", isDarkMode);
|
||||
browser.gZenThemePicker.panel.removeAttribute("invalidate-controls");
|
||||
} else {
|
||||
docElement.removeAttribute("zen-default-theme");
|
||||
}
|
||||
if (dominantColor) {
|
||||
// Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
|
||||
// so instead get the pref value directly.
|
||||
browser.gZenThemePicker.isLegacyVersion =
|
||||
Services.prefs.getIntPref(
|
||||
"zen.theme.gradient-legacy-version",
|
||||
1
|
||||
) === 0;
|
||||
|
||||
let isDarkMode = isDarkModeWindow;
|
||||
if (!isDefaultTheme && !this.isLegacyVersion) {
|
||||
// Check for the primary color
|
||||
isDarkMode =
|
||||
browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
|
||||
docElement.setAttribute("zen-should-be-dark-mode", isDarkMode);
|
||||
browser.gZenThemePicker.panel.removeAttribute(
|
||||
"invalidate-controls"
|
||||
docElement.removeAttribute("zen-should-be-dark-mode");
|
||||
if (!this.isLegacyVersion) {
|
||||
browser.gZenThemePicker.panel.setAttribute(
|
||||
"invalidate-controls",
|
||||
"true"
|
||||
);
|
||||
} else {
|
||||
docElement.removeAttribute("zen-should-be-dark-mode");
|
||||
if (!this.isLegacyVersion) {
|
||||
browser.gZenThemePicker.panel.setAttribute(
|
||||
"invalidate-controls",
|
||||
"true"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const primaryColor = this.getAccentColorForUI(
|
||||
dominantColor,
|
||||
isDarkMode
|
||||
);
|
||||
docElement.style.setProperty("--zen-primary-color", primaryColor);
|
||||
|
||||
// Set `--toolbox-textcolor` to have a contrast with the primary color
|
||||
let textColor = this.getToolbarColor(isDarkMode, dominantColor);
|
||||
docElement.style.setProperty(
|
||||
"--toolbox-textcolor",
|
||||
`rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
|
||||
);
|
||||
docElement.style.setProperty(
|
||||
"--toolbar-color-scheme",
|
||||
isDarkMode ? "dark" : "light"
|
||||
);
|
||||
}
|
||||
|
||||
const primaryColor = this.getAccentColorForUI(
|
||||
dominantColor,
|
||||
isDarkMode
|
||||
);
|
||||
docElement.style.setProperty("--zen-primary-color", primaryColor);
|
||||
|
||||
// Set `--toolbox-textcolor` to have a contrast with the primary color
|
||||
let textColor = this.getToolbarColor(isDarkMode, dominantColor);
|
||||
docElement.style.setProperty(
|
||||
"--toolbox-textcolor",
|
||||
`rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
|
||||
);
|
||||
docElement.style.setProperty(
|
||||
"--toolbar-color-scheme",
|
||||
isDarkMode ? "dark" : "light"
|
||||
);
|
||||
}
|
||||
|
||||
if (!skipUpdate) {
|
||||
@@ -1894,36 +1878,6 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
}
|
||||
}
|
||||
|
||||
#editingWorkspaceId = null;
|
||||
|
||||
get editingWorkspaceId() {
|
||||
return this.#editingWorkspaceId;
|
||||
}
|
||||
|
||||
get workspaceBeingEdited() {
|
||||
return (
|
||||
gZenWorkspaces.getWorkspaceFromId(this.#editingWorkspaceId) ??
|
||||
gZenWorkspaces.getActiveWorkspace()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the picker for a space without switching to it.
|
||||
*
|
||||
* @param {object} workspace - The space to edit
|
||||
* @param {Element} anchor - Element to anchor the panel to
|
||||
* @param {Event} event - The triggering event
|
||||
*/
|
||||
openThemePickerForWorkspace(workspace, anchor, event) {
|
||||
this.#editingWorkspaceId = workspace.uuid;
|
||||
this.onWorkspaceChange(workspace);
|
||||
this.panel.removeAttribute("hidepopovertail");
|
||||
PanelMultiView.openPopup(this.panel, anchor, {
|
||||
position: "bottomleft topleft",
|
||||
triggerEvent: event,
|
||||
});
|
||||
}
|
||||
|
||||
updateCurrentWorkspace(skipSave = true) {
|
||||
this.updated = skipSave;
|
||||
const dots = this.panel.querySelectorAll(".zen-theme-picker-dot");
|
||||
@@ -1962,7 +1916,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
this.currentOpacity,
|
||||
this.currentTexture
|
||||
);
|
||||
let currentWorkspace = this.workspaceBeingEdited;
|
||||
let currentWorkspace = gZenWorkspaces.getActiveWorkspace();
|
||||
|
||||
currentWorkspace.theme = gradient;
|
||||
if (!skipSave) {
|
||||
@@ -1974,10 +1928,6 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
skipSave,
|
||||
skipSave ? gradient : null
|
||||
);
|
||||
if (currentWorkspace.uuid !== gZenWorkspaces.activeWorkspace) {
|
||||
this.invalidateGradientCache(currentWorkspace.uuid);
|
||||
Services.obs.notifyObservers(null, "zen-space-gradient-update");
|
||||
}
|
||||
}
|
||||
|
||||
handlePanelClose() {
|
||||
@@ -1985,11 +1935,6 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
||||
this.updateCurrentWorkspace(false);
|
||||
}
|
||||
this.uninitThemePicker();
|
||||
if (this.#editingWorkspaceId) {
|
||||
this.#editingWorkspaceId = null;
|
||||
this.panel.setAttribute("hidepopovertail", "true");
|
||||
this.onWorkspaceChange(gZenWorkspaces.getActiveWorkspace());
|
||||
}
|
||||
}
|
||||
|
||||
handlePanelOpen() {
|
||||
|
||||
@@ -3314,7 +3314,7 @@ class nsZenWorkspaces {
|
||||
parent.removeAttribute("icons-overflow");
|
||||
return;
|
||||
}
|
||||
const maxButtonSize = AppConstants.platform == "macosx" ? 34 : 32; // IMPORTANT: This should match the CSS size of the icons
|
||||
const maxButtonSize = 32; // IMPORTANT: This should match the CSS size of the icons
|
||||
const minButtonSize = maxButtonSize / 2; // Minimum size for icons when space is limited
|
||||
const separation = 3; // Space between icons
|
||||
|
||||
|
||||
@@ -12,23 +12,15 @@ ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
|
||||
return document.getElementById("zen-toolbar-background");
|
||||
});
|
||||
|
||||
ChromeUtils.defineESModuleGetters(
|
||||
lazy,
|
||||
{ ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs" },
|
||||
{ global: "current" }
|
||||
);
|
||||
|
||||
export class ZenSpacesSwipe {
|
||||
_swipeState = {
|
||||
isGestureActive: false,
|
||||
lastDelta: 0,
|
||||
direction: null,
|
||||
isSwipingLibrary: false,
|
||||
beforeLibraryState: 0,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.attachWorkspaceSwipeGestures(gNavToolbox);
|
||||
this.#attachWorkspaceSwipeGestures(gNavToolbox);
|
||||
this._popupOpenHandler = this._popupOpenHandler.bind(this);
|
||||
}
|
||||
|
||||
@@ -43,45 +35,20 @@ export class ZenSpacesSwipe {
|
||||
);
|
||||
}
|
||||
|
||||
#readySwipeLibrary = null;
|
||||
#readySwipeOpenLibrary() {
|
||||
if (this.#readySwipeLibrary) {
|
||||
return this.#readySwipeLibrary;
|
||||
}
|
||||
|
||||
const spaces = gZenWorkspaces.getWorkspaces();
|
||||
const current = gZenWorkspaces.getActiveWorkspaceFromCache();
|
||||
const libraryEnabled = Services.prefs.getBoolPref("zen.library.enabled");
|
||||
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
|
||||
|
||||
this.#readySwipeLibrary =
|
||||
spaces.indexOf(current) === (libraryOnRight ? spaces.length - 1 : 0) &&
|
||||
libraryEnabled;
|
||||
return this.#readySwipeLibrary;
|
||||
}
|
||||
|
||||
attachWorkspaceSwipeGestures(element) {
|
||||
const gestureControl = {
|
||||
_handleSwipeMayStart: this._handleSwipeMayStart.bind(this),
|
||||
_handleSwipeStart: this._handleSwipeStart.bind(this),
|
||||
_handleSwipeUpdate: this._handleSwipeUpdate.bind(this),
|
||||
_handleSwipeEnd: this._handleSwipeEnd.bind(this),
|
||||
_handleSwipeAnimationEnd: this.onSwipeGestureAnimationEnd.bind(this),
|
||||
};
|
||||
|
||||
#attachWorkspaceSwipeGestures(element) {
|
||||
element.addEventListener(
|
||||
"MozSwipeGestureMayStart",
|
||||
gestureControl._handleSwipeMayStart,
|
||||
this._handleSwipeMayStart.bind(this),
|
||||
true
|
||||
);
|
||||
element.addEventListener(
|
||||
"MozSwipeGestureStart",
|
||||
gestureControl._handleSwipeStart,
|
||||
this._handleSwipeStart.bind(this),
|
||||
true
|
||||
);
|
||||
element.addEventListener(
|
||||
"MozSwipeGestureUpdate",
|
||||
gestureControl._handleSwipeUpdate,
|
||||
this._handleSwipeUpdate.bind(this),
|
||||
true
|
||||
);
|
||||
|
||||
@@ -89,47 +56,15 @@ export class ZenSpacesSwipe {
|
||||
// while MozSwipeGesture is fired immediately after swipe ends.
|
||||
element.addEventListener(
|
||||
"MozSwipeGesture",
|
||||
gestureControl._handleSwipeEnd,
|
||||
this._handleSwipeEnd.bind(this),
|
||||
true
|
||||
);
|
||||
|
||||
element.addEventListener(
|
||||
"MozSwipeGestureEnd",
|
||||
gestureControl._handleSwipeAnimationEnd,
|
||||
true
|
||||
);
|
||||
|
||||
return gestureControl;
|
||||
}
|
||||
|
||||
detachWorkspaceSwipeGestures(element, gestureControl) {
|
||||
element.removeEventListener(
|
||||
"MozSwipeGestureMayStart",
|
||||
gestureControl._handleSwipeMayStart,
|
||||
true
|
||||
);
|
||||
element.removeEventListener(
|
||||
"MozSwipeGestureStart",
|
||||
gestureControl._handleSwipeStart,
|
||||
true
|
||||
);
|
||||
element.removeEventListener(
|
||||
"MozSwipeGestureUpdate",
|
||||
gestureControl._handleSwipeUpdate,
|
||||
true
|
||||
);
|
||||
|
||||
// Use MozSwipeGesture instead of MozSwipeGestureEnd because MozSwipeGestureEnd is fired after animation ends,
|
||||
// while MozSwipeGesture is fired immediately after swipe ends.
|
||||
element.removeEventListener(
|
||||
"MozSwipeGesture",
|
||||
gestureControl._handleSwipeEnd,
|
||||
true
|
||||
);
|
||||
|
||||
element.removeEventListener(
|
||||
"MozSwipeGestureEnd",
|
||||
gestureControl._handleSwipeAnimationEnd,
|
||||
() => {
|
||||
this.onSwipeGestureAnimationEnd();
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
@@ -160,19 +95,6 @@ export class ZenSpacesSwipe {
|
||||
}
|
||||
}
|
||||
|
||||
#toggleSwipeGestureAttr(enable) {
|
||||
const elements = [
|
||||
"zen-workspace",
|
||||
"#tabbrowser-arrowscrollbox",
|
||||
".zen-browser-grain",
|
||||
];
|
||||
elements.forEach(el =>
|
||||
document
|
||||
.querySelectorAll(el)
|
||||
.forEach(node => node?.toggleAttribute("swipe-gesture", enable))
|
||||
);
|
||||
}
|
||||
|
||||
_handleSwipeStart(event) {
|
||||
const ws = gZenWorkspaces;
|
||||
|
||||
@@ -182,7 +104,7 @@ export class ZenSpacesSwipe {
|
||||
|
||||
gZenFolders.cancelPopupTimer();
|
||||
|
||||
this.#toggleSwipeGestureAttr(true);
|
||||
document.documentElement.setAttribute("swipe-gesture", "true");
|
||||
document.addEventListener("popupshown", this._popupOpenHandler, {
|
||||
once: true,
|
||||
});
|
||||
@@ -193,8 +115,6 @@ export class ZenSpacesSwipe {
|
||||
isGestureActive: true,
|
||||
lastDelta: 0,
|
||||
direction: null,
|
||||
isSwipingLibrary: false,
|
||||
beforeLibraryState: 0,
|
||||
};
|
||||
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", true);
|
||||
}
|
||||
@@ -234,55 +154,6 @@ export class ZenSpacesSwipe {
|
||||
this._swipeState.direction = delta > 0 ? "left" : "right";
|
||||
}
|
||||
|
||||
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
|
||||
const libraryOpen = lazy.ZenLibrary.isLibraryOpen;
|
||||
const couldClose = libraryOpen;
|
||||
const wantsOpen =
|
||||
!libraryOpen &&
|
||||
(libraryOnRight ? translateX < 0 : translateX > 0) &&
|
||||
this.#readySwipeOpenLibrary();
|
||||
|
||||
if (wantsOpen || couldClose || this._swipeState.isSwipingLibrary) {
|
||||
if (!this._swipeState.isSwipingLibrary) {
|
||||
this._swipeState.isSwipingLibrary = true;
|
||||
this._swipeState.beforeLibraryState = libraryOpen ? 1 : 0;
|
||||
lazy.ZenLibrary.startSwipe();
|
||||
}
|
||||
|
||||
const rubberBand = function (offset, dimension, constant = 0.55) {
|
||||
if (offset === 0 || dimension === 0) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
dimension *
|
||||
(1 - Math.exp(-(Math.abs(offset) * constant) / dimension)) *
|
||||
Math.sign(offset)
|
||||
);
|
||||
};
|
||||
|
||||
const DAMPING_DIMENSION = 0.2;
|
||||
const RUBBER_BAND_CONSTANT = 0.08;
|
||||
|
||||
const LIBRARY_SWIPE_FULL = 0.8;
|
||||
const translation = libraryOnRight ? -translateX : translateX;
|
||||
const deltaProgress = (translation / stripWidth) * LIBRARY_SWIPE_FULL;
|
||||
const progress = this._swipeState.beforeLibraryState + deltaProgress;
|
||||
|
||||
let progressDamped;
|
||||
if (progress < 0) {
|
||||
progressDamped =
|
||||
0 + rubberBand(progress, DAMPING_DIMENSION, RUBBER_BAND_CONSTANT);
|
||||
} else if (progress > 1) {
|
||||
progressDamped =
|
||||
1 + rubberBand(progress - 1, DAMPING_DIMENSION, RUBBER_BAND_CONSTANT);
|
||||
} else {
|
||||
progressDamped = progress;
|
||||
}
|
||||
|
||||
lazy.ZenLibrary.swipeProgress(progressDamped);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply a translateX to the tab strip to give the user feedback on the swipe
|
||||
const currentWorkspace = ws.getActiveWorkspaceFromCache();
|
||||
ws._organizeWorkspaceStripLocations(currentWorkspace, true, translateX);
|
||||
@@ -302,35 +173,21 @@ export class ZenSpacesSwipe {
|
||||
|
||||
const rawDirection = moveForward ? 1 : -1;
|
||||
const direction = ws.naturalScroll ? -1 : 1;
|
||||
|
||||
if (this._swipeState.isSwipingLibrary) {
|
||||
lazy.ZenLibrary.stopSwipe(rawDirection * direction);
|
||||
this.#readySwipeLibrary = null;
|
||||
return;
|
||||
}
|
||||
|
||||
await ws.changeWorkspaceShortcut(rawDirection * direction, true);
|
||||
}
|
||||
|
||||
onSwipeGestureAnimationEnd() {
|
||||
const ws = gZenWorkspaces;
|
||||
|
||||
if (this._swipeState.isSwipingLibrary) {
|
||||
lazy.ZenLibrary.stopSwipe(null);
|
||||
this.#readySwipeLibrary = null;
|
||||
}
|
||||
|
||||
// Reset swipe state
|
||||
this._swipeState = {
|
||||
isGestureActive: false,
|
||||
lastDelta: 0,
|
||||
direction: null,
|
||||
isSwipingLibrary: false,
|
||||
beforeLibraryState: 0,
|
||||
};
|
||||
|
||||
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);
|
||||
this.#toggleSwipeGestureAttr(false);
|
||||
document.documentElement.removeAttribute("swipe-gesture");
|
||||
gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
|
||||
[lazy.browserBackgroundElement, lazy.toolbarBackgroundElement].forEach(
|
||||
element => {
|
||||
|
||||
@@ -43,30 +43,20 @@
|
||||
fill: currentColor;
|
||||
scroll-margin: 0 20px;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
height: 32px;
|
||||
max-width: 32px;
|
||||
}
|
||||
|
||||
& .zen-workspace-icon {
|
||||
pointer-events: none;
|
||||
line-height: 0;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:is(img) {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
font-size: 15px;
|
||||
|
||||
&:is(img) {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&[no-icon='true'] {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -366,7 +356,7 @@ zen-workspace {
|
||||
overscroll-behavior: auto;
|
||||
}
|
||||
|
||||
&[swipe-gesture]::part(scrollbox) {
|
||||
:root[swipe-gesture] &::part(scrollbox) {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@@ -414,7 +404,7 @@ zen-workspace {
|
||||
}
|
||||
}
|
||||
|
||||
&:not([swipe-gesture]) :root:not(:is([animating-background])) #navigator-toolbox:not([movingtab]) &:not([active]) {
|
||||
:root:not(:is([animating-background], [swipe-gesture])) #navigator-toolbox:not([movingtab]) &:not([active]) {
|
||||
-moz-subtree-hidden-only-visually: 1;
|
||||
content-visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -1425,15 +1425,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
* use -1 to avoid selecting any tab.
|
||||
* @param {object} options - Additional options.
|
||||
* @param {string|null} options.groupFetchId - An optional group fetch ID.
|
||||
* @param {boolean} options.activate - Whether to select the split after creating it.
|
||||
* @returns {object|undefined} The split view data or undefined if the split was not performed.
|
||||
*/
|
||||
splitTabs(
|
||||
tabs,
|
||||
gridType,
|
||||
initialIndex = 0,
|
||||
{ groupFetchId = null, activate = true } = {}
|
||||
) {
|
||||
splitTabs(tabs, gridType, initialIndex = 0, { groupFetchId = null } = {}) {
|
||||
const tabIndexToUse = Math.max(0, initialIndex);
|
||||
return this.#withoutSplitViewTransition(() => {
|
||||
// TODO: Add support for splitting essential tabs
|
||||
@@ -1449,7 +1443,6 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
|
||||
const existingSplitTab = tabs.find(tab => tab.splitView);
|
||||
let shouldActivateSplit =
|
||||
activate &&
|
||||
(initialIndex >= 0 || tabs.includes(window.gBrowser.selectedTab)) &&
|
||||
!this._sessionRestoring;
|
||||
|
||||
|
||||
@@ -474,17 +474,14 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
|
||||
// Remove everything except the entry we want to keep
|
||||
let url;
|
||||
try {
|
||||
url = Services.io.newURI(initialState.entry.url);
|
||||
} catch {}
|
||||
state.entries = [
|
||||
{
|
||||
...initialState.entry,
|
||||
triggeringPrincipal_base64: E10SUtils.serializePrincipal(
|
||||
url
|
||||
? Services.scriptSecurityManager.createContentPrincipal(url, {})
|
||||
: Services.scriptSecurityManager.createNullPrincipal()
|
||||
Services.scriptSecurityManager.createContentPrincipal(
|
||||
Services.io.newURI(initialState.entry.url),
|
||||
{}
|
||||
)
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -83,8 +83,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#browser,
|
||||
zen-library {
|
||||
#browser {
|
||||
--zen-min-toolbox-padding: 5px;
|
||||
@media (-moz-platform: macos) {
|
||||
--zen-min-toolbox-padding: 6px;
|
||||
@@ -230,8 +229,7 @@ zen-library {
|
||||
/* ==========================================================================
|
||||
Navigator Toolbox (Main Sidebar Container) Base Styles
|
||||
========================================================================== */
|
||||
#navigator-toolbox,
|
||||
.zen-library-space-body {
|
||||
#navigator-toolbox {
|
||||
/* Define theme variables, including platform specifics for native look */
|
||||
--zen-toolbox-min-width: 1px;
|
||||
--border-radius-medium: 14px;
|
||||
@@ -454,15 +452,7 @@ zen-library {
|
||||
}
|
||||
}
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
#zen-sidebar-foot-buttons,
|
||||
#zen-library-footer {
|
||||
--zen-toolbar-button-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-sidebar-foot-buttons {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
@@ -470,10 +460,6 @@ zen-library {
|
||||
& > toolbarbutton:not(#zen-workspaces-button) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:root[zen-right-side="true"][zen-sidebar-expanded="true"] & {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
#tabbrowser-arrowscrollbox {
|
||||
@@ -522,7 +508,7 @@ zen-library {
|
||||
}
|
||||
}
|
||||
|
||||
:is(#navigator-toolbox, .zen-library-space-body)[zen-sidebar-expanded="true"] {
|
||||
#navigator-toolbox[zen-sidebar-expanded="true"] {
|
||||
--zen-toolbox-min-width: fit-content;
|
||||
|
||||
padding: var(--zen-toolbox-padding);
|
||||
@@ -578,6 +564,7 @@ zen-library {
|
||||
|
||||
& #zen-sidebar-foot-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@@ -890,10 +877,6 @@ zen-library {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#zen-sidebar-splitter[zen-library-open="true"] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#zen-sidebar-splitter {
|
||||
opacity: 0;
|
||||
max-width: var(--zen-toolbox-padding) !important;
|
||||
|
||||
@@ -35,7 +35,7 @@ const globalActionsTemplate = [
|
||||
{
|
||||
l10nId: "zen-action-open-theme-picker",
|
||||
command: "cmd_zenOpenZenThemePicker",
|
||||
icon: "chrome://browser/skin/zen-icons/paintbrush-fill.svg",
|
||||
icon: "chrome://browser/skin/zen-icons/edit-theme.svg",
|
||||
},
|
||||
{
|
||||
l10nId: "zen-action-new-split-view",
|
||||
|
||||
Reference in New Issue
Block a user