mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-18 18:54:43 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8645a5b9c8 | ||
|
|
031b5d8d0a | ||
|
|
2804ba8350 | ||
|
|
5e5926e708 | ||
|
|
a65f96c3b8 | ||
|
|
4b4a659c7b | ||
|
|
5ff06e4274 | ||
|
|
f31f99e507 | ||
|
|
2ee1275f28 | ||
|
|
45b9cfe37f | ||
|
|
f800d550cb |
@@ -39,3 +39,4 @@ zen-action-switch-to-dark-mode = Switch to Dark Mode
|
|||||||
zen-action-print = Print
|
zen-action-print = Print
|
||||||
zen-action-focus-on = Focus on
|
zen-action-focus-on = Focus on
|
||||||
zen-action-extension = Extension
|
zen-action-extension = Extension
|
||||||
|
zen-action-open = Open
|
||||||
|
|||||||
90
locales/en-US/browser/browser/zen-library.ftl
Normal file
90
locales/en-US/browser/browser/zen-library.ftl
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 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-history-forget-button =
|
||||||
|
.title = Remove from history
|
||||||
|
library-history-reopen-button =
|
||||||
|
.title = Reopen page
|
||||||
|
|
||||||
|
library-downloads-empty = No downloads found
|
||||||
|
library-downloads-more-button =
|
||||||
|
.title = More options
|
||||||
|
library-downloads-menu-open =
|
||||||
|
.label = Open { $name }
|
||||||
|
library-downloads-menu-copy =
|
||||||
|
.label = Copy { $name }
|
||||||
|
library-downloads-menu-hide =
|
||||||
|
.label = Hide from Zen
|
||||||
|
library-downloads-menu-trash =
|
||||||
|
.label = Move to Trash
|
||||||
|
library-downloads-opening-in = { PLATFORM() ->
|
||||||
|
[macos] Opening in Finder…
|
||||||
|
[windows] Opening in File Explorer…
|
||||||
|
*[other] Opening in file manager…
|
||||||
|
}
|
||||||
|
library-downloads-cancel-button =
|
||||||
|
.title = Cancel download
|
||||||
|
library-downloads-filter-title = Filter Downloads…
|
||||||
|
library-downloads-filter-type = File type
|
||||||
|
library-downloads-filter-when = When was it downloaded?
|
||||||
|
library-downloads-type-images = Images
|
||||||
|
library-downloads-type-video = Video
|
||||||
|
library-downloads-type-audio = Audio
|
||||||
|
library-downloads-type-documents = Documents
|
||||||
|
library-downloads-type-archives = Archives
|
||||||
|
library-downloads-type-apps = Apps
|
||||||
|
library-downloads-retry-button =
|
||||||
|
.title = Retry download
|
||||||
|
|
||||||
|
library-spaces-theme-button =
|
||||||
|
.title = Change theme
|
||||||
|
library-spaces-move-button =
|
||||||
|
.title = Drag to reorder
|
||||||
|
library-spaces-actions-button =
|
||||||
|
.tooltiptext = More options
|
||||||
|
library-spaces-icon-button =
|
||||||
|
.title = Change icon
|
||||||
|
library-spaces-name-button =
|
||||||
|
.title = Rename space
|
||||||
|
|
||||||
|
library-boosts-search-placeholder =
|
||||||
|
.placeholder = Search Boosts…
|
||||||
|
library-boosts-empty = No boosts yet
|
||||||
|
library-boosts-toggle =
|
||||||
|
.aria-label = Enable boost
|
||||||
|
library-boosts-menu-edit =
|
||||||
|
.label = Edit Boost
|
||||||
9
prefs/zen/library.yaml
Normal file
9
prefs/zen/library.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
- name: zen.library.enabled
|
||||||
|
value: "@IS_TWILIGHT@"
|
||||||
|
|
||||||
|
- name: zen.library.last-tab
|
||||||
|
value: "history"
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/.stylelintignore b/.stylelintignore
|
diff --git a/.stylelintignore b/.stylelintignore
|
||||||
index 185490999507b8a5032977237af50f5e61c71df1..77c1635d90c6480af4ebd3c6e3ec49780eaca571 100644
|
index 185490999507b8a5032977237af50f5e61c71df1..4b1b1dc76d316f38f60c90e6cbd74d9569800d04 100644
|
||||||
--- a/.stylelintignore
|
--- a/.stylelintignore
|
||||||
+++ b/.stylelintignore
|
+++ b/.stylelintignore
|
||||||
@@ -106,3 +106,19 @@ build/pgo/blueprint/**/*.css
|
@@ -106,3 +106,20 @@ build/pgo/blueprint/**/*.css
|
||||||
# under our control or we don't want to modify at this point:
|
# under our control or we don't want to modify at this point:
|
||||||
testing/web-platform/mozilla/
|
testing/web-platform/mozilla/
|
||||||
testing/web-platform/tests/
|
testing/web-platform/tests/
|
||||||
@@ -20,5 +20,6 @@ index 185490999507b8a5032977237af50f5e61c71df1..77c1635d90c6480af4ebd3c6e3ec4978
|
|||||||
+zen/split-view/zen-split-view.css
|
+zen/split-view/zen-split-view.css
|
||||||
+zen/spaces/zen-workspaces.css
|
+zen/spaces/zen-workspaces.css
|
||||||
+zen/common/styles/zen-toolbar.css
|
+zen/common/styles/zen-toolbar.css
|
||||||
|
+zen/common/styles/zen-single-components.css
|
||||||
+
|
+
|
||||||
+*.inc
|
+*.inc
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
|
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||||
index a76bb7aae7636c07989a4acb35281777d3103505..c5eafbdad5270cd9926ed4f1d850d134440d775b 100644
|
index a76bb7aae7636c07989a4acb35281777d3103505..d35aa0457307e95ad00464861fcee2827325193e 100644
|
||||||
--- a/browser/base/content/navigator-toolbox.inc.xhtml
|
--- a/browser/base/content/navigator-toolbox.inc.xhtml
|
||||||
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
|
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
|
||||||
@@ -2,7 +2,7 @@
|
@@ -2,7 +2,7 @@
|
||||||
@@ -55,3 +55,11 @@ index a76bb7aae7636c07989a4acb35281777d3103505..c5eafbdad5270cd9926ed4f1d850d134
|
|||||||
|
|
||||||
<toolbar id="nav-bar"
|
<toolbar id="nav-bar"
|
||||||
class="browser-toolbar"
|
class="browser-toolbar"
|
||||||
|
@@ -180,7 +188,6 @@
|
||||||
|
<toolbartabstop/>
|
||||||
|
<html:moz-urlbar id="urlbar"
|
||||||
|
class="urlbar"
|
||||||
|
- popover="manual"
|
||||||
|
pageproxystate="invalid"
|
||||||
|
unifiedsearchbutton-available=""
|
||||||
|
sap-name="urlbar">
|
||||||
|
|||||||
@@ -22,3 +22,4 @@
|
|||||||
#include ../../../zen/live-folders/jar.inc.mn
|
#include ../../../zen/live-folders/jar.inc.mn
|
||||||
#include ../../../zen/space-routing/jar.inc.mn
|
#include ../../../zen/space-routing/jar.inc.mn
|
||||||
#include ../../../zen/share/jar.inc.mn
|
#include ../../../zen/share/jar.inc.mn
|
||||||
|
#include ../../../zen/library/jar.inc.mn
|
||||||
|
|||||||
@@ -72,4 +72,6 @@
|
|||||||
<command id="cmd_zenNewLiveFolder" />
|
<command id="cmd_zenNewLiveFolder" />
|
||||||
|
|
||||||
<command id="cmd_zenDuplicateTab" />
|
<command id="cmd_zenDuplicateTab" />
|
||||||
|
|
||||||
|
<command id="cmd_zenToggleLibrary" />
|
||||||
</commandset>
|
</commandset>
|
||||||
|
|||||||
@@ -13,4 +13,5 @@
|
|||||||
<link rel="localization" href="browser/zen-live-folders.ftl"/>
|
<link rel="localization" href="browser/zen-live-folders.ftl"/>
|
||||||
<link rel="localization" href="browser/zen-space-routing.ftl"/>
|
<link rel="localization" href="browser/zen-space-routing.ftl"/>
|
||||||
<link rel="localization" href="browser/zen-share.ftl"/>
|
<link rel="localization" href="browser/zen-share.ftl"/>
|
||||||
|
<link rel="localization" href="browser/zen-library.ftl"/>
|
||||||
</linkset>
|
</linkset>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/browser/components/tabbrowser/Tabbrowser.sys.mjs b/browser/components/tabbrowser/Tabbrowser.sys.mjs
|
diff --git a/browser/components/tabbrowser/Tabbrowser.sys.mjs b/browser/components/tabbrowser/Tabbrowser.sys.mjs
|
||||||
index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa074f1ac2 100644
|
index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..5cfd47aa16ed47e90bf75c81070bbc6a4aadcff3 100644
|
||||||
--- a/browser/components/tabbrowser/Tabbrowser.sys.mjs
|
--- a/browser/components/tabbrowser/Tabbrowser.sys.mjs
|
||||||
+++ b/browser/components/tabbrowser/Tabbrowser.sys.mjs
|
+++ b/browser/components/tabbrowser/Tabbrowser.sys.mjs
|
||||||
@@ -484,6 +484,7 @@ export class Tabbrowser {
|
@@ -484,6 +484,7 @@ export class Tabbrowser {
|
||||||
@@ -538,7 +538,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
);
|
);
|
||||||
tabsFragment.appendChild(tabGroup.node);
|
tabsFragment.appendChild(tabGroup.node);
|
||||||
}
|
}
|
||||||
@@ -5080,9 +5253,21 @@ export class Tabbrowser {
|
@@ -5080,9 +5253,18 @@ export class Tabbrowser {
|
||||||
// to remove the old selected tab.
|
// to remove the old selected tab.
|
||||||
if (tabToSelect) {
|
if (tabToSelect) {
|
||||||
let leftoverTab = this.selectedTab;
|
let leftoverTab = this.selectedTab;
|
||||||
@@ -547,9 +547,6 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
this.removeTab(leftoverTab);
|
this.removeTab(leftoverTab);
|
||||||
+ } else {
|
+ } else {
|
||||||
+ this.documentGlobal.gZenWorkspaces._tabToRemoveForEmpty = leftoverTab;
|
+ this.documentGlobal.gZenWorkspaces._tabToRemoveForEmpty = leftoverTab;
|
||||||
+ if (Services.prefs.getBoolPref("zen.workspaces.continue-where-left-off")) {
|
|
||||||
+ this.documentGlobal.gZenWorkspaces._tabToSelect = selectTab - 1; // -1 for the empty tab.
|
|
||||||
+ }
|
|
||||||
+ if (this.documentGlobal.gZenWorkspaces._initialTab && !this.documentGlobal.gZenVerticalTabsManager._canReplaceNewTab) {
|
+ if (this.documentGlobal.gZenWorkspaces._initialTab && !this.documentGlobal.gZenVerticalTabsManager._canReplaceNewTab) {
|
||||||
+ this.documentGlobal.gZenWorkspaces._initialTab._shouldRemove = true;
|
+ this.documentGlobal.gZenWorkspaces._initialTab._shouldRemove = true;
|
||||||
+ }
|
+ }
|
||||||
@@ -560,7 +557,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
if (tabs.length > 1 || !tabs[0].selected) {
|
if (tabs.length > 1 || !tabs[0].selected) {
|
||||||
this.#updateTabsAfterInsert();
|
this.#updateTabsAfterInsert();
|
||||||
@@ -5313,11 +5498,17 @@ export class Tabbrowser {
|
@@ -5313,11 +5495,17 @@ export class Tabbrowser {
|
||||||
if (ownerTab) {
|
if (ownerTab) {
|
||||||
tab.owner = ownerTab;
|
tab.owner = ownerTab;
|
||||||
}
|
}
|
||||||
@@ -579,7 +576,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
let insertRelatedAfterCurrent = Services.prefs.getBoolPref(
|
let insertRelatedAfterCurrent = Services.prefs.getBoolPref(
|
||||||
"browser.tabs.insertRelatedAfterCurrent"
|
"browser.tabs.insertRelatedAfterCurrent"
|
||||||
);
|
);
|
||||||
@@ -5332,7 +5523,7 @@ export class Tabbrowser {
|
@@ -5332,7 +5520,7 @@ export class Tabbrowser {
|
||||||
(insertRelatedAfterCurrent && lastRelatedTab) ||
|
(insertRelatedAfterCurrent && lastRelatedTab) ||
|
||||||
openerTab ||
|
openerTab ||
|
||||||
this.selectedTab;
|
this.selectedTab;
|
||||||
@@ -588,7 +585,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
tabGroup = previousTab.group;
|
tabGroup = previousTab.group;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -5348,7 +5539,7 @@ export class Tabbrowser {
|
@@ -5348,7 +5536,7 @@ export class Tabbrowser {
|
||||||
previousTab.splitview
|
previousTab.splitview
|
||||||
) + 1;
|
) + 1;
|
||||||
} else if (previousTab.visible) {
|
} else if (previousTab.visible) {
|
||||||
@@ -597,7 +594,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
} else if (previousTab == this.documentGlobal.FirefoxViewHandler.tab) {
|
} else if (previousTab == this.documentGlobal.FirefoxViewHandler.tab) {
|
||||||
elementIndex = 0;
|
elementIndex = 0;
|
||||||
}
|
}
|
||||||
@@ -5376,14 +5567,14 @@ export class Tabbrowser {
|
@@ -5376,14 +5564,14 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
// Ensure index is within bounds.
|
// Ensure index is within bounds.
|
||||||
if (tab.pinned) {
|
if (tab.pinned) {
|
||||||
@@ -616,7 +613,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
if (pinned && !itemAfter?.pinned) {
|
if (pinned && !itemAfter?.pinned) {
|
||||||
itemAfter = null;
|
itemAfter = null;
|
||||||
@@ -5400,7 +5591,7 @@ export class Tabbrowser {
|
@@ -5400,7 +5588,7 @@ export class Tabbrowser {
|
||||||
|
|
||||||
this.tabContainer._invalidateCachedTabs();
|
this.tabContainer._invalidateCachedTabs();
|
||||||
|
|
||||||
@@ -625,7 +622,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
if (
|
if (
|
||||||
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
|
(this.isTab(itemAfter) && itemAfter.group == tabGroup) ||
|
||||||
this.isSplitViewWrapper(itemAfter)
|
this.isSplitViewWrapper(itemAfter)
|
||||||
@@ -5431,7 +5622,11 @@ export class Tabbrowser {
|
@@ -5431,7 +5619,11 @@ export class Tabbrowser {
|
||||||
const tabContainer = pinned
|
const tabContainer = pinned
|
||||||
? this.tabContainer.pinnedTabsContainer
|
? this.tabContainer.pinnedTabsContainer
|
||||||
: this.tabContainer;
|
: this.tabContainer;
|
||||||
@@ -637,7 +634,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tab.group?.collapsed) {
|
if (tab.group?.collapsed) {
|
||||||
@@ -5446,6 +5641,7 @@ export class Tabbrowser {
|
@@ -5446,6 +5638,7 @@ export class Tabbrowser {
|
||||||
if (pinned) {
|
if (pinned) {
|
||||||
this.#updateTabBarForPinnedTabs();
|
this.#updateTabBarForPinnedTabs();
|
||||||
}
|
}
|
||||||
@@ -645,7 +642,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
this.documentGlobal.TabBarVisibility.update();
|
this.documentGlobal.TabBarVisibility.update();
|
||||||
}
|
}
|
||||||
@@ -6025,6 +6221,7 @@ export class Tabbrowser {
|
@@ -6025,6 +6218,7 @@ export class Tabbrowser {
|
||||||
metricsContext,
|
metricsContext,
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
@@ -653,7 +650,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
|
// When 'closeWindowWithLastTab' pref is enabled, closing all tabs
|
||||||
// can be considered equivalent to closing the window.
|
// can be considered equivalent to closing the window.
|
||||||
if (
|
if (
|
||||||
@@ -6135,6 +6332,7 @@ export class Tabbrowser {
|
@@ -6135,6 +6329,7 @@ export class Tabbrowser {
|
||||||
closedTabCount -= 1;
|
closedTabCount -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -661,7 +658,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
if (closedTabCount > 0) {
|
if (closedTabCount > 0) {
|
||||||
this.recordTabMetrics(
|
this.recordTabMetrics(
|
||||||
@@ -6236,6 +6434,14 @@ export class Tabbrowser {
|
@@ -6236,6 +6431,14 @@ export class Tabbrowser {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,7 +673,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
let isVisibleTab = aTab.visible;
|
let isVisibleTab = aTab.visible;
|
||||||
// We have to sample the tab width now, since #beginRemoveTab might
|
// We have to sample the tab width now, since #beginRemoveTab might
|
||||||
// end up modifying the DOM in such a way that aTab gets a new
|
// end up modifying the DOM in such a way that aTab gets a new
|
||||||
@@ -6244,6 +6450,9 @@ export class Tabbrowser {
|
@@ -6244,6 +6447,9 @@ export class Tabbrowser {
|
||||||
let tabWidth =
|
let tabWidth =
|
||||||
this.documentGlobal.windowUtils.getBoundsWithoutFlushing(aTab).width;
|
this.documentGlobal.windowUtils.getBoundsWithoutFlushing(aTab).width;
|
||||||
let isLastTab = this.#isLastTabInWindow(aTab);
|
let isLastTab = this.#isLastTabInWindow(aTab);
|
||||||
@@ -686,7 +683,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
if (
|
if (
|
||||||
!this.#beginRemoveTab(aTab, {
|
!this.#beginRemoveTab(aTab, {
|
||||||
closeWindowFastpath: true,
|
closeWindowFastpath: true,
|
||||||
@@ -6254,13 +6463,14 @@ export class Tabbrowser {
|
@@ -6254,13 +6460,14 @@ export class Tabbrowser {
|
||||||
metricsContext,
|
metricsContext,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
@@ -702,7 +699,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
let lockTabSizing =
|
let lockTabSizing =
|
||||||
!this.tabContainer.verticalMode &&
|
!this.tabContainer.verticalMode &&
|
||||||
!aTab.pinned &&
|
!aTab.pinned &&
|
||||||
@@ -6291,7 +6501,13 @@ export class Tabbrowser {
|
@@ -6291,7 +6498,13 @@ export class Tabbrowser {
|
||||||
// We're not animating, so we can cancel the animation stopwatch.
|
// We're not animating, so we can cancel the animation stopwatch.
|
||||||
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
|
Glean.browserTabclose.timeAnim.cancel(aTab._closeTimeAnimTimerId);
|
||||||
aTab._closeTimeAnimTimerId = null;
|
aTab._closeTimeAnimTimerId = null;
|
||||||
@@ -717,7 +714,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6331,7 +6547,9 @@ export class Tabbrowser {
|
@@ -6331,7 +6544,9 @@ export class Tabbrowser {
|
||||||
get #shouldCloseWindowWithLastTab() {
|
get #shouldCloseWindowWithLastTab() {
|
||||||
return (
|
return (
|
||||||
!this.documentGlobal.toolbar.visible ||
|
!this.documentGlobal.toolbar.visible ||
|
||||||
@@ -728,7 +725,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6347,7 +6565,7 @@ export class Tabbrowser {
|
@@ -6347,7 +6562,7 @@ export class Tabbrowser {
|
||||||
*/
|
*/
|
||||||
#isLastTabInWindow(tab) {
|
#isLastTabInWindow(tab) {
|
||||||
for (const otherTab of this.tabs) {
|
for (const otherTab of this.tabs) {
|
||||||
@@ -737,7 +734,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6488,6 +6706,7 @@ export class Tabbrowser {
|
@@ -6488,6 +6703,7 @@ export class Tabbrowser {
|
||||||
|
|
||||||
newTab = true;
|
newTab = true;
|
||||||
}
|
}
|
||||||
@@ -745,7 +742,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
aTab._endRemoveArgs = [closeWindow, newTab];
|
aTab._endRemoveArgs = [closeWindow, newTab];
|
||||||
|
|
||||||
// swapBrowsersAndCloseOther will take care of closing the window without animation.
|
// swapBrowsersAndCloseOther will take care of closing the window without animation.
|
||||||
@@ -6542,13 +6761,7 @@ export class Tabbrowser {
|
@@ -6542,13 +6758,7 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newTab) {
|
if (newTab) {
|
||||||
@@ -760,7 +757,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
} else {
|
} else {
|
||||||
this.documentGlobal.TabBarVisibility.update();
|
this.documentGlobal.TabBarVisibility.update();
|
||||||
}
|
}
|
||||||
@@ -6701,6 +6914,7 @@ export class Tabbrowser {
|
@@ -6701,6 +6911,7 @@ export class Tabbrowser {
|
||||||
this.tabs[i]._index = i;
|
this.tabs[i]._index = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,7 +765,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
if (!this.#windowIsClosing) {
|
if (!this.#windowIsClosing) {
|
||||||
// update tab close buttons state
|
// update tab close buttons state
|
||||||
this.tabContainer._updateCloseButtons();
|
this.tabContainer._updateCloseButtons();
|
||||||
@@ -6891,6 +7105,7 @@ export class Tabbrowser {
|
@@ -6891,6 +7102,7 @@ export class Tabbrowser {
|
||||||
memory_after: await getTotalMemoryUsage(),
|
memory_after: await getTotalMemoryUsage(),
|
||||||
time_to_unload_in_ms: timeElapsed,
|
time_to_unload_in_ms: timeElapsed,
|
||||||
});
|
});
|
||||||
@@ -776,7 +773,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6937,11 +7152,12 @@ export class Tabbrowser {
|
@@ -6937,11 +7149,12 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
|
|
||||||
let excludeTabs = new Set(aExcludeTabs);
|
let excludeTabs = new Set(aExcludeTabs);
|
||||||
@@ -790,7 +787,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -6949,13 +7165,13 @@ export class Tabbrowser {
|
@@ -6949,13 +7162,13 @@ export class Tabbrowser {
|
||||||
!excludeTabs.has(aTab.owner) &&
|
!excludeTabs.has(aTab.owner) &&
|
||||||
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
|
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")
|
||||||
) {
|
) {
|
||||||
@@ -806,7 +803,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (Services.prefs.getBoolPref("browser.tabs.selectMRUOnClose", false)) {
|
if (Services.prefs.getBoolPref("browser.tabs.selectMRUOnClose", false)) {
|
||||||
@@ -6970,6 +7186,13 @@ export class Tabbrowser {
|
@@ -6970,6 +7183,13 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,7 +817,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
let tab = this.tabContainer.findNextTab(aTab, {
|
let tab = this.tabContainer.findNextTab(aTab, {
|
||||||
direction: 1,
|
direction: 1,
|
||||||
filter: _tab => remainingTabs.includes(_tab),
|
filter: _tab => remainingTabs.includes(_tab),
|
||||||
@@ -6983,7 +7206,7 @@ export class Tabbrowser {
|
@@ -6983,7 +7203,7 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tab) {
|
if (tab) {
|
||||||
@@ -829,7 +826,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no qualifying visible tab was found, see if there is a tab in
|
// If no qualifying visible tab was found, see if there is a tab in
|
||||||
@@ -7004,7 +7227,7 @@ export class Tabbrowser {
|
@@ -7004,7 +7224,7 @@ export class Tabbrowser {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -838,7 +835,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
#blurTab(aTab) {
|
#blurTab(aTab) {
|
||||||
@@ -7021,7 +7244,7 @@ export class Tabbrowser {
|
@@ -7021,7 +7241,7 @@ export class Tabbrowser {
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
* False if swapping isn't permitted, true otherwise.
|
* False if swapping isn't permitted, true otherwise.
|
||||||
*/
|
*/
|
||||||
@@ -847,7 +844,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
// Do not allow transfering a private tab to a non-private window
|
// Do not allow transfering a private tab to a non-private window
|
||||||
// and vice versa.
|
// and vice versa.
|
||||||
if (
|
if (
|
||||||
@@ -7080,6 +7303,7 @@ export class Tabbrowser {
|
@@ -7080,6 +7300,7 @@ export class Tabbrowser {
|
||||||
// fire the beforeunload event in the process. Close the other
|
// fire the beforeunload event in the process. Close the other
|
||||||
// window if this was its last tab.
|
// window if this was its last tab.
|
||||||
if (
|
if (
|
||||||
@@ -855,7 +852,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
!remoteBrowser.#beginRemoveTab(aOtherTab, {
|
!remoteBrowser.#beginRemoveTab(aOtherTab, {
|
||||||
adoptedByTab: aOurTab,
|
adoptedByTab: aOurTab,
|
||||||
closeWindowWithLastTab: true,
|
closeWindowWithLastTab: true,
|
||||||
@@ -7091,7 +7315,7 @@ export class Tabbrowser {
|
@@ -7091,7 +7312,7 @@ export class Tabbrowser {
|
||||||
// If this is the last tab of the window, hide the window
|
// If this is the last tab of the window, hide the window
|
||||||
// immediately without animation before the docshell swap, to avoid
|
// immediately without animation before the docshell swap, to avoid
|
||||||
// about:blank being painted.
|
// about:blank being painted.
|
||||||
@@ -864,7 +861,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
if (closeWindow) {
|
if (closeWindow) {
|
||||||
let win = aOtherTab.documentGlobal;
|
let win = aOtherTab.documentGlobal;
|
||||||
win.windowUtils.suppressAnimation(true);
|
win.windowUtils.suppressAnimation(true);
|
||||||
@@ -7231,11 +7455,13 @@ export class Tabbrowser {
|
@@ -7231,11 +7452,13 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finish tearing down the tab that's going away.
|
// Finish tearing down the tab that's going away.
|
||||||
@@ -878,7 +875,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
this.setTabTitle(aOurTab);
|
this.setTabTitle(aOurTab);
|
||||||
|
|
||||||
@@ -7484,10 +7710,10 @@ export class Tabbrowser {
|
@@ -7484,10 +7707,10 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,7 +888,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
aTab.selected ||
|
aTab.selected ||
|
||||||
aTab.closing ||
|
aTab.closing ||
|
||||||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
|
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
|
||||||
@@ -7576,7 +7802,8 @@ export class Tabbrowser {
|
@@ -7576,7 +7799,8 @@ export class Tabbrowser {
|
||||||
* @param {object} [aOptions={}]
|
* @param {object} [aOptions={}]
|
||||||
* Key-value pairs that will be serialized into the features string.
|
* Key-value pairs that will be serialized into the features string.
|
||||||
*/
|
*/
|
||||||
@@ -901,7 +898,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
if (this.tabs.length == 1) {
|
if (this.tabs.length == 1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -7593,7 +7820,7 @@ export class Tabbrowser {
|
@@ -7593,7 +7817,7 @@ export class Tabbrowser {
|
||||||
// tell a new window to take the "dropped" tab
|
// tell a new window to take the "dropped" tab
|
||||||
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
|
let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
|
||||||
args.appendElement(aTab.splitview ?? aTab);
|
args.appendElement(aTab.splitview ?? aTab);
|
||||||
@@ -910,7 +907,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
private: lazy.PrivateBrowsingUtils.isWindowPrivate(this.documentGlobal),
|
private: lazy.PrivateBrowsingUtils.isWindowPrivate(this.documentGlobal),
|
||||||
features: Object.entries(aOptions)
|
features: Object.entries(aOptions)
|
||||||
.map(([key, value]) => `${key}=${value}`)
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
@@ -7601,6 +7828,8 @@ export class Tabbrowser {
|
@@ -7601,6 +7825,8 @@ export class Tabbrowser {
|
||||||
openerWindow: this.documentGlobal,
|
openerWindow: this.documentGlobal,
|
||||||
args,
|
args,
|
||||||
});
|
});
|
||||||
@@ -919,7 +916,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7734,7 +7963,7 @@ export class Tabbrowser {
|
@@ -7734,7 +7960,7 @@ export class Tabbrowser {
|
||||||
* @returns {element is MozTabbrowserTabGroup}
|
* @returns {element is MozTabbrowserTabGroup}
|
||||||
*/
|
*/
|
||||||
isTabGroup(element) {
|
isTabGroup(element) {
|
||||||
@@ -928,7 +925,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7807,8 +8036,8 @@ export class Tabbrowser {
|
@@ -7807,8 +8033,8 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow mixing pinned and unpinned tabs.
|
// Don't allow mixing pinned and unpinned tabs.
|
||||||
@@ -939,7 +936,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
} else {
|
} else {
|
||||||
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
|
tabIndex = Math.max(tabIndex, this.pinnedTabCount);
|
||||||
}
|
}
|
||||||
@@ -7854,8 +8083,8 @@ export class Tabbrowser {
|
@@ -7854,8 +8080,8 @@ export class Tabbrowser {
|
||||||
this.#handleTabMove(
|
this.#handleTabMove(
|
||||||
element,
|
element,
|
||||||
() => {
|
() => {
|
||||||
@@ -950,7 +947,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
neighbor = neighbor.group;
|
neighbor = neighbor.group;
|
||||||
}
|
}
|
||||||
if (neighbor?.splitview) {
|
if (neighbor?.splitview) {
|
||||||
@@ -7866,6 +8095,12 @@ export class Tabbrowser {
|
@@ -7866,6 +8092,12 @@ export class Tabbrowser {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -963,7 +960,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
if (movingForwards && neighbor) {
|
if (movingForwards && neighbor) {
|
||||||
neighbor.after(element);
|
neighbor.after(element);
|
||||||
@@ -7939,23 +8174,31 @@ export class Tabbrowser {
|
@@ -7939,23 +8171,31 @@ export class Tabbrowser {
|
||||||
) {
|
) {
|
||||||
if (this.isTabGroupLabel(targetElement)) {
|
if (this.isTabGroupLabel(targetElement)) {
|
||||||
targetElement = targetElement.group;
|
targetElement = targetElement.group;
|
||||||
@@ -1001,7 +998,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
} else if (!element.pinned && targetElement && targetElement.pinned) {
|
} else if (!element.pinned && targetElement && targetElement.pinned) {
|
||||||
// If the caller asks to move an unpinned element next to a pinned
|
// If the caller asks to move an unpinned element next to a pinned
|
||||||
// tab, move the unpinned element to be the first unpinned element
|
// tab, move the unpinned element to be the first unpinned element
|
||||||
@@ -7968,12 +8211,35 @@ export class Tabbrowser {
|
@@ -7968,12 +8208,35 @@ export class Tabbrowser {
|
||||||
// move the tab group right before the first unpinned tab.
|
// move the tab group right before the first unpinned tab.
|
||||||
// 4. Moving a tab group and the first unpinned tab is grouped:
|
// 4. Moving a tab group and the first unpinned tab is grouped:
|
||||||
// move the tab group right before the first unpinned tab's tab group.
|
// move the tab group right before the first unpinned tab's tab group.
|
||||||
@@ -1038,7 +1035,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
// We want to include the splitview wrapper if it's the targetElement, but
|
// We want to include the splitview wrapper if it's the targetElement, but
|
||||||
// not in the case where we want to reverse tabs within the same splitview.
|
// not in the case where we want to reverse tabs within the same splitview.
|
||||||
@@ -7982,6 +8248,7 @@ export class Tabbrowser {
|
@@ -7982,6 +8245,7 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
|
|
||||||
let getContainer = () =>
|
let getContainer = () =>
|
||||||
@@ -1046,7 +1043,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
element.pinned
|
element.pinned
|
||||||
? this.tabContainer.pinnedTabsContainer
|
? this.tabContainer.pinnedTabsContainer
|
||||||
: this.tabContainer;
|
: this.tabContainer;
|
||||||
@@ -7990,11 +8257,15 @@ export class Tabbrowser {
|
@@ -7990,11 +8254,15 @@ export class Tabbrowser {
|
||||||
element,
|
element,
|
||||||
() => {
|
() => {
|
||||||
if (moveBefore) {
|
if (moveBefore) {
|
||||||
@@ -1063,7 +1060,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ metricsContext }
|
{ metricsContext }
|
||||||
@@ -8070,11 +8341,15 @@ export class Tabbrowser {
|
@@ -8070,11 +8338,15 @@ export class Tabbrowser {
|
||||||
* The context for the operation for telemetry purposes.
|
* The context for the operation for telemetry purposes.
|
||||||
*/
|
*/
|
||||||
moveTabToExistingGroup(aTab, aGroup, { metricsContext } = {}) {
|
moveTabToExistingGroup(aTab, aGroup, { metricsContext } = {}) {
|
||||||
@@ -1082,7 +1079,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
}
|
}
|
||||||
if (aTab.group && aTab.group.id === aGroup.id) {
|
if (aTab.group && aTab.group.id === aGroup.id) {
|
||||||
return;
|
return;
|
||||||
@@ -8153,6 +8428,7 @@ export class Tabbrowser {
|
@@ -8153,6 +8425,7 @@ export class Tabbrowser {
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
tabIndex: tab.index,
|
tabIndex: tab.index,
|
||||||
@@ -1090,7 +1087,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
};
|
};
|
||||||
if (tab.visible) {
|
if (tab.visible) {
|
||||||
state.elementIndex = tab.elementIndex;
|
state.elementIndex = tab.elementIndex;
|
||||||
@@ -8190,7 +8466,7 @@ export class Tabbrowser {
|
@@ -8190,7 +8463,7 @@ export class Tabbrowser {
|
||||||
let changedSplitView =
|
let changedSplitView =
|
||||||
previousTabState.splitViewId != currentTabState.splitViewId;
|
previousTabState.splitViewId != currentTabState.splitViewId;
|
||||||
|
|
||||||
@@ -1099,7 +1096,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
tab.dispatchEvent(
|
tab.dispatchEvent(
|
||||||
new this.documentGlobal.CustomEvent("TabMove", {
|
new this.documentGlobal.CustomEvent("TabMove", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
@@ -8248,6 +8524,10 @@ export class Tabbrowser {
|
@@ -8248,6 +8521,10 @@ export class Tabbrowser {
|
||||||
|
|
||||||
moveActionCallback();
|
moveActionCallback();
|
||||||
|
|
||||||
@@ -1110,7 +1107,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
// Clear tabs cache after moving nodes because the order of tabs may have
|
// Clear tabs cache after moving nodes because the order of tabs may have
|
||||||
// changed.
|
// changed.
|
||||||
this.tabContainer._invalidateCachedTabs();
|
this.tabContainer._invalidateCachedTabs();
|
||||||
@@ -8307,7 +8587,22 @@ export class Tabbrowser {
|
@@ -8307,7 +8584,22 @@ export class Tabbrowser {
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
* The new tab in the current window, null if the tab couldn't be adopted.
|
* The new tab in the current window, null if the tab couldn't be adopted.
|
||||||
*/
|
*/
|
||||||
@@ -1134,7 +1131,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
// Swap the dropped tab with a new one we create and then close
|
// Swap the dropped tab with a new one we create and then close
|
||||||
// it in the other window (making it seem to have moved between
|
// it in the other window (making it seem to have moved between
|
||||||
// windows). We also ensure that the tab we create to swap into has
|
// windows). We also ensure that the tab we create to swap into has
|
||||||
@@ -8350,6 +8645,8 @@ export class Tabbrowser {
|
@@ -8350,6 +8642,8 @@ export class Tabbrowser {
|
||||||
}
|
}
|
||||||
params.skipLoad = true;
|
params.skipLoad = true;
|
||||||
let newTab = this.addWebTab("about:blank", params);
|
let newTab = this.addWebTab("about:blank", params);
|
||||||
@@ -1143,7 +1140,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
aTab.container.tabDragAndDrop.finishAnimateTabMove();
|
aTab.container.tabDragAndDrop.finishAnimateTabMove();
|
||||||
|
|
||||||
@@ -9189,7 +9486,7 @@ export class Tabbrowser {
|
@@ -9189,7 +9483,7 @@ export class Tabbrowser {
|
||||||
// preventDefault(). It will still raise the window if appropriate.
|
// preventDefault(). It will still raise the window if appropriate.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1152,7 +1149,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
this.documentGlobal.focus();
|
this.documentGlobal.focus();
|
||||||
aEvent.preventDefault();
|
aEvent.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -9206,7 +9503,6 @@ export class Tabbrowser {
|
@@ -9206,7 +9500,6 @@ export class Tabbrowser {
|
||||||
|
|
||||||
on_TabGroupCollapse(aEvent) {
|
on_TabGroupCollapse(aEvent) {
|
||||||
aEvent.target.tabs.forEach(tab => {
|
aEvent.target.tabs.forEach(tab => {
|
||||||
@@ -1160,7 +1157,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9554,7 +9850,9 @@ export class Tabbrowser {
|
@@ -9554,7 +9847,9 @@ export class Tabbrowser {
|
||||||
|
|
||||||
let filter = this.#tabFilters.get(tab);
|
let filter = this.#tabFilters.get(tab);
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@@ -1170,7 +1167,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
|
|
||||||
let listener = this.#tabListeners.get(tab);
|
let listener = this.#tabListeners.get(tab);
|
||||||
if (listener) {
|
if (listener) {
|
||||||
@@ -10354,6 +10652,7 @@ class TabProgressListener {
|
@@ -10354,6 +10649,7 @@ class TabProgressListener {
|
||||||
aWebProgress.isTopLevel
|
aWebProgress.isTopLevel
|
||||||
) {
|
) {
|
||||||
this._tab.setAttribute("busy", "true");
|
this._tab.setAttribute("busy", "true");
|
||||||
@@ -1178,7 +1175,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
this.#tabbrowser._tabAttrModified(this._tab, ["busy"]);
|
this.#tabbrowser._tabAttrModified(this._tab, ["busy"]);
|
||||||
this._tab._notselectedsinceload = !this._tab.selected;
|
this._tab._notselectedsinceload = !this._tab.selected;
|
||||||
}
|
}
|
||||||
@@ -10434,6 +10733,7 @@ class TabProgressListener {
|
@@ -10434,6 +10730,7 @@ class TabProgressListener {
|
||||||
// known defaults. Note we use the original URL since about:newtab
|
// known defaults. Note we use the original URL since about:newtab
|
||||||
// redirects to a prerendered page.
|
// redirects to a prerendered page.
|
||||||
const shouldRemoveFavicon =
|
const shouldRemoveFavicon =
|
||||||
@@ -1186,7 +1183,7 @@ index f68e37926001384d0c8e80a3c163dc89e9c4ee5c..350cebd50aa6ee58fbbbb53d0f5334aa
|
|||||||
!this._browser.mIconURL &&
|
!this._browser.mIconURL &&
|
||||||
!ignoreBlank &&
|
!ignoreBlank &&
|
||||||
!(originalLocation.spec in FAVICON_DEFAULTS);
|
!(originalLocation.spec in FAVICON_DEFAULTS);
|
||||||
@@ -10610,13 +10910,6 @@ class TabProgressListener {
|
@@ -10610,13 +10907,6 @@ class TabProgressListener {
|
||||||
this._browser.originalURI = aRequest.originalURI;
|
this._browser.originalURI = aRequest.originalURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/browser/components/urlbar/content/UrlbarInputBase.mjs b/browser/components/urlbar/content/UrlbarInputBase.mjs
|
diff --git a/browser/components/urlbar/content/UrlbarInputBase.mjs b/browser/components/urlbar/content/UrlbarInputBase.mjs
|
||||||
index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc4292d4e6 100644
|
index 8be35b58c5369e123ca69426e852e3849bf75513..778d8aecce4da6748ecbf8b86a38c2da6e01bec5 100644
|
||||||
--- a/browser/components/urlbar/content/UrlbarInputBase.mjs
|
--- a/browser/components/urlbar/content/UrlbarInputBase.mjs
|
||||||
+++ b/browser/components/urlbar/content/UrlbarInputBase.mjs
|
+++ b/browser/components/urlbar/content/UrlbarInputBase.mjs
|
||||||
@@ -83,6 +83,12 @@ if (lazy) {
|
@@ -83,6 +83,12 @@ if (lazy) {
|
||||||
@@ -126,8 +126,11 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
startLayoutExtend() {
|
startLayoutExtend() {
|
||||||
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
|
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
|
||||||
// Do not expand if the Urlbar does not support being expanded or it is
|
// Do not expand if the Urlbar does not support being expanded or it is
|
||||||
@@ -3195,6 +3266,13 @@ ${
|
@@ -3193,8 +3264,16 @@ ${
|
||||||
|
}
|
||||||
|
|
||||||
this.toggleAttribute("breakout-extend", true);
|
this.toggleAttribute("breakout-extend", true);
|
||||||
|
+ this.#showBreakoutPopover();
|
||||||
this.#updateTextboxPosition();
|
this.#updateTextboxPosition();
|
||||||
|
|
||||||
+ this.window.gZenUIManager.onUrlbarOpen();
|
+ this.window.gZenUIManager.onUrlbarOpen();
|
||||||
@@ -140,7 +143,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
// Enable the animation only after the first extend call to ensure it
|
// Enable the animation only after the first extend call to ensure it
|
||||||
// doesn't run when opening a new window.
|
// doesn't run when opening a new window.
|
||||||
if (!this.hasAttribute("breakout-extend-animate")) {
|
if (!this.hasAttribute("breakout-extend-animate")) {
|
||||||
@@ -3218,6 +3296,29 @@ ${
|
@@ -3218,7 +3297,33 @@ ${
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,9 +171,13 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
+ this.removeAttribute("zen-floating-urlbar");
|
+ this.removeAttribute("zen-floating-urlbar");
|
||||||
+
|
+
|
||||||
this.toggleAttribute("breakout-extend", false);
|
this.toggleAttribute("breakout-extend", false);
|
||||||
|
+ if (this.#isAddressbar) {
|
||||||
|
+ this.#hideBreakoutPopover();
|
||||||
|
+ }
|
||||||
this.#updateTextboxPosition();
|
this.#updateTextboxPosition();
|
||||||
}
|
}
|
||||||
@@ -3256,7 +3357,7 @@ ${
|
|
||||||
|
@@ -3256,7 +3361,7 @@ ${
|
||||||
forceUnifiedSearchButtonAvailable = false
|
forceUnifiedSearchButtonAvailable = false
|
||||||
) {
|
) {
|
||||||
let prevState = this.getAttribute("pageproxystate");
|
let prevState = this.getAttribute("pageproxystate");
|
||||||
@@ -179,7 +186,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
this.setAttribute("pageproxystate", state);
|
this.setAttribute("pageproxystate", state);
|
||||||
this._inputContainer.setAttribute("pageproxystate", state);
|
this._inputContainer.setAttribute("pageproxystate", state);
|
||||||
this._identityBox?.setAttribute("pageproxystate", state);
|
this._identityBox?.setAttribute("pageproxystate", state);
|
||||||
@@ -3533,7 +3634,11 @@ ${
|
@@ -3533,7 +3638,11 @@ ${
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +199,38 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
}
|
}
|
||||||
|
|
||||||
#updateTextboxPositionNextFrame() {
|
#updateTextboxPositionNextFrame() {
|
||||||
@@ -3591,9 +3696,10 @@ ${
|
@@ -3553,12 +3662,29 @@ ${
|
||||||
|
this.removeAttribute("breakout");
|
||||||
|
this.parentNode.removeAttribute("breakout");
|
||||||
|
this.style.top = "";
|
||||||
|
+ this.#hideBreakoutPopover();
|
||||||
|
+ this._layoutBreakoutUpdateKey = {};
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #showBreakoutPopover() {
|
||||||
|
+ if (this.#isAddressbar) {
|
||||||
|
+ this.setAttribute("popover", "manual");
|
||||||
|
+ }
|
||||||
|
+ if (!this.matches(":popover-open")) {
|
||||||
|
+ this.showPopover();
|
||||||
|
+ }
|
||||||
|
+ this.#fixAddressbarSearchbarOrder();
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ #hideBreakoutPopover() {
|
||||||
|
try {
|
||||||
|
this.hidePopover();
|
||||||
|
} catch (ex) {
|
||||||
|
// No big deal if not a popover already.
|
||||||
|
}
|
||||||
|
- this._layoutBreakoutUpdateKey = {};
|
||||||
|
+ if (this.#isAddressbar) {
|
||||||
|
+ this.removeAttribute("popover");
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
|
||||||
|
incrementBreakoutBlockerCount() {
|
||||||
|
@@ -3591,9 +3717,10 @@ ${
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +242,19 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (this.#breakoutBlockerCount) {
|
if (this.#breakoutBlockerCount) {
|
||||||
@@ -4108,6 +4214,7 @@ ${
|
@@ -3602,8 +3729,9 @@ ${
|
||||||
|
|
||||||
|
this.setAttribute("breakout", "true");
|
||||||
|
this.parentNode.setAttribute("breakout", "true");
|
||||||
|
- this.showPopover();
|
||||||
|
- this.#fixAddressbarSearchbarOrder();
|
||||||
|
+ if (!this.#isAddressbar || this.hasAttribute("breakout-extend")) {
|
||||||
|
+ this.#showBreakoutPopover();
|
||||||
|
+ }
|
||||||
|
this.#updateTextboxPosition();
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
@@ -4108,6 +4236,7 @@ ${
|
||||||
}
|
}
|
||||||
|
|
||||||
_toggleActionOverride(event) {
|
_toggleActionOverride(event) {
|
||||||
@@ -212,7 +262,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
if (
|
if (
|
||||||
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
|
event.keyCode == KeyEvent.DOM_VK_SHIFT ||
|
||||||
event.keyCode == KeyEvent.DOM_VK_ALT ||
|
event.keyCode == KeyEvent.DOM_VK_ALT ||
|
||||||
@@ -4206,9 +4313,10 @@ ${
|
@@ -4206,9 +4335,10 @@ ${
|
||||||
if (!this.#isAddressbar) {
|
if (!this.#isAddressbar) {
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
@@ -226,7 +276,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
// Only trim value if the directionality doesn't change to RTL and we're not
|
// Only trim value if the directionality doesn't change to RTL and we're not
|
||||||
// showing a strikeout https protocol.
|
// showing a strikeout https protocol.
|
||||||
return UrlbarContentUtils.isTextDirectionRTL(trimmedValue, window) ||
|
return UrlbarContentUtils.isTextDirectionRTL(trimmedValue, window) ||
|
||||||
@@ -4410,6 +4518,11 @@ ${
|
@@ -4410,6 +4540,11 @@ ${
|
||||||
keepViewOpen = false,
|
keepViewOpen = false,
|
||||||
browserId = null,
|
browserId = null,
|
||||||
}) {
|
}) {
|
||||||
@@ -238,7 +288,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
let keyDownEnterDeferred;
|
let keyDownEnterDeferred;
|
||||||
if (
|
if (
|
||||||
this._keyDownEnterDeferred &&
|
this._keyDownEnterDeferred &&
|
||||||
@@ -4731,6 +4844,7 @@ ${
|
@@ -4731,6 +4866,7 @@ ${
|
||||||
this.setResultForCurrentValue(null);
|
this.setResultForCurrentValue(null);
|
||||||
this.handleCommand();
|
this.handleCommand();
|
||||||
this.controller.clearLastQueryContextCache();
|
this.controller.clearLastQueryContextCache();
|
||||||
@@ -246,7 +296,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
|
|
||||||
this._suppressStartQuery = false;
|
this._suppressStartQuery = false;
|
||||||
});
|
});
|
||||||
@@ -4742,7 +4856,6 @@ ${
|
@@ -4742,7 +4878,6 @@ ${
|
||||||
// Close the results pane, because paste and go doesn't want a result
|
// Close the results pane, because paste and go doesn't want a result
|
||||||
// selection. This has to happen before the menu opens: ending
|
// selection. This has to happen before the menu opens: ending
|
||||||
// breakout-extend once it's open keeps it from showing (bug 2037468).
|
// breakout-extend once it's open keeps it from showing (bug 2037468).
|
||||||
@@ -254,7 +304,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
|
|
||||||
let controller =
|
let controller =
|
||||||
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
|
this.document.commandDispatcher.getControllerForCommand("cmd_paste");
|
||||||
@@ -5025,7 +5138,11 @@ ${
|
@@ -5025,7 +5160,11 @@ ${
|
||||||
if (!engineName && !source && !this.hasAttribute("searchmode")) {
|
if (!engineName && !source && !this.hasAttribute("searchmode")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -267,7 +317,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
if (this._searchModeIndicatorTitle) {
|
if (this._searchModeIndicatorTitle) {
|
||||||
this._searchModeIndicatorTitle.textContent = "";
|
this._searchModeIndicatorTitle.textContent = "";
|
||||||
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
|
this._searchModeIndicatorTitle.removeAttribute("data-l10n-id");
|
||||||
@@ -5335,6 +5452,7 @@ ${
|
@@ -5335,6 +5474,7 @@ ${
|
||||||
|
|
||||||
this.document.l10n.setAttributes(
|
this.document.l10n.setAttributes(
|
||||||
this.inputField,
|
this.inputField,
|
||||||
@@ -275,7 +325,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
l10nId,
|
l10nId,
|
||||||
l10nId == "urlbar-placeholder-with-name"
|
l10nId == "urlbar-placeholder-with-name"
|
||||||
? { name: engineName }
|
? { name: engineName }
|
||||||
@@ -5385,6 +5503,12 @@ ${
|
@@ -5385,6 +5525,12 @@ ${
|
||||||
}
|
}
|
||||||
|
|
||||||
logger().debug("Blur Event");
|
logger().debug("Blur Event");
|
||||||
@@ -288,7 +338,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
// We cannot count every blur events after a missed engagement as abandoment
|
// We cannot count every blur events after a missed engagement as abandoment
|
||||||
// because the user may have clicked on some view element that executes
|
// because the user may have clicked on some view element that executes
|
||||||
// a command causing a focus change. For example opening preferences from
|
// a command causing a focus change. For example opening preferences from
|
||||||
@@ -5464,6 +5588,11 @@ ${
|
@@ -5464,6 +5610,11 @@ ${
|
||||||
}
|
}
|
||||||
|
|
||||||
_on_click(event) {
|
_on_click(event) {
|
||||||
@@ -300,7 +350,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
switch (event.target) {
|
switch (event.target) {
|
||||||
case this.inputField:
|
case this.inputField:
|
||||||
case this._inputContainer:
|
case this._inputContainer:
|
||||||
@@ -5558,10 +5687,11 @@ ${
|
@@ -5558,10 +5709,11 @@ ${
|
||||||
}
|
}
|
||||||
if (untrim) {
|
if (untrim) {
|
||||||
this.setValue(this._untrimmedValue);
|
this.setValue(this._untrimmedValue);
|
||||||
@@ -313,7 +363,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
this.view.autoOpen({ event });
|
this.view.autoOpen({ event });
|
||||||
} else {
|
} else {
|
||||||
if (this._untrimOnFocusAfterKeydown) {
|
if (this._untrimOnFocusAfterKeydown) {
|
||||||
@@ -5605,9 +5735,16 @@ ${
|
@@ -5605,9 +5757,16 @@ ${
|
||||||
}
|
}
|
||||||
|
|
||||||
_on_mousedown(event) {
|
_on_mousedown(event) {
|
||||||
@@ -331,7 +381,7 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
if (
|
if (
|
||||||
event.composedTarget != this.inputField &&
|
event.composedTarget != this.inputField &&
|
||||||
event.composedTarget != this._inputContainer
|
event.composedTarget != this._inputContainer
|
||||||
@@ -5617,6 +5754,10 @@ ${
|
@@ -5617,6 +5776,10 @@ ${
|
||||||
|
|
||||||
this.focusedViaMousedown = !this.focused;
|
this.focusedViaMousedown = !this.focused;
|
||||||
this.#preventClickSelectsAll = this.focused;
|
this.#preventClickSelectsAll = this.focused;
|
||||||
@@ -342,16 +392,16 @@ index 8be35b58c5369e123ca69426e852e3849bf75513..170cffbdd12a72dab15976cdab9207dc
|
|||||||
|
|
||||||
// Keep the focus status, since the attribute may be changed
|
// Keep the focus status, since the attribute may be changed
|
||||||
// upon calling this.focus().
|
// upon calling this.focus().
|
||||||
@@ -5654,7 +5795,7 @@ ${
|
@@ -5654,7 +5817,7 @@ ${
|
||||||
// view open on tab switch, and the TabSelect event arrived earlier.
|
// view open on tab switch, and the TabSelect event arrived earlier.
|
||||||
// Also ignore mousedown on the urlbarView context menu: opening/closing the
|
// Also ignore mousedown on the urlbarView context menu: opening/closing the
|
||||||
// view is already handled by the result opening flow.
|
// view is already handled by the result opening flow.
|
||||||
- if (event.target.closest?.("tab, #urlbarView-context-menu")) {
|
- if (event.target.closest?.("tab, #urlbarView-context-menu")) {
|
||||||
+ if (event.target.closest?.("tab, #urlbarView-context-menu") || event.target.closest?.("#tabs-newtab-button")) {
|
+ if (event.target.closest?.("tab, #urlbarView-context-menu") || event.target.closest?.("menupopup") || event.target.closest?.("#tabs-newtab-button")) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5948,7 +6089,7 @@ ${
|
@@ -5948,7 +6111,7 @@ ${
|
||||||
// When we are in actions search mode we can show more results so
|
// When we are in actions search mode we can show more results so
|
||||||
// increase the limit.
|
// increase the limit.
|
||||||
let maxResults =
|
let maxResults =
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/browser/components/urlbar/content/UrlbarShared.mjs b/browser/components/urlbar/content/UrlbarShared.mjs
|
diff --git a/browser/components/urlbar/content/UrlbarShared.mjs b/browser/components/urlbar/content/UrlbarShared.mjs
|
||||||
index 0ac5330223c84fbb98880a5c2d83ae58a8f3079e..f9d3d27333f5552404bd8a1138fb1e5d6150a3d8 100644
|
index 0ac5330223c84fbb98880a5c2d83ae58a8f3079e..ff488cf5a2c8b7857a8e145e40aaf861cfa655ec 100644
|
||||||
--- a/browser/components/urlbar/content/UrlbarShared.mjs
|
--- a/browser/components/urlbar/content/UrlbarShared.mjs
|
||||||
+++ b/browser/components/urlbar/content/UrlbarShared.mjs
|
+++ b/browser/components/urlbar/content/UrlbarShared.mjs
|
||||||
@@ -158,6 +158,7 @@ export const UrlbarShared = {
|
@@ -158,6 +158,7 @@ export const UrlbarShared = {
|
||||||
@@ -71,3 +71,11 @@ index 0ac5330223c84fbb98880a5c2d83ae58a8f3079e..f9d3d27333f5552404bd8a1138fb1e5d
|
|||||||
case "UrlbarProviderOmnibox":
|
case "UrlbarProviderOmnibox":
|
||||||
return UrlbarShared.RESULT_GROUP.HEURISTIC_OMNIBOX;
|
return UrlbarShared.RESULT_GROUP.HEURISTIC_OMNIBOX;
|
||||||
case "UrlbarProviderRestrictKeywordsAutofill":
|
case "UrlbarProviderRestrictKeywordsAutofill":
|
||||||
|
@@ -1060,6 +1080,7 @@ export const UrlbarShared = {
|
||||||
|
case "UrlbarProviderAboutPages":
|
||||||
|
return UrlbarShared.RESULT_GROUP.ABOUT_PAGES;
|
||||||
|
case "UrlbarProviderInputHistory":
|
||||||
|
+ case "ZenUrlbarProviderSidebar":
|
||||||
|
return UrlbarShared.RESULT_GROUP.INPUT_HISTORY;
|
||||||
|
case "UrlbarProviderQuickSuggest":
|
||||||
|
return UrlbarShared.RESULT_GROUP.GENERAL_PARENT;
|
||||||
|
|||||||
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
@@ -0,0 +1,37 @@
|
|||||||
|
#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,6 +143,7 @@
|
|||||||
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
|
* 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/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/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/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-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)
|
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
|
||||||
@@ -294,6 +295,7 @@
|
|||||||
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
|
* 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/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/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/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-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)
|
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
|
||||||
@@ -445,6 +447,7 @@
|
|||||||
* skin/classic/browser/zen-icons/tracking-protection.svg (../shared/zen-icons/nucleo/tracking-protection.svg)
|
* 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/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/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/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-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)
|
* skin/classic/browser/zen-icons/video-fill.svg (../shared/zen-icons/nucleo/video-fill.svg)
|
||||||
@@ -543,4 +546,11 @@
|
|||||||
* skin/classic/browser/zen-icons/selectable/warning.svg (../shared/zen-icons/common/selectable/warning.svg)
|
* 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/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/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)
|
skin/classic/browser/zen-icons/icons.css (../shared/zen-icons/icons.css)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#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,6 +60,13 @@ do_common_icons() {
|
|||||||
echo "Working on $filename"
|
echo "Working on $filename"
|
||||||
echo "* skin/classic/browser/zen-icons/selectable/$filename (../shared/zen-icons/common/selectable/$filename) " >> jar.inc.mn
|
echo "* skin/classic/browser/zen-icons/selectable/$filename (../shared/zen-icons/common/selectable/$filename) " >> jar.inc.mn
|
||||||
done
|
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
|
do_icons nucleo WIN # TODO: use windows icons
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ export class nsZenBoostEditor {
|
|||||||
* @returns {ZenBoostsParent} Boost JSActor parent
|
* @returns {ZenBoostsParent} Boost JSActor parent
|
||||||
*/
|
*/
|
||||||
get zenBoostsParent() {
|
get zenBoostsParent() {
|
||||||
const linkedBrowser = this.openerWindow.gBrowser.selectedTab.linkedBrowser;
|
const linkedBrowser =
|
||||||
|
this.editorWindow.browser ??
|
||||||
|
this.openerWindow.gBrowser.selectedTab.linkedBrowser;
|
||||||
const actor =
|
const actor =
|
||||||
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
|
linkedBrowser.browsingContext.currentWindowGlobal.getActor("ZenBoosts");
|
||||||
return actor;
|
return actor;
|
||||||
|
|||||||
@@ -606,9 +606,12 @@ class nsZenBoostsManager {
|
|||||||
* @param {Window} parentWindow - The parent browser window
|
* @param {Window} parentWindow - The parent browser window
|
||||||
* @param {Boost} boost - The boost which will be edited
|
* @param {Boost} boost - The boost which will be edited
|
||||||
* @param {nsIURI} domainUri - 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
|
* @returns {Window|null} The instanced editor window
|
||||||
*/
|
*/
|
||||||
openBoostWindow(parentWindow, boost, domainUri) {
|
openBoostWindow(parentWindow, boost, domainUri, { browser = null } = {}) {
|
||||||
if (!this.canBoostSite(domainUri)) {
|
if (!this.canBoostSite(domainUri)) {
|
||||||
console.error(
|
console.error(
|
||||||
"[ZenBoostsManager] Cannot open editor for boost with invalid domain."
|
"[ZenBoostsManager] Cannot open editor for boost with invalid domain."
|
||||||
@@ -648,26 +651,33 @@ class nsZenBoostsManager {
|
|||||||
null
|
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 = {
|
const progressListener = {
|
||||||
|
QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener"]),
|
||||||
onLocationChange: webProgress => {
|
onLocationChange: webProgress => {
|
||||||
if (webProgress.isTopLevel) {
|
if (webProgress.isTopLevel) {
|
||||||
editor.close();
|
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);
|
parentWindow.gBrowser.addProgressListener(progressListener);
|
||||||
|
|
||||||
editor.addEventListener(
|
editor.addEventListener(
|
||||||
"unload",
|
"unload",
|
||||||
() => {
|
() => {
|
||||||
@@ -676,10 +686,13 @@ class nsZenBoostsManager {
|
|||||||
onTabSelect
|
onTabSelect
|
||||||
);
|
);
|
||||||
parentWindow.gBrowser.removeProgressListener(progressListener);
|
parentWindow.gBrowser.removeProgressListener(progressListener);
|
||||||
|
editor.browser = null;
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.browser = browser;
|
||||||
// Give the domain
|
// Give the domain
|
||||||
editor.domain = domain;
|
editor.domain = domain;
|
||||||
editor.openerWindow = parentWindow;
|
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-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-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-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-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-toolbar.css (../../zen/common/styles/zen-toolbar.css)
|
||||||
content/browser/zen-styles/zen-browser-container.css (../../zen/common/styles/zen-browser-container.css)
|
content/browser/zen-styles/zen-browser-container.css (../../zen/common/styles/zen-browser-container.css)
|
||||||
|
|||||||
@@ -146,6 +146,20 @@ window.gZenUIManager = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shakes an element from side to side to catch the user's eye.
|
||||||
|
*
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {number} delay Milliseconds to wait before shaking.
|
||||||
|
*/
|
||||||
|
shakeElement(element, delay = 0) {
|
||||||
|
return this.elementAnimate(
|
||||||
|
element,
|
||||||
|
{ x: [0, -12, 8, -4, 2, 0] },
|
||||||
|
{ duration: 600, delay, easing: "ease-out" }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
_addNewCustomizableButtonsIfNeeded() {
|
_addNewCustomizableButtonsIfNeeded() {
|
||||||
const kPref = "zen.ui.migration.compact-mode-button-added";
|
const kPref = "zen.ui.migration.compact-mode-button-added";
|
||||||
let navbarPlacements = CustomizableUI.getWidgetIdsInArea(
|
let navbarPlacements = CustomizableUI.getWidgetIdsInArea(
|
||||||
@@ -1404,18 +1418,37 @@ window.gZenVerticalTabsManager = {
|
|||||||
|
|
||||||
if (isSingleToolbar) {
|
if (isSingleToolbar) {
|
||||||
this._navbarParent = navBar.parentElement;
|
this._navbarParent = navBar.parentElement;
|
||||||
let elements = document.querySelectorAll(
|
let elements = Array.from(
|
||||||
|
document.querySelectorAll(
|
||||||
'#nav-bar-customization-target > :is([cui-areatype="toolbar"], .chromeclass-toolbar-additional):not(#urlbar-container):not(toolbarspring)'
|
'#nav-bar-customization-target > :is([cui-areatype="toolbar"], .chromeclass-toolbar-additional):not(#urlbar-container):not(toolbarspring)'
|
||||||
|
)
|
||||||
);
|
);
|
||||||
elements = Array.from(elements).reverse();
|
let normalButtons = [];
|
||||||
|
let extensionButtons = [];
|
||||||
|
for (const element of elements) {
|
||||||
|
if (
|
||||||
|
element.hasAttribute("data-extensionid") &&
|
||||||
|
Services.prefs.getBoolPref("zen.view.overflow-webext-toolbar", true)
|
||||||
|
) {
|
||||||
|
extensionButtons.push(element);
|
||||||
|
} else {
|
||||||
|
normalButtons.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Add separator if it doesn't exist
|
// Add separator if it doesn't exist
|
||||||
if (!this._hasSetSingleToolbar) {
|
if (!this._hasSetSingleToolbar) {
|
||||||
buttonsTarget.append(this._topButtonsSeparatorElement);
|
buttonsTarget.append(this._topButtonsSeparatorElement);
|
||||||
}
|
}
|
||||||
this._hasSetSingleToolbar = true;
|
this._hasSetSingleToolbar = true;
|
||||||
for (const button of elements) {
|
for (const button of normalButtons.reverse()) {
|
||||||
this.appendCustomizableItem(this._topButtonsSeparatorElement, button);
|
this.appendCustomizableItem(this._topButtonsSeparatorElement, button);
|
||||||
}
|
}
|
||||||
|
for (const extension of extensionButtons) {
|
||||||
|
this.appendCustomizableItem(
|
||||||
|
this._topButtonsSeparatorElement,
|
||||||
|
extension
|
||||||
|
);
|
||||||
|
}
|
||||||
buttonsTarget.prepend(
|
buttonsTarget.prepend(
|
||||||
document.getElementById("unified-extensions-button")
|
document.getElementById("unified-extensions-button")
|
||||||
);
|
);
|
||||||
@@ -1576,10 +1609,6 @@ window.gZenVerticalTabsManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
rebuildURLBarMenus() {
|
rebuildURLBarMenus() {
|
||||||
if (document.getElementById("paste-and-go")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
gURLBar._initCopyCutController();
|
|
||||||
gURLBar.updatePlaceholder();
|
gURLBar.updatePlaceholder();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
.tab-group-overflow-count,
|
.tab-group-overflow-count,
|
||||||
.tab-group-editor-swatch,
|
.tab-group-editor-swatch,
|
||||||
.tab-preview-container-indicator,
|
.tab-preview-container-indicator,
|
||||||
|
.tab-preview-thumbnail-container > canvas,
|
||||||
#tabbrowser-tabs[orient='vertical']:not([expanded]) .tab-close-button,
|
#tabbrowser-tabs[orient='vertical']:not([expanded]) .tab-close-button,
|
||||||
.all-tabs-item > toolbarbutton.all-tabs-container-indicator::before,
|
.all-tabs-item > toolbarbutton.all-tabs-container-indicator::before,
|
||||||
.urlbarView-button,
|
.urlbarView-button,
|
||||||
|
|||||||
@@ -4,6 +4,38 @@
|
|||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* 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 {
|
@keyframes zen-back-and-forth-text {
|
||||||
0%,
|
0%,
|
||||||
10% {
|
10% {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
& > * {
|
||||||
z-index: 1;
|
z-index: var(--zen-main-app-child-z-index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stylelint-disable-next-line media-query-no-invalid */
|
/* stylelint-disable-next-line media-query-no-invalid */
|
||||||
|
|||||||
305
src/zen/common/styles/zen-library-widget.css
Normal file
305
src/zen/common/styles/zen-library-widget.css
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
|
||||||
|
@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");
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.81) * 100%));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#library-button-badge,
|
||||||
|
#zen-sidebar-foot-buttons[zen-library-badge] #zen-library-download-list > :last-child .zen-library-download-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#zen-sidebar-foot-buttons[zen-library-badge] {
|
||||||
|
& #library-button-badge {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[zen-library-stack-open] {
|
||||||
|
& #library-button-badge {
|
||||||
|
visibility: hidden;
|
||||||
|
transition: visibility 0s var(--zen-library-open-duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
& #zen-library-download-list > :last-child .zen-library-download-badge {
|
||||||
|
visibility: visible;
|
||||||
|
transition: visibility 0s var(--zen-library-open-duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-library-download-badge {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-library-download-badge:not([downloading]) {
|
||||||
|
background-image: var(--download-image);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-size: cover;
|
||||||
|
scale: 2;
|
||||||
|
|
||||||
|
& .zen-library-download-progress {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-library-download-badge[downloading] {
|
||||||
|
will-change: transform;
|
||||||
|
align-self: start;
|
||||||
|
transform: translateX(-2px);
|
||||||
|
|
||||||
|
--border-color: color-mix(in srgb, var(--zen-primary-color), light-dark(rgba(0,0,0,.8), white) 70%);
|
||||||
|
|
||||||
|
background-color: color-mix(in srgb, var(--zen-primary-color), light-dark(white, rgba(0,0,0,.5)) 80%);
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
box-shadow: 0 0 6px 2px rgba(0,0,0,.1);
|
||||||
|
color: var(--border-color);
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-library-download-badge[downloading]::after {
|
||||||
|
background-image: url("chrome://browser/skin/zen-icons/forward.svg");
|
||||||
|
rotate: 90deg;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
content: '';
|
||||||
|
background-size: contain;
|
||||||
|
inset: 2px;
|
||||||
|
fill: currentColor;
|
||||||
|
-moz-context-properties: fill;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-library-download-progress {
|
||||||
|
position: absolute;
|
||||||
|
inset: 1px;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
--badge-size: 25px;
|
||||||
|
--border-width: 4px;
|
||||||
|
--size: calc(var(--badge-size) + 2 * var(--border-width));
|
||||||
|
margin: calc(-1 * var(--border-width));
|
||||||
|
|
||||||
|
--thickness: 3px;
|
||||||
|
--progress: var(--zen-accent-button-background);
|
||||||
|
|
||||||
|
background: conic-gradient(
|
||||||
|
from -0deg,
|
||||||
|
var(--progress) calc(var(--value) * 1%),
|
||||||
|
transparent 0
|
||||||
|
);
|
||||||
|
|
||||||
|
mask: radial-gradient(
|
||||||
|
circle closest-side,
|
||||||
|
transparent calc(100% - var(--thickness)),
|
||||||
|
black calc(100% - var(--thickness))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#zen-library-download-list {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
|
||||||
|
visibility: hidden;
|
||||||
|
transition: visibility 0s var(--zen-library-open-duration);
|
||||||
|
|
||||||
|
#zen-sidebar-foot-buttons[zen-library-stack-open] & {
|
||||||
|
visibility: visible;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0;
|
||||||
|
padding-top: 10px;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.zen-library-download-list-download {
|
||||||
|
container-type: inline-size;
|
||||||
|
-moz-window-dragging: no-drag;
|
||||||
|
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
--zen-library-progress: 0;
|
||||||
|
--zen-library-entry-index: 0;
|
||||||
|
--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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,11 +164,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#navigator-toolbox[zen-has-implicit-hover="true"] &,
|
#navigator-toolbox[zen-has-implicit-hover="true"] &,
|
||||||
&[open],
|
&:is([open], [showing]),
|
||||||
#urlbar[has-popup-open="true"] &,
|
#urlbar[has-popup-open="true"] &,
|
||||||
:root[zen-has-empty-tab="true"] & {
|
:root[zen-has-empty-tab="true"] & {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
transition: opacity 0.15s !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -652,12 +653,11 @@
|
|||||||
--urlbarview-row-padding-block: 10px;
|
--urlbarview-row-padding-block: 10px;
|
||||||
color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)) !important;
|
color: light-dark(rgba(0, 0, 0, 0.7), rgba(255, 255, 255, 0.7)) !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover,
|
||||||
&,
|
&:hover .urlbarView-favicon,
|
||||||
& .urlbarView-favicon {
|
.urlbarView-action-favicon {
|
||||||
background-color: color-mix(in srgb, var(--zen-branding-bg-reverse) 5%, transparent 95%) !important;
|
background-color: color-mix(in srgb, var(--zen-branding-bg-reverse) 5%, transparent 95%) !important;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&[selected] {
|
&[selected] {
|
||||||
--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-bg: color-mix(in srgb, var(--zen-primary-color) 50%, light-dark(rgba(0, 0, 0, 0.5), rgba(255, 255, 255, 0.1)));
|
||||||
|
|||||||
@@ -411,3 +411,7 @@ menuseparator {
|
|||||||
#window-modal-dialog {
|
#window-modal-dialog {
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-preview-thumbnail-container > canvas {
|
||||||
|
border-radius: calc(var(--border-radius-medium) / var(--zen-squircle-value));
|
||||||
|
}
|
||||||
|
|||||||
@@ -800,3 +800,15 @@
|
|||||||
display: none;
|
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
|
||||||
|
|||||||
@@ -4,17 +4,25 @@
|
|||||||
|
|
||||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
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 {
|
export const ZenCustomizableUI = new (class {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
TYPE_TOOLBAR = "toolbar";
|
TYPE_TOOLBAR = "toolbar";
|
||||||
defaultSidebarIcons = [
|
defaultSidebarIcons = [
|
||||||
"downloads-button",
|
Services.prefs.getBoolPref("zen.library.enabled")
|
||||||
|
? "zen-library-button"
|
||||||
|
: "downloads-button",
|
||||||
"zen-workspaces-button",
|
"zen-workspaces-button",
|
||||||
"zen-create-new-button",
|
"zen-create-new-button",
|
||||||
];
|
];
|
||||||
|
|
||||||
startup(CustomizableUIInternal) {
|
startup(CustomizableUIInternal) {
|
||||||
|
CustomizableUIInternal.createBuiltinWidget(lazy.ZenLibraryWidget);
|
||||||
CustomizableUIInternal.registerArea(
|
CustomizableUIInternal.registerArea(
|
||||||
"zen-sidebar-top-buttons",
|
"zen-sidebar-top-buttons",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|||||||
const lazy = {};
|
const lazy = {};
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
CustomizableUI:
|
||||||
|
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
|
||||||
SessionStore:
|
SessionStore:
|
||||||
"moz-src:///browser/components/sessionstore/SessionStore.sys.mjs",
|
"moz-src:///browser/components/sessionstore/SessionStore.sys.mjs",
|
||||||
});
|
});
|
||||||
@@ -23,6 +25,7 @@ class nsZenUIMigration {
|
|||||||
console.error("ZenUIMigration: Error during migration", e);
|
console.error("ZenUIMigration: Error during migration", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.#migrateLibraryButton();
|
||||||
this.clearVariables();
|
this.clearVariables();
|
||||||
if (this.shouldRestart) {
|
if (this.shouldRestart) {
|
||||||
Services.startup.quit(
|
Services.startup.quit(
|
||||||
@@ -51,6 +54,41 @@ class nsZenUIMigration {
|
|||||||
this._migrationVersion = this.MIGRATION_VERSION;
|
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() {
|
_migrateV1() {
|
||||||
// If there's an userChrome.css or userContent.css existing, we set
|
// If there's an userChrome.css or userContent.css existing, we set
|
||||||
// 'toolkit.legacyUserProfileCustomizations.stylesheets' back to true
|
// 'toolkit.legacyUserProfileCustomizations.stylesheets' back to true
|
||||||
|
|||||||
@@ -166,6 +166,14 @@ document.addEventListener(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "cmd_zenToggleLibrary": {
|
||||||
|
const { ZenLibrary } = ChromeUtils.importESModule(
|
||||||
|
"moz-src:///zen/library/ZenLibrary.mjs",
|
||||||
|
{ global: "current" }
|
||||||
|
);
|
||||||
|
ZenLibrary.toggle();
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
gZenGlanceManager.handleMainCommandSet(event);
|
gZenGlanceManager.handleMainCommandSet(event);
|
||||||
if (event.target.id.startsWith("cmd_zenWorkspaceSwitch")) {
|
if (event.target.id.startsWith("cmd_zenWorkspaceSwitch")) {
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ ChromeUtils.defineLazyGetter(lazy, "mainAppWrapper", () =>
|
|||||||
document.getElementById("zen-main-app-wrapper")
|
document.getElementById("zen-main-app-wrapper")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(
|
||||||
|
lazy,
|
||||||
|
{ ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs" },
|
||||||
|
{ global: "current" }
|
||||||
|
);
|
||||||
|
|
||||||
window.gZenCompactModeManager = {
|
window.gZenCompactModeManager = {
|
||||||
_flashTimeouts: {},
|
_flashTimeouts: {},
|
||||||
_eventListeners: [],
|
_eventListeners: [],
|
||||||
@@ -169,6 +175,9 @@ window.gZenCompactModeManager = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set preference(value) {
|
set preference(value) {
|
||||||
|
if (lazy.ZenLibrary.isLibrarySlightlyOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!this.shouldBeCompact) {
|
if (!this.shouldBeCompact) {
|
||||||
value = false;
|
value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,9 +148,11 @@ class nsZenDownloadAnimationElement extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#determineEndPosition() {
|
#determineEndPosition() {
|
||||||
const downloadsButton = document.getElementById("downloads-button");
|
const downloadsButton = [
|
||||||
const isDownloadButtonVisible =
|
document.getElementById("zen-library-button"),
|
||||||
downloadsButton && this.#isElementVisible(downloadsButton);
|
document.getElementById("downloads-button"),
|
||||||
|
].find(button => button && this.#isElementVisible(button));
|
||||||
|
const isDownloadButtonVisible = !!downloadsButton;
|
||||||
|
|
||||||
let endPosition = { clientX: 0, clientY: 0 };
|
let endPosition = { clientX: 0, clientY: 0 };
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
width: 50%;
|
width: 50%;
|
||||||
height: 50%;
|
height: 50%;
|
||||||
background-color: var(--zen-primary-color);
|
background-color: var(--zen-primary-color);
|
||||||
mask: url("chrome://browser/content/zen-images/downloads/archive.svg") no-repeat center;
|
mask: url("chrome://browser/skin/zen-icons/library/library.svg") no-repeat center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
startTabDrag(event, tab, ...args) {
|
startTabDrag(event, tab, ...args) {
|
||||||
|
const options = args[0] ?? {};
|
||||||
this.ZenDragAndDropService.onDragStart(1);
|
this.ZenDragAndDropService.onDragStart(1);
|
||||||
this.#isOutOfWindow = false;
|
this.#isOutOfWindow = false;
|
||||||
gZenCompactModeManager._isTabBeingDragged = true;
|
gZenCompactModeManager._isTabBeingDragged = true;
|
||||||
@@ -145,13 +146,19 @@
|
|||||||
if (isTabGroupLabel(tab)) {
|
if (isTabGroupLabel(tab)) {
|
||||||
tab = tab.group;
|
tab = tab.group;
|
||||||
}
|
}
|
||||||
const draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
|
const shown = options.dragImageSource ?? tab;
|
||||||
|
let draggingTabs;
|
||||||
|
if (options.dragImageSource) {
|
||||||
|
draggingTabs = [shown];
|
||||||
|
} else {
|
||||||
|
draggingTabs = tab.multiselected ? gBrowser.selectedTabs : [tab];
|
||||||
|
}
|
||||||
const { offsetX, offsetY } = this.#getDragImageOffset(
|
const { offsetX, offsetY } = this.#getDragImageOffset(
|
||||||
event,
|
event,
|
||||||
tab,
|
shown,
|
||||||
draggingTabs
|
draggingTabs
|
||||||
);
|
);
|
||||||
const dragImage = this.#createDragImageForTabs(draggingTabs);
|
const dragImage = this._createDragImageForTabs(draggingTabs);
|
||||||
this.originalDragImageArgs = [dragImage, offsetX, offsetY];
|
this.originalDragImageArgs = [dragImage, offsetX, offsetY];
|
||||||
this.#dragImageEssential = tab.hasAttribute("zen-essential");
|
this.#dragImageEssential = tab.hasAttribute("zen-essential");
|
||||||
dt.setDragImage(...this.originalDragImageArgs);
|
dt.setDragImage(...this.originalDragImageArgs);
|
||||||
@@ -159,11 +166,13 @@
|
|||||||
tab.style.visibility = "hidden";
|
tab.style.visibility = "hidden";
|
||||||
}
|
}
|
||||||
this._dropLandingArmed =
|
this._dropLandingArmed =
|
||||||
!args[0]?.fromTabList && this.getDropEffectForTabDrag(event) == "move";
|
options.armLanding ??
|
||||||
|
(!args[0]?.fromTabList &&
|
||||||
|
this.getDropEffectForTabDrag(event) == "move");
|
||||||
this.ZenDragAndDropService.armDropLanding(this._dropLandingArmed);
|
this.ZenDragAndDropService.armDropLanding(this._dropLandingArmed);
|
||||||
}
|
}
|
||||||
|
|
||||||
#createDragImageForTabs(movingTabs) {
|
_createDragImageForTabs(movingTabs) {
|
||||||
const periphery = gZenWorkspaces.activeWorkspaceElement.querySelector(
|
const periphery = gZenWorkspaces.activeWorkspaceElement.querySelector(
|
||||||
"#tabbrowser-arrowscrollbox-periphery"
|
"#tabbrowser-arrowscrollbox-periphery"
|
||||||
);
|
);
|
||||||
@@ -675,7 +684,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#handle_sidebarDragOver(event);
|
this.#handle_sidebarDragOver(event);
|
||||||
this.#handle_tabDragOverToSplit(event);
|
this._handle_tabDragOverToSplit(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
#shouldSwitchSpace(event) {
|
#shouldSwitchSpace(event) {
|
||||||
@@ -721,16 +730,30 @@
|
|||||||
|
|
||||||
#onSpaceChanged(spaceChanged, dt) {
|
#onSpaceChanged(spaceChanged, dt) {
|
||||||
if (AppConstants.platform !== "macosx") {
|
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.
|
// explains why we need to do this on non-macOS platforms.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let tabs = this.originalDragImageArgs[0].children;
|
this._recolorDragImage(spaceChanged);
|
||||||
|
this._refreshDragImage(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recolours the current drag image's tab clones for a workspace's theme,
|
||||||
|
* so the image reads as the space it is over.
|
||||||
|
*
|
||||||
|
* @param {object} workspace
|
||||||
|
*/
|
||||||
|
_recolorDragImage(workspace) {
|
||||||
|
const wrapper = this.originalDragImageArgs?.[0];
|
||||||
|
if (!wrapper || !workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { isDarkMode, isExplicitMode } =
|
const { isDarkMode, isExplicitMode } =
|
||||||
gZenThemePicker.getGradientForWorkspace(spaceChanged, {
|
gZenThemePicker.getGradientForWorkspace(workspace, {
|
||||||
getGradient: false,
|
getGradient: false,
|
||||||
});
|
});
|
||||||
for (let tab of tabs) {
|
for (const tab of wrapper.querySelectorAll("tab")) {
|
||||||
if (isExplicitMode) {
|
if (isExplicitMode) {
|
||||||
tab.style.colorScheme = isDarkMode ? "dark" : "light";
|
tab.style.colorScheme = isDarkMode ? "dark" : "light";
|
||||||
tab.style.setProperty(
|
tab.style.setProperty(
|
||||||
@@ -742,9 +765,21 @@
|
|||||||
tab.style.removeProperty("--tab-selected-textcolor");
|
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(() => {
|
||||||
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -802,7 +837,60 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#handle_tabDragOverToSplit(event) {
|
/**
|
||||||
|
* The element a drop on `element` acts on.
|
||||||
|
*
|
||||||
|
* @param {Element} element
|
||||||
|
* @returns {Element}
|
||||||
|
*/
|
||||||
|
_targetForDrop(element) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a drop lands before (vs after) an element, from how far down its
|
||||||
|
* box the pointer sits and the move-over threshold pref.
|
||||||
|
*
|
||||||
|
* @param {DragEvent} event
|
||||||
|
* @param {DOMRect} rect - The element's bounds
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_dropsBefore(event, rect) {
|
||||||
|
const threshold =
|
||||||
|
Services.prefs.getIntPref(
|
||||||
|
"browser.tabs.dragDrop.moveOverThresholdPercent"
|
||||||
|
) / 100;
|
||||||
|
return (event.clientY - rect.top) / rect.height <= threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lays the shared drop indicator across an element, horizontally.
|
||||||
|
*
|
||||||
|
* @param {object} options
|
||||||
|
* @param {Element} [options.parent] - Reparent the indicator here first
|
||||||
|
* @param {number} options.left - Inset from the left, in px
|
||||||
|
* @param {number} options.width - Indicator width, in px
|
||||||
|
* @param {number} options.top - Top offset, in px
|
||||||
|
* @returns {Element} The indicator
|
||||||
|
*/
|
||||||
|
_placeDropIndicator({ parent = null, left, width, top }) {
|
||||||
|
const indicator = gZenPinnedTabManager.dragIndicator;
|
||||||
|
if (parent && indicator.parentNode !== parent) {
|
||||||
|
parent.appendChild(indicator);
|
||||||
|
}
|
||||||
|
indicator.setAttribute("orientation", "horizontal");
|
||||||
|
indicator.style.setProperty("--indicator-left", `${left}px`);
|
||||||
|
indicator.style.setProperty("--indicator-width", `${width}px`);
|
||||||
|
indicator.style.top = `${Math.round(top)}px`;
|
||||||
|
indicator.style.removeProperty("left");
|
||||||
|
return indicator;
|
||||||
|
}
|
||||||
|
|
||||||
|
get _splitDropReady() {
|
||||||
|
return !!this.#dragOverSplit.canDrop;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handle_tabDragOverToSplit(event) {
|
||||||
if (!this._dndSplitEnabled) {
|
if (!this._dndSplitEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -831,7 +919,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
movingTabsSet.has(dropElement) ||
|
movingTabsSet.has(this._targetForDrop(dropElement)) ||
|
||||||
!isTab(draggedTab) ||
|
!isTab(draggedTab) ||
|
||||||
draggedTab?.group?.hasAttribute("split-view-group") ||
|
draggedTab?.group?.hasAttribute("split-view-group") ||
|
||||||
draggedTab.hasAttribute("zen-live-folder-item-id") ||
|
draggedTab.hasAttribute("zen-live-folder-item-id") ||
|
||||||
@@ -1049,13 +1137,13 @@
|
|||||||
const placesBefore = new Map(
|
const placesBefore = new Map(
|
||||||
this.#draggedElements(event).map(element => [
|
this.#draggedElements(event).map(element => [
|
||||||
element,
|
element,
|
||||||
this.#placeOf(element),
|
this._placeOf(element),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
super.handle_drop(event);
|
super.handle_drop(event);
|
||||||
this.#maybeClearVerticalPinnedGridDragOver();
|
this.#maybeClearVerticalPinnedGridDragOver();
|
||||||
this.#handle_dropSwitchSpace(event);
|
this.#handle_dropSwitchSpace(event);
|
||||||
this.#handle_dropCreateSplit(event);
|
this._handle_dropCreateSplit(event);
|
||||||
this._clearDragOverSplit();
|
this._clearDragOverSplit();
|
||||||
if (!toSplit) {
|
if (!toSplit) {
|
||||||
this.#landDragImage(event, placesBefore);
|
this.#landDragImage(event, placesBefore);
|
||||||
@@ -1075,7 +1163,7 @@
|
|||||||
return (this._dragImageTabs ?? [draggedTab]).map(elementToMove);
|
return (this._dragImageTabs ?? [draggedTab]).map(elementToMove);
|
||||||
}
|
}
|
||||||
|
|
||||||
#placeOf(element) {
|
_placeOf(element) {
|
||||||
return `${element.screenX},${element.screenY}`;
|
return `${element.screenX},${element.screenY}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,8 +1182,20 @@
|
|||||||
if (gReduceMotion) {
|
if (gReduceMotion) {
|
||||||
return;
|
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 = [];
|
const landing = [];
|
||||||
for (const element of this.#draggedElements(event)) {
|
for (const element of elements) {
|
||||||
const { width, height } = element.getBoundingClientRect();
|
const { width, height } = element.getBoundingClientRect();
|
||||||
this.ZenDragAndDropService.addDropLandingRect(
|
this.ZenDragAndDropService.addDropLandingRect(
|
||||||
Math.round(element.screenX),
|
Math.round(element.screenX),
|
||||||
@@ -1103,7 +1203,7 @@
|
|||||||
Math.round(width),
|
Math.round(width),
|
||||||
Math.round(height)
|
Math.round(height)
|
||||||
);
|
);
|
||||||
if (placesBefore.get(element) !== this.#placeOf(element)) {
|
if (placesBefore?.get(element) !== this._placeOf(element)) {
|
||||||
element.style.visibility = "hidden";
|
element.style.visibility = "hidden";
|
||||||
landing.push(element);
|
landing.push(element);
|
||||||
}
|
}
|
||||||
@@ -1137,9 +1237,16 @@
|
|||||||
gZenWorkspaces.updateTabsContainers();
|
gZenWorkspaces.updateTabsContainers();
|
||||||
}
|
}
|
||||||
|
|
||||||
#handle_dropCreateSplit(event) {
|
/**
|
||||||
|
* @param {DragEvent} event
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {boolean} [options.activate] - Whether to select the created split
|
||||||
|
* @returns {boolean} Whether the drop split the dragged tab with the tab
|
||||||
|
* it was held over
|
||||||
|
*/
|
||||||
|
_handle_dropCreateSplit(event, { activate = true } = {}) {
|
||||||
if (!this.#dragOverSplit.canDrop) {
|
if (!this.#dragOverSplit.canDrop) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragData = this.#dragOverSplit.data;
|
const dragData = this.#dragOverSplit.data;
|
||||||
@@ -1147,11 +1254,11 @@
|
|||||||
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
const draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||||
|
|
||||||
if (!dragData || !draggedTab) {
|
if (!dragData || !draggedTab) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._dontAnimateTabMove = true;
|
this._dontAnimateTabMove = true;
|
||||||
const droppedOnTab = dragData.dropElement;
|
const droppedOnTab = this._targetForDrop(dragData.dropElement);
|
||||||
const dropSide = dragData.dropSide;
|
const dropSide = dragData.dropSide;
|
||||||
|
|
||||||
// Clear any visuals and timer
|
// Clear any visuals and timer
|
||||||
@@ -1161,8 +1268,10 @@
|
|||||||
gZenViewSplitter.splitTabs(
|
gZenViewSplitter.splitTabs(
|
||||||
isLeft ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab],
|
isLeft ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab],
|
||||||
"vsep",
|
"vsep",
|
||||||
isLeft ? 0 : 1
|
isLeft ? 0 : 1,
|
||||||
|
{ activate }
|
||||||
);
|
);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line complexity
|
// eslint-disable-next-line complexity
|
||||||
@@ -1550,33 +1659,17 @@
|
|||||||
this.#dragShiftableItems.at(-1)
|
this.#dragShiftableItems.at(-1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const indicator = gZenPinnedTabManager.dragIndicator;
|
dropBefore =
|
||||||
let top = 0;
|
!showIndicatorUnderNewTabButton && this._dropsBefore(event, rect);
|
||||||
threshold =
|
const top = Math.round(dropBefore ? rect.top : rect.top + rect.height);
|
||||||
Services.prefs.getIntPref(
|
if (gZenPinnedTabManager.dragIndicator.style.top !== `${top}px`) {
|
||||||
"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;
|
shouldPlayHapticFeedback = true;
|
||||||
}
|
}
|
||||||
indicator.setAttribute("orientation", "horizontal");
|
this._placeDropIndicator({
|
||||||
indicator.style.setProperty(
|
left: rect.left + separation / 2,
|
||||||
"--indicator-left",
|
width: rect.width - separation,
|
||||||
rect.left + separation / 2 + "px"
|
top,
|
||||||
);
|
});
|
||||||
indicator.style.setProperty(
|
|
||||||
"--indicator-width",
|
|
||||||
rect.width - separation + "px"
|
|
||||||
);
|
|
||||||
indicator.style.top = top;
|
|
||||||
indicator.style.removeProperty("left");
|
|
||||||
this.#removeDragOverBackground();
|
this.#removeDragOverBackground();
|
||||||
if (!isTab(dropElement) && dropElement?.parentElement?.isZenFolder) {
|
if (!isTab(dropElement) && dropElement?.parentElement?.isZenFolder) {
|
||||||
dropElement = dropElement.parentElement;
|
dropElement = dropElement.parentElement;
|
||||||
|
|||||||
@@ -404,13 +404,18 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collapsedRoot = group.rootMostCollapsedFolder;
|
let collapsedRoot = group.rootMostCollapsedFolder;
|
||||||
if (!collapsedRoot) {
|
if (!collapsedRoot) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the folder got expanded instead, the tab may still be
|
||||||
|
// inside of another collapsed subfolder.
|
||||||
|
while (collapsedRoot) {
|
||||||
collapsedRoot.setAttribute("has-active", "true");
|
collapsedRoot.setAttribute("has-active", "true");
|
||||||
await this.animateSelect(collapsedRoot);
|
const expanded = await this.animateSelect(collapsedRoot);
|
||||||
|
collapsedRoot = expanded ? group.rootMostCollapsedFolder : null;
|
||||||
|
}
|
||||||
gBrowser.tabContainer._invalidateCachedTabs();
|
gBrowser.tabContainer._invalidateCachedTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1505,22 +1510,28 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
#createAnimation(items, targetState, opts, callback = () => {}) {
|
#createAnimation(items, targetState, opts, callback = () => {}) {
|
||||||
items = Array.isArray(items) ? items : [items];
|
items = Array.isArray(items) ? items : [items];
|
||||||
return items.map(item =>
|
return items.map(item =>
|
||||||
this.#animateItem(item, targetState, opts).then(callback)
|
this.#animateItem(item, targetState, opts).then(
|
||||||
|
finished => finished && callback()
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#itemAnimations = new WeakMap();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Animates an element to the given target state. An array value is a
|
* Animates an element to the given target state. An array value is a
|
||||||
* [from, to] pair, "auto" endpoints are resolved by measuring and an
|
* [from, to] pair, "auto" endpoints are resolved by measuring and an
|
||||||
* empty string animates back to the element's natural value. The final
|
* empty string animates back to the element's natural value. The final
|
||||||
* values are left applied as inline styles.
|
* values are left applied as inline styles. A running animation on the
|
||||||
|
* same element is replaced, continuing from its current values.
|
||||||
*
|
*
|
||||||
* @param {Element} item - The element to animate.
|
* @param {Element} item - The element to animate.
|
||||||
* @param {object} targetState - Property to value (or [from, to]) map.
|
* @param {object} targetState - Property to value (or [from, to]) map.
|
||||||
* @param {object} opts - The animation options.
|
* @param {object} opts - The animation options.
|
||||||
* @param {number} opts.duration - The duration in seconds.
|
* @param {number} opts.duration - The duration in seconds.
|
||||||
* @param {string} opts.ease - The easing name.
|
* @param {string} opts.ease - The easing name.
|
||||||
* @returns {Promise} Resolves when the animation has finished.
|
* @returns {Promise<boolean>} Resolves when the animation has finished,
|
||||||
|
* false if it got replaced by another one before that.
|
||||||
*/
|
*/
|
||||||
async #animateItem(item, targetState, { duration = 0.18, ease } = {}) {
|
async #animateItem(item, targetState, { duration = 0.18, ease } = {}) {
|
||||||
const computed = window.getComputedStyle(item);
|
const computed = window.getComputedStyle(item);
|
||||||
@@ -1539,13 +1550,18 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
const to = {};
|
const to = {};
|
||||||
const finalStyles = new Map();
|
const finalStyles = new Map();
|
||||||
for (const [prop, value] of Object.entries(targetState)) {
|
for (const [prop, value] of Object.entries(targetState)) {
|
||||||
let [start, end] = Array.isArray(value) ? value : [undefined, value];
|
const start = Array.isArray(value) ? value[0] : undefined;
|
||||||
const cssProp = prop.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`);
|
from[prop] =
|
||||||
if (start === undefined || start === "auto") {
|
start === undefined || start === "auto"
|
||||||
start = measure(prop);
|
? measure(prop)
|
||||||
} else {
|
: toCssValue(prop, start);
|
||||||
start = toCssValue(prop, start);
|
|
||||||
}
|
}
|
||||||
|
// The start values are taken, the natural values need to be measured
|
||||||
|
// without the previous animation applying.
|
||||||
|
this.#itemAnimations.get(item)?.cancel();
|
||||||
|
for (const [prop, value] of Object.entries(targetState)) {
|
||||||
|
let end = Array.isArray(value) ? value[1] : value;
|
||||||
|
const cssProp = prop.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`);
|
||||||
if (end === "" || end === "auto") {
|
if (end === "" || end === "auto") {
|
||||||
// Resolve the natural value by clearing any inline override.
|
// Resolve the natural value by clearing any inline override.
|
||||||
item.style.removeProperty(cssProp);
|
item.style.removeProperty(cssProp);
|
||||||
@@ -1555,18 +1571,18 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
end = toCssValue(prop, end);
|
end = toCssValue(prop, end);
|
||||||
finalStyles.set(cssProp, end);
|
finalStyles.set(cssProp, end);
|
||||||
}
|
}
|
||||||
from[prop] = start;
|
|
||||||
to[prop] = end;
|
to[prop] = end;
|
||||||
}
|
}
|
||||||
const animation = item.animate([from, to], {
|
const animation = item.animate([from, to], {
|
||||||
duration: duration * 1000,
|
duration: duration * 1000,
|
||||||
easing: ease === "easeInOut" ? "ease-in-out" : "ease",
|
easing: ease === "easeInOut" ? "ease-in-out" : "ease",
|
||||||
});
|
});
|
||||||
|
this.#itemAnimations.set(item, animation);
|
||||||
try {
|
try {
|
||||||
await animation.finished;
|
await animation.finished;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// The animation was cancelled, leave the element as-is.
|
// The animation was replaced, leave the element to the new one.
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
for (const [cssProp, value] of finalStyles) {
|
for (const [cssProp, value] of finalStyles) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
@@ -1575,6 +1591,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
item.style.setProperty(cssProp, value);
|
item.style.setProperty(cssProp, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#calculateHeightShift(tabsContainer, selectedTabs) {
|
#calculateHeightShift(tabsContainer, selectedTabs) {
|
||||||
@@ -1757,20 +1774,17 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const afterMarginTop = () => {
|
// Update the active state and indentation right away instead of when
|
||||||
tabsContainer.style.overflowY = "";
|
// the animation ends, it would otherwise be lost if the folder gets
|
||||||
|
// collapsed again before that.
|
||||||
if (group.hasAttribute("has-active")) {
|
if (group.hasAttribute("has-active")) {
|
||||||
const activeTabs = group.activeTabs;
|
const activeTabs = group.activeTabs;
|
||||||
const folders = new Map();
|
|
||||||
group.removeAttribute("has-active");
|
group.removeAttribute("has-active");
|
||||||
for (let tab of activeTabs) {
|
for (const tab of activeTabs) {
|
||||||
const tabGroup = tab?.group?.hasAttribute("split-view-group")
|
const tabGroup = tab?.group?.hasAttribute("split-view-group")
|
||||||
? tab?.group?.group
|
? tab?.group?.group
|
||||||
: tab?.group;
|
: tab?.group;
|
||||||
if (!folders.has(tabGroup?.id)) {
|
const activeGroup = tabGroup?.activeGroups?.at(-1);
|
||||||
folders.set(tabGroup?.id, tabGroup?.activeGroups?.at(-1));
|
|
||||||
}
|
|
||||||
let activeGroup = folders.get(tabGroup?.id);
|
|
||||||
if (activeGroup) {
|
if (activeGroup) {
|
||||||
this.setFolderIndentation(
|
this.setFolderIndentation(
|
||||||
[tab],
|
[tab],
|
||||||
@@ -1788,18 +1802,25 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
folders.clear();
|
|
||||||
}
|
}
|
||||||
// Folder has been expanded and has no active tabs
|
// Folder has been expanded and has no active tabs
|
||||||
group.activeTabs = [];
|
group.activeTabs = [];
|
||||||
|
|
||||||
|
const afterMarginTop = () => {
|
||||||
|
tabsContainer.style.overflowY = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
let duration = this.#folderAnimationDuration;
|
let duration = this.#folderAnimationDuration;
|
||||||
|
|
||||||
|
// Items of collapsed subfolders must stay hidden, don't animate them twice.
|
||||||
|
const itemsToReveal = itemsToShow.filter(
|
||||||
|
item => !itemsToHide.includes(item)
|
||||||
|
);
|
||||||
|
|
||||||
animations.push(
|
animations.push(
|
||||||
...this.#createAnimation(
|
...this.#createAnimation(
|
||||||
itemsToShow,
|
itemsToReveal,
|
||||||
{ opacity: "", height: "" },
|
{ opacity: "", height: "", minHeight: "" },
|
||||||
{ duration, ease: "easeInOut" }
|
{ duration, ease: "easeInOut" }
|
||||||
),
|
),
|
||||||
...this.#createAnimation(
|
...this.#createAnimation(
|
||||||
@@ -1821,10 +1842,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
this.#animationCount += 1;
|
this.#animationCount += 1;
|
||||||
await Promise.all(animations);
|
await Promise.all(animations);
|
||||||
this.#animationCount -= 1;
|
this.#animationCount -= 1;
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
this.styleCleanup(itemsToShow);
|
|
||||||
this.styleCleanup(itemsToHide);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async animateUnloadAll(group) {
|
async animateUnloadAll(group) {
|
||||||
@@ -1969,9 +1986,16 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
gBrowser.tabContainer._invalidateCachedTabs();
|
gBrowser.tabContainer._invalidateCachedTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the selected tabs of a collapsed folder.
|
||||||
|
*
|
||||||
|
* @param {MozTabbrowserTabGroup} group - The collapsed folder.
|
||||||
|
* @returns {Promise<boolean>} True if the folder got expanded instead,
|
||||||
|
* since all of its tabs are active.
|
||||||
|
*/
|
||||||
async animateSelect(group) {
|
async animateSelect(group) {
|
||||||
if (!group?.isZenFolder) {
|
if (!group?.isZenFolder) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cancelPopupTimer();
|
this.cancelPopupTimer();
|
||||||
@@ -1989,7 +2013,24 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
|
|
||||||
if (group.collapsed && selectedTabs.length) {
|
if (group.collapsed && selectedTabs.length) {
|
||||||
const active = new Set([...(group.activeTabs ?? []), ...selectedTabs]);
|
const active = new Set([...(group.activeTabs ?? []), ...selectedTabs]);
|
||||||
const tabs = group.tabs.filter(tab => !tab.hasAttribute("zen-empty-tab"));
|
// Tabs inside of collapsed subfolders would stay hidden if the folder
|
||||||
|
// gets expanded, so they don't need to be active.
|
||||||
|
const isInsideCollapsedSubfolder = tab => {
|
||||||
|
let folder = tab.group?.hasAttribute("split-view-group")
|
||||||
|
? tab.group.group
|
||||||
|
: tab.group;
|
||||||
|
while (folder && folder !== group) {
|
||||||
|
if (folder.collapsed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
folder = folder.group;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
const tabs = group.tabs.filter(
|
||||||
|
tab =>
|
||||||
|
!tab.hasAttribute("zen-empty-tab") && !isInsideCollapsedSubfolder(tab)
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
tabs.length &&
|
tabs.length &&
|
||||||
tabs.every(
|
tabs.every(
|
||||||
@@ -2001,7 +2042,7 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
group.collapsed = false;
|
group.collapsed = false;
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2160,11 +2201,12 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
await Promise.all(animations);
|
await Promise.all(animations);
|
||||||
this.#animationCount -= 1;
|
this.#animationCount -= 1;
|
||||||
if (this.#animationCount) {
|
if (this.#animationCount) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
this.styleCleanup(selectedTabs);
|
this.styleCleanup(selectedTabs);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
animateGroupMove(group, expand = false) {
|
animateGroupMove(group, expand = false) {
|
||||||
@@ -2187,6 +2229,31 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to the folder's space, expands its parents (root first) and the
|
||||||
|
* folder itself, then jiggles it so it's easy to spot.
|
||||||
|
*
|
||||||
|
* @param {MozTabbrowserTabGroup} folder The folder to reveal.
|
||||||
|
*/
|
||||||
|
async revealFolder(folder) {
|
||||||
|
const workspaceId = folder.getAttribute("zen-workspace-id");
|
||||||
|
if (workspaceId && workspaceId != gZenWorkspaces.activeWorkspace) {
|
||||||
|
await gZenWorkspaces.changeWorkspaceWithID(workspaceId);
|
||||||
|
}
|
||||||
|
let collapsedRoot = folder.rootMostCollapsedFolder;
|
||||||
|
const wasCollapsed = !!collapsedRoot;
|
||||||
|
while (collapsedRoot) {
|
||||||
|
collapsedRoot.collapsed = false;
|
||||||
|
collapsedRoot = folder.rootMostCollapsedFolder;
|
||||||
|
}
|
||||||
|
const label = folder.labelElement.parentElement;
|
||||||
|
label.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||||
|
await gZenUIManager.shakeElement(
|
||||||
|
label,
|
||||||
|
wasCollapsed ? this.#folderRevealDuration * 1000 : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
styleCleanup(items) {
|
styleCleanup(items) {
|
||||||
items.forEach(item => {
|
items.forEach(item => {
|
||||||
item.style.removeProperty("opacity");
|
item.style.removeProperty("opacity");
|
||||||
|
|||||||
@@ -8,7 +8,10 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root,
|
||||||
|
/* Add zen-folder so that accent color changes done to each
|
||||||
|
* specific space is applied. */
|
||||||
|
zen-folder {
|
||||||
--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-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-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));
|
--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,6 +63,153 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
|||||||
.addEventListener("click", this.onOverlayClick.bind(this));
|
.addEventListener("click", this.onOverlayClick.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#detached = null;
|
||||||
|
|
||||||
|
get detachedBrowser() {
|
||||||
|
return this.#detached?.browser ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #detachedHost() {
|
||||||
|
let host = document.getElementById("zen-glance-detached");
|
||||||
|
if (!host) {
|
||||||
|
host = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<html:div id="zen-glance-detached" hidden="true">
|
||||||
|
<html:div class="zen-glance-detached-backdrop"/>
|
||||||
|
<html:div class="browserContainer">
|
||||||
|
<html:div class="browserStack"/>
|
||||||
|
</html:div>
|
||||||
|
</html:div>
|
||||||
|
`).firstElementChild;
|
||||||
|
host
|
||||||
|
.querySelector(".zen-glance-detached-backdrop")
|
||||||
|
.addEventListener("click", () => this.closeDetachedGlance());
|
||||||
|
document.getElementById("zen-main-app-wrapper").appendChild(host);
|
||||||
|
}
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a detached glance.
|
||||||
|
*
|
||||||
|
* @param {object} data - The URL, its triggering principal, the point the
|
||||||
|
* glance animates out of as clientX/clientY in window coordinates, and
|
||||||
|
* optionally the userContextId to load in
|
||||||
|
* @returns {Promise<{browser: Element, closed: Promise<string>}|null>}
|
||||||
|
* Once open, the glance's browser and a promise for its closing, which
|
||||||
|
*/
|
||||||
|
async openDetachedGlance(data) {
|
||||||
|
if (this.#detached || !this.#isGlanceLoadAllowed(data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const host = this.#detachedHost;
|
||||||
|
const wrapper = host.querySelector(".browserContainer");
|
||||||
|
const browser = document.createXULElement("browser");
|
||||||
|
browser.setAttribute("type", "content");
|
||||||
|
browser.setAttribute("remote", "true");
|
||||||
|
browser.setAttribute("maychangeremoteness", "true");
|
||||||
|
browser.setAttribute("disableglobalhistory", "true");
|
||||||
|
browser.setAttribute("messagemanagergroup", "browsers");
|
||||||
|
browser.setAttribute("nodefaultsrc", "true");
|
||||||
|
if (data.userContextId) {
|
||||||
|
browser.setAttribute("usercontextid", data.userContextId);
|
||||||
|
}
|
||||||
|
host.querySelector(".browserStack").appendChild(browser);
|
||||||
|
wrapper.appendChild(this.#createNewOverlayButtons());
|
||||||
|
for (const selector of [
|
||||||
|
".zen-glance-sidebar-open",
|
||||||
|
".zen-glance-sidebar-split",
|
||||||
|
]) {
|
||||||
|
wrapper.querySelector(selector)?.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
host.hidden = false;
|
||||||
|
const hostRect = host.getBoundingClientRect();
|
||||||
|
const startPoint = {
|
||||||
|
clientX: data.clientX - hostRect.left,
|
||||||
|
clientY: data.clientY - hostRect.top,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
let resolveClosed;
|
||||||
|
const whenClosed = new Promise(resolve => {
|
||||||
|
resolveClosed = resolve;
|
||||||
|
});
|
||||||
|
this.#detached = {
|
||||||
|
host,
|
||||||
|
wrapper,
|
||||||
|
browser,
|
||||||
|
startPoint,
|
||||||
|
closing: false,
|
||||||
|
resolveClosed,
|
||||||
|
};
|
||||||
|
|
||||||
|
browser.loadURI(Services.io.newURI(data.url), {
|
||||||
|
triggeringPrincipal: data.triggeringPrincipal,
|
||||||
|
});
|
||||||
|
wrapper.setAttribute("animate", true);
|
||||||
|
await gZenUIManager.elementAnimate(
|
||||||
|
wrapper,
|
||||||
|
this.#createGlanceArcSequence(startPoint, "opening", null, hostRect),
|
||||||
|
{
|
||||||
|
duration: gZenUIManager.testingEnabled
|
||||||
|
? 0
|
||||||
|
: this.#GLANCE_ANIMATION_DURATION,
|
||||||
|
easing: "ease-in-out",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
wrapper.removeAttribute("animate");
|
||||||
|
wrapper.setAttribute("has-finished-animation", true);
|
||||||
|
return { browser, closed: whenClosed };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {boolean} [options.skipPermitUnload] - Close even if the page
|
||||||
|
* would rather not unload
|
||||||
|
*/
|
||||||
|
async closeDetachedGlance({ skipPermitUnload = false } = {}) {
|
||||||
|
const detached = this.#detached;
|
||||||
|
if (!detached || detached.closing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!skipPermitUnload && !detached.browser.permitUnload().permitUnload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
detached.closing = true;
|
||||||
|
const { host, wrapper, browser } = detached;
|
||||||
|
detached.resolveClosed();
|
||||||
|
this.#animateSidebarButtons(
|
||||||
|
wrapper.querySelector(".zen-glance-sidebar-container")
|
||||||
|
);
|
||||||
|
host.setAttribute("fade-out", true);
|
||||||
|
wrapper.removeAttribute("has-finished-animation");
|
||||||
|
wrapper.setAttribute("animate", true);
|
||||||
|
try {
|
||||||
|
await gZenUIManager.elementAnimate(
|
||||||
|
wrapper,
|
||||||
|
this.#createGlanceArcSequence(
|
||||||
|
detached.startPoint,
|
||||||
|
"closing",
|
||||||
|
null,
|
||||||
|
host.getBoundingClientRect()
|
||||||
|
),
|
||||||
|
{
|
||||||
|
duration: gZenUIManager.testingEnabled
|
||||||
|
? 0
|
||||||
|
: this.#GLANCE_ANIMATION_DURATION,
|
||||||
|
easing: "ease-out",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
browser.destroy();
|
||||||
|
browser.remove();
|
||||||
|
wrapper.removeAttribute("animate");
|
||||||
|
host.removeAttribute("fade-out");
|
||||||
|
host.hidden = true;
|
||||||
|
this.#detached = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#setupPreferences() {
|
#setupPreferences() {
|
||||||
XPCOMUtils.defineLazyPreferenceGetter(
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
this._lazyPref,
|
this._lazyPref,
|
||||||
@@ -102,7 +249,10 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
|||||||
handleMainCommandSet(event) {
|
handleMainCommandSet(event) {
|
||||||
const command = event.target;
|
const command = event.target;
|
||||||
const commandHandlers = {
|
const commandHandlers = {
|
||||||
cmd_zenGlanceClose: () => this.closeGlance({ onTabClose: true }),
|
cmd_zenGlanceClose: () =>
|
||||||
|
this.#detached
|
||||||
|
? this.closeDetachedGlance()
|
||||||
|
: this.closeGlance({ onTabClose: true }),
|
||||||
cmd_zenGlanceExpand: () => this.fullyOpenGlance(),
|
cmd_zenGlanceExpand: () => this.fullyOpenGlance(),
|
||||||
cmd_zenGlanceSplit: () => this.splitGlance(),
|
cmd_zenGlanceSplit: () => this.splitGlance(),
|
||||||
};
|
};
|
||||||
@@ -617,9 +767,16 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
|||||||
* @param {object} data - Glance data with position and dimensions
|
* @param {object} data - Glance data with position and dimensions
|
||||||
* @param {string} direction - 'opening' or 'closing'
|
* @param {string} direction - 'opening' or 'closing'
|
||||||
* @param {Element|null} imageDataElement - The image data element for preview (optional)
|
* @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
|
* @returns {object} Animation sequence object
|
||||||
*/
|
*/
|
||||||
#createGlanceArcSequence(data, direction, imageDataElement = null) {
|
#createGlanceArcSequence(
|
||||||
|
data,
|
||||||
|
direction,
|
||||||
|
imageDataElement = null,
|
||||||
|
referenceRect = null
|
||||||
|
) {
|
||||||
let { clientX, clientY, width, height } = data;
|
let { clientX, clientY, width, height } = data;
|
||||||
if (imageDataElement?.parentElement) {
|
if (imageDataElement?.parentElement) {
|
||||||
// Since we are animating scale transforms on the wrapper, we need to
|
// Since we are animating scale transforms on the wrapper, we need to
|
||||||
@@ -649,9 +806,9 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
|||||||
// Calculate start and end positions based on direction
|
// Calculate start and end positions based on direction
|
||||||
let startPosition, endPosition;
|
let startPosition, endPosition;
|
||||||
|
|
||||||
const tabPanelsRect = window.windowUtils.getBoundsWithoutFlushing(
|
const tabPanelsRect =
|
||||||
gBrowser.tabpanels
|
referenceRect ??
|
||||||
);
|
window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
|
||||||
|
|
||||||
const widthPercent = 0.8;
|
const widthPercent = 0.8;
|
||||||
if (direction === "opening") {
|
if (direction === "opening") {
|
||||||
|
|||||||
@@ -103,7 +103,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.browserSidebarContainer.zen-glance-background,
|
.browserSidebarContainer.zen-glance-background,
|
||||||
.browserSidebarContainer.zen-glance-overlay .browserContainer:not([fade-out="true"]) {
|
:is(.browserSidebarContainer.zen-glance-overlay, #zen-glance-detached)
|
||||||
|
.browserContainer:not([fade-out="true"]) {
|
||||||
border-radius: var(--zen-native-inner-radius);
|
border-radius: var(--zen-native-inner-radius);
|
||||||
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
|
@media not (-moz-pref("zen.theme.squircle-browser-view")) {
|
||||||
/* stylelint-disable-next-line property-no-unknown */
|
/* stylelint-disable-next-line property-no-unknown */
|
||||||
@@ -112,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.browserSidebarContainer.zen-glance-overlay {
|
:is(.browserSidebarContainer.zen-glance-overlay, #zen-glance-detached) {
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
visibility: inherit;
|
visibility: inherit;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -198,3 +199,19 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#zen-main-app-wrapper > #zen-glance-detached {
|
||||||
|
position: fixed;
|
||||||
|
inset: 24px;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .zen-glance-detached-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
599
src/zen/library/ZenLibrary.mjs
Normal file
599
src/zen/library/ZenLibrary.mjs
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||||
|
|
||||||
|
const { ZenLibraryWidget } = ChromeUtils.importESModule(
|
||||||
|
"moz-src:///zen/library/ZenLibraryWidget.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
let lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(
|
||||||
|
lazy,
|
||||||
|
{
|
||||||
|
ZenLibraryHistorySection:
|
||||||
|
"moz-src:///zen/library/sections/ZenLibraryHistorySection.mjs",
|
||||||
|
ZenLibraryDownloadsSection:
|
||||||
|
"moz-src:///zen/library/sections/ZenLibraryDownloadsSection.mjs",
|
||||||
|
ZenLibraryBoostsSection:
|
||||||
|
"moz-src:///zen/library/sections/ZenLibraryBoostsSection.mjs",
|
||||||
|
ZenLibraryMediaSection:
|
||||||
|
"moz-src:///zen/library/sections/ZenLibraryMediaSection.mjs",
|
||||||
|
ZenLibrarySpacesSection:
|
||||||
|
"moz-src:///zen/library/sections/ZenLibrarySpacesSection.mjs",
|
||||||
|
},
|
||||||
|
{ global: "current" }
|
||||||
|
);
|
||||||
|
|
||||||
|
const LAST_TAB_PREF = "zen.library.last-tab";
|
||||||
|
|
||||||
|
ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
|
||||||
|
return document.getElementById("zen-appcontent-wrapper");
|
||||||
|
});
|
||||||
|
|
||||||
|
export class ZenLibrary extends MozLitElement {
|
||||||
|
static instance = null;
|
||||||
|
#progress = 0;
|
||||||
|
|
||||||
|
#springControls = null;
|
||||||
|
|
||||||
|
#toolboxWidth = 0;
|
||||||
|
|
||||||
|
#originalButtonsNextSibling = null;
|
||||||
|
|
||||||
|
get #hasAdoptedButtons() {
|
||||||
|
return this.#originalButtonsNextSibling !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canSwipe = false;
|
||||||
|
#isOpen = false;
|
||||||
|
|
||||||
|
#isWrapperSwipeAttached = false;
|
||||||
|
#wrapperGestureControl = null;
|
||||||
|
|
||||||
|
#resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.openProgress = this.#progress;
|
||||||
|
});
|
||||||
|
|
||||||
|
static queries = {
|
||||||
|
_content: "#zen-library-content",
|
||||||
|
_header: "#zen-library-header",
|
||||||
|
_footer: "#zen-library-footer",
|
||||||
|
};
|
||||||
|
|
||||||
|
static properties = {
|
||||||
|
_activeTab: { type: String },
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.zenLibrarySections = {
|
||||||
|
media: lazy.ZenLibraryMediaSection,
|
||||||
|
downloads: lazy.ZenLibraryDownloadsSection,
|
||||||
|
boosts: lazy.ZenLibraryBoostsSection,
|
||||||
|
...(!window.gZenWorkspaces.privateWindowOrDisabled
|
||||||
|
? {
|
||||||
|
spaces: lazy.ZenLibrarySpacesSection,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
history: lazy.ZenLibraryHistorySection,
|
||||||
|
};
|
||||||
|
const lastTab = Services.prefs.getStringPref(LAST_TAB_PREF, "history");
|
||||||
|
this.activeTab = lastTab in this.zenLibrarySections ? lastTab : "history";
|
||||||
|
this.#hijackFirefoxCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isLibraryOpen() {
|
||||||
|
const lib = this.getInstance();
|
||||||
|
return lib.#isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isLibrarySlightlyOpen() {
|
||||||
|
const lib = this.getInstance(/* createIfMissing = */ false);
|
||||||
|
if (!lib) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Due to calculation inaccuracies assume
|
||||||
|
// that openProgress never goes back to 0
|
||||||
|
return lib.openProgress > 0.001;
|
||||||
|
}
|
||||||
|
|
||||||
|
set activeTab(value) {
|
||||||
|
if (this._activeTab === value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._activeTab = value;
|
||||||
|
Services.prefs.setStringPref(LAST_TAB_PREF, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeTab() {
|
||||||
|
return this._activeTab;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeSection() {
|
||||||
|
return this.zenLibrarySections[this.activeTab];
|
||||||
|
}
|
||||||
|
|
||||||
|
set openProgress(value) {
|
||||||
|
const p = value;
|
||||||
|
const stealWindowButtonsPastPoint = 0.6;
|
||||||
|
const wasOpen = this.#progress > 0.001;
|
||||||
|
this.#progress = p;
|
||||||
|
const isPastWindowButtonSwitchPoint = p > stealWindowButtonsPastPoint;
|
||||||
|
const isOpen = p > 0.001;
|
||||||
|
|
||||||
|
let libraryWidth = window.windowUtils.getBoundsWithoutFlushing(this).width;
|
||||||
|
const compactModeOffsetDirection = this.#libraryOnRight
|
||||||
|
? -this.#toolboxWidth
|
||||||
|
: this.#toolboxWidth;
|
||||||
|
const compactModeOffset = this.#isCompactMode
|
||||||
|
? compactModeOffsetDirection
|
||||||
|
: 0;
|
||||||
|
let webOffset =
|
||||||
|
(this.#libraryOnRight ? -1 : 1) * (libraryWidth - this.#toolboxWidth) +
|
||||||
|
compactModeOffset;
|
||||||
|
|
||||||
|
lazy.appContentWrapper?.style.setProperty(
|
||||||
|
"--library-wrapper-target-px",
|
||||||
|
`${webOffset}px`
|
||||||
|
);
|
||||||
|
[this, lazy.appContentWrapper, gNavToolbox].forEach(elem => {
|
||||||
|
elem?.style.setProperty("--library-progress", String(p));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isOpen && !wasOpen) {
|
||||||
|
this.setAttribute("open", "true");
|
||||||
|
document.documentElement.setAttribute("zen-library-open", "true");
|
||||||
|
} else if (!isOpen && wasOpen) {
|
||||||
|
this.removeAttribute("open");
|
||||||
|
document.documentElement.removeAttribute("zen-library-open");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPastWindowButtonSwitchPoint && this.#coversWindowButtons) {
|
||||||
|
this.#adoptWindowButtons();
|
||||||
|
} else if (!isPastWindowButtonSwitchPoint) {
|
||||||
|
this.#restoreWindowButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the window buttons sit in the sidebar column the library covers.
|
||||||
|
*/
|
||||||
|
get #coversWindowButtons() {
|
||||||
|
if (!gZenVerticalTabsManager.isWindowsStyledButtons) {
|
||||||
|
return !this.#libraryOnRight;
|
||||||
|
}
|
||||||
|
return this.#libraryOnRight && !this.#isCompactMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
get openProgress() {
|
||||||
|
return this.#progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hijackFirefoxCommands() {
|
||||||
|
document
|
||||||
|
.getElementById("Browser:ShowAllHistory")
|
||||||
|
.addEventListener("command", event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
|
ZenLibrary.toggle("history");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#adoptWindowButtons() {
|
||||||
|
if (this.#hasAdoptedButtons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realButtons = gZenVerticalTabsManager.actualWindowButtons;
|
||||||
|
if (!this.#originalButtonsNextSibling) {
|
||||||
|
this.#originalButtonsNextSibling = {
|
||||||
|
isNext: realButtons.nextSibling,
|
||||||
|
sibling: realButtons.nextSibling || realButtons.previousSibling,
|
||||||
|
clone: realButtons.cloneNode(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#originalButtonsNextSibling.clone.classList.add(
|
||||||
|
"zen-library-window-buttons-clone"
|
||||||
|
);
|
||||||
|
if (this.#originalButtonsNextSibling.isNext) {
|
||||||
|
this.#originalButtonsNextSibling.sibling.before(
|
||||||
|
this.#originalButtonsNextSibling.clone
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.#originalButtonsNextSibling.sibling.after(
|
||||||
|
this.#originalButtonsNextSibling.clone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._header.appendChild(realButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#restoreWindowButtons() {
|
||||||
|
if (!this.#hasAdoptedButtons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realButtons = gZenVerticalTabsManager.actualWindowButtons;
|
||||||
|
if (this.#originalButtonsNextSibling) {
|
||||||
|
this.#originalButtonsNextSibling.clone.remove();
|
||||||
|
if (this.#originalButtonsNextSibling.isNext) {
|
||||||
|
this.#originalButtonsNextSibling.sibling.before(realButtons);
|
||||||
|
} else {
|
||||||
|
this.#originalButtonsNextSibling.sibling.after(realButtons);
|
||||||
|
}
|
||||||
|
this.#originalButtonsNextSibling = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#stylesLoaded = null;
|
||||||
|
|
||||||
|
#whenStylesLoaded() {
|
||||||
|
this.#stylesLoaded ??= this.updateComplete.then(() => {
|
||||||
|
const link = this.querySelector("link[rel='stylesheet']");
|
||||||
|
if (!link || link.sheet) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
link.addEventListener("load", resolve, { once: true });
|
||||||
|
link.addEventListener("error", resolve, { once: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return this.#stylesLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
#idleCleanup = null;
|
||||||
|
|
||||||
|
#scheduleIdleCleanup() {
|
||||||
|
this.#idleCleanup = window.requestIdleCallback(() => {
|
||||||
|
this.#idleCleanup = null;
|
||||||
|
this.#stylesLoaded = null;
|
||||||
|
ZenLibrary.instance = null;
|
||||||
|
this.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancelIdleCleanup() {
|
||||||
|
if (this.#idleCleanup) {
|
||||||
|
window.cancelIdleCallback(this.#idleCleanup);
|
||||||
|
this.#idleCleanup = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens or closes the library. With a tab id, opens the library on that
|
||||||
|
* tab, switches to it if already open on another, or closes if it is
|
||||||
|
* already the open one. Without one, plainly toggles open and closed.
|
||||||
|
*
|
||||||
|
* @param {string?} [tab] - A section id to open on
|
||||||
|
*/
|
||||||
|
static toggle(tab = undefined) {
|
||||||
|
if (!Services.prefs.getBoolPref("zen.library.enabled")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lib = this.getInstance();
|
||||||
|
if (tab && tab in lib.zenLibrarySections) {
|
||||||
|
if (lib.#isOpen && lib.activeTab === tab) {
|
||||||
|
this.animateProgress(0);
|
||||||
|
} else if (lib.#isOpen) {
|
||||||
|
lib.activeTab = tab;
|
||||||
|
} else {
|
||||||
|
lib.activeTab = tab;
|
||||||
|
this.animateProgress(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.animateProgress(lib.#isOpen ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async animateProgress(target) {
|
||||||
|
const lib = this.getInstance();
|
||||||
|
lib.#detachWrapperOfSwipe();
|
||||||
|
lib.#cancelIdleCleanup();
|
||||||
|
await lib.#whenStylesLoaded();
|
||||||
|
lib.style.visibility = "";
|
||||||
|
await window.promiseDocumentFlushed(() => {});
|
||||||
|
|
||||||
|
if (lib.#springControls) {
|
||||||
|
lib.#springControls.stop();
|
||||||
|
lib.#springControls = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === 1) {
|
||||||
|
lib.#onOpenLibrary();
|
||||||
|
lib.#isOpen = true;
|
||||||
|
} else if (target === 0) {
|
||||||
|
lib.#isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lib.setAttribute("transitioning", "true");
|
||||||
|
lib.#springControls = gZenUIManager.motion.animate(
|
||||||
|
lib.openProgress,
|
||||||
|
target,
|
||||||
|
{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 720,
|
||||||
|
damping: 47,
|
||||||
|
mass: 1.2,
|
||||||
|
onUpdate: latest => {
|
||||||
|
lib.openProgress = latest;
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
lib.openProgress = target;
|
||||||
|
lib.#springControls = null;
|
||||||
|
lib.removeAttribute("transitioning");
|
||||||
|
if (target === 0) {
|
||||||
|
lib.#scheduleIdleCleanup();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async startSwipe() {
|
||||||
|
const lib = this.getInstance();
|
||||||
|
lib.#cancelIdleCleanup();
|
||||||
|
lib.#canSwipe = true;
|
||||||
|
await lib.#whenStylesLoaded();
|
||||||
|
lib.style.visibility = "";
|
||||||
|
await window.promiseDocumentFlushed(() => {});
|
||||||
|
if (!lib.#canSwipe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lib.#onOpenLibrary();
|
||||||
|
|
||||||
|
if (lib.#springControls) {
|
||||||
|
lib.#springControls.stop();
|
||||||
|
lib.#springControls = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
lib.style.setProperty("pointer-events", "none");
|
||||||
|
lib.#attachWrapperToSwipe();
|
||||||
|
}
|
||||||
|
|
||||||
|
static stopSwipe(direction) {
|
||||||
|
const lib = this.getInstance();
|
||||||
|
lib.style.setProperty("pointer-events", "unset");
|
||||||
|
lib.#canSwipe = false;
|
||||||
|
|
||||||
|
if (lib.#libraryOnRight) {
|
||||||
|
direction = direction * -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction) {
|
||||||
|
const target = Math.max(-direction, 0);
|
||||||
|
this.animateProgress(target);
|
||||||
|
}
|
||||||
|
lib.#detachWrapperOfSwipe();
|
||||||
|
|
||||||
|
// Return library open state
|
||||||
|
return lib.#isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
static swipeProgress(target) {
|
||||||
|
const lib = this.getInstance();
|
||||||
|
if (!lib.#canSwipe) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lib.openProgress = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
#attachWrapperToSwipe() {
|
||||||
|
if (!this.#isWrapperSwipeAttached) {
|
||||||
|
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||||
|
this.#wrapperGestureControl =
|
||||||
|
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(
|
||||||
|
appWrapper
|
||||||
|
);
|
||||||
|
this.#isWrapperSwipeAttached = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#detachWrapperOfSwipe() {
|
||||||
|
if (this.#isWrapperSwipeAttached || this.#wrapperGestureControl) {
|
||||||
|
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||||
|
window.gZenWorkspaces._swipeManager?.detachWorkspaceSwipeGestures(
|
||||||
|
appWrapper,
|
||||||
|
this.#wrapperGestureControl
|
||||||
|
);
|
||||||
|
this.#wrapperGestureControl = null;
|
||||||
|
this.#isWrapperSwipeAttached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onOpenLibrary() {
|
||||||
|
gURLBar.view.close();
|
||||||
|
// Get the width from the css property,
|
||||||
|
// getBoundsWithoutFlushing will fail as it takes the
|
||||||
|
// toolbox transformation during the animation into account
|
||||||
|
this.#toolboxWidth = parseFloat(
|
||||||
|
gNavToolbox.style
|
||||||
|
.getPropertyValue("--actual-zen-sidebar-width")
|
||||||
|
.replace("/\D/g", "")
|
||||||
|
);
|
||||||
|
if (document.documentElement.hasAttribute("zen-sidebar-expanded")) {
|
||||||
|
this.#toolboxWidth += window.windowUtils.getBoundsWithoutFlushing(
|
||||||
|
document.getElementById("zen-sidebar-splitter")
|
||||||
|
).width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(createIfMissing = true) {
|
||||||
|
if (!this.instance && createIfMissing) {
|
||||||
|
this.instance = new ZenLibrary();
|
||||||
|
this.instance.style.visibility = "collapse";
|
||||||
|
const mountRoot = document.getElementById("zen-main-app-wrapper");
|
||||||
|
mountRoot.append(this.instance);
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #libraryOnRight() {
|
||||||
|
return gZenVerticalTabsManager._prefsRightSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get libraryOnRight() {
|
||||||
|
const lib = this.getInstance();
|
||||||
|
return lib.#libraryOnRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (super.connectedCallback) {
|
||||||
|
super.connectedCallback();
|
||||||
|
}
|
||||||
|
this.onKeyDown = this.onKeyDown.bind(this);
|
||||||
|
document.addEventListener("keydown", this.onKeyDown, true);
|
||||||
|
this.#resizeObserver.observe(this);
|
||||||
|
|
||||||
|
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(this);
|
||||||
|
|
||||||
|
this._tabOpen = this.onTabOpen.bind(this);
|
||||||
|
window.addEventListener("TabOpen", this._tabOpen);
|
||||||
|
|
||||||
|
ZenLibraryWidget.attachLibrary(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#cancelIdleCleanup();
|
||||||
|
if (this.#springControls) {
|
||||||
|
this.#springControls.stop();
|
||||||
|
this.#springControls = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#restoreWindowButtons();
|
||||||
|
ZenLibraryWidget.detachLibrary(this);
|
||||||
|
|
||||||
|
super.disconnectedCallback();
|
||||||
|
document.removeEventListener("keydown", this.onKeyDown, true);
|
||||||
|
this.#resizeObserver.disconnect();
|
||||||
|
|
||||||
|
if (this._tabOpen) {
|
||||||
|
window.removeEventListener("TabOpen", this._tabOpen);
|
||||||
|
this._tabOpen = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get #isCompactMode() {
|
||||||
|
return (
|
||||||
|
window.gZenCompactModeManager.preference &&
|
||||||
|
Services.prefs.getBoolPref("zen.view.compact.hide-tabbar")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onTabOpen() {
|
||||||
|
if (this.#isOpen) {
|
||||||
|
ZenLibrary.animateProgress(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(e) {
|
||||||
|
if (!this.hasAttribute("open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
ZenLibrary.animateProgress(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
if (super.firstUpdated) {
|
||||||
|
super.firstUpdated();
|
||||||
|
}
|
||||||
|
this.#buildFooterButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildFooterButtons() {
|
||||||
|
const footer = this.querySelector("#zen-library-footer");
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
image: "chrome://browser/skin/zen-icons/back.svg",
|
||||||
|
l10nId: "library-footer-close-button",
|
||||||
|
command: () => ZenLibrary.animateProgress(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "chrome://browser/skin/zen-icons/heart-circle-fill.svg",
|
||||||
|
l10nId: "library-footer-donate-button",
|
||||||
|
command: () => {
|
||||||
|
window.openTrustedLinkIn("https://www.zen-browser.app/donate", "tab");
|
||||||
|
ZenLibrary.animateProgress(0);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { image, l10nId, command } of buttons) {
|
||||||
|
const button = document.createXULElement("toolbarbutton");
|
||||||
|
button.className = "toolbarbutton-1";
|
||||||
|
button.setAttribute("image", image);
|
||||||
|
button.setAttribute("data-l10n-id", l10nId);
|
||||||
|
button.addEventListener("command", command);
|
||||||
|
footer.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#animateTabIcon(tab) {
|
||||||
|
tab.removeAttribute("animate");
|
||||||
|
// Flush styles so re-adding the attribute restarts the animation.
|
||||||
|
void tab.offsetWidth;
|
||||||
|
tab.setAttribute("animate", "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="chrome://browser/content/zen-styles/zen-library.css"
|
||||||
|
/>
|
||||||
|
<hbox id="zen-library-panel">
|
||||||
|
<vbox id="zen-library-side">
|
||||||
|
<vbox id="zen-library-header"></vbox>
|
||||||
|
<vbox id="zen-library-sidebar-tabs">
|
||||||
|
${Object.values(this.zenLibrarySections).map(
|
||||||
|
Section => html`
|
||||||
|
<vbox
|
||||||
|
class="zen-library-tab"
|
||||||
|
?active=${this.activeTab === Section.id}
|
||||||
|
data-section=${Section.id}
|
||||||
|
@click=${event => {
|
||||||
|
if (this.activeTab !== Section.id) {
|
||||||
|
this.activeTab = Section.id;
|
||||||
|
this.#animateTabIcon(event.currentTarget);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="zen-library-tab-icon">
|
||||||
|
<div class="zen-library-tab-icon-image"></div>
|
||||||
|
</div>
|
||||||
|
<label data-l10n-id=${Section.label}></label>
|
||||||
|
</vbox>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</vbox>
|
||||||
|
<toolbar
|
||||||
|
id="zen-library-footer"
|
||||||
|
class="chromeclass-location"
|
||||||
|
mode="icons"
|
||||||
|
fullscreentoolbar="true"
|
||||||
|
></toolbar>
|
||||||
|
</vbox>
|
||||||
|
<vbox id="zen-library-content">
|
||||||
|
${this.activeSection.render(this)}
|
||||||
|
</vbox>
|
||||||
|
</hbox>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("zen-library", ZenLibrary);
|
||||||
347
src/zen/library/ZenLibraryDragAndDrop.mjs
Normal file
347
src/zen/library/ZenLibraryDragAndDrop.mjs
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
|
||||||
|
#section;
|
||||||
|
/** @type {{element: Element, dropBefore: boolean, intoFolder: boolean}|null} */
|
||||||
|
#target = null;
|
||||||
|
#highlightedLabel = null;
|
||||||
|
// The space whose theme the drag image currently wears, so it is only
|
||||||
|
// restyled when the pointer crosses into a different card.
|
||||||
|
#dragImageSpace = null;
|
||||||
|
|
||||||
|
constructor(section) {
|
||||||
|
super(section);
|
||||||
|
this.#section = section;
|
||||||
|
}
|
||||||
|
|
||||||
|
_targetForDrop(element) {
|
||||||
|
return this.#section.realElementFor(element) ?? element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} event
|
||||||
|
* @returns {Element|null} The dragged tab, or the label of the dragged
|
||||||
|
* split view, when it comes from this window
|
||||||
|
*/
|
||||||
|
#draggedItem(event) {
|
||||||
|
if (this.getDropEffectForTabDrag(event) !== "move") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const item = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
|
||||||
|
if (!item || item.documentGlobal !== window) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return gBrowser.isTab(item) || this.#isSplitViewLabel(item) ? item : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#isSplitViewLabel(item) {
|
||||||
|
return (
|
||||||
|
gBrowser.isTabGroupLabel(item) &&
|
||||||
|
!!item.group?.hasAttribute("split-view-group")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The tabs and split views a drag moves, from the drag data set on the
|
||||||
|
* dragged item.
|
||||||
|
*
|
||||||
|
* @param {Element} item - The dragged tab or split view label
|
||||||
|
* @returns {Element[]} Tabs and split view groups
|
||||||
|
*/
|
||||||
|
#movingElements(item) {
|
||||||
|
const moving = item._dragData?.movingTabs ?? [item];
|
||||||
|
return moving.map(element =>
|
||||||
|
gBrowser.isTabGroupLabel(element) ? element.group : element
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {DragEvent} event
|
||||||
|
* @param {Element} copy - The copied tab being dragged
|
||||||
|
*/
|
||||||
|
startTabDrag(event, copy) {
|
||||||
|
let item = this._targetForDrop(copy);
|
||||||
|
if (!gBrowser.isTab(item)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shown = copy;
|
||||||
|
if (item.group?.hasAttribute("split-view-group")) {
|
||||||
|
item = item.group.labelElement;
|
||||||
|
shown = copy.group;
|
||||||
|
}
|
||||||
|
const strip = gBrowser.tabContainer.tabDragAndDrop;
|
||||||
|
strip.startTabDrag(event, item, {
|
||||||
|
fromTabList: true,
|
||||||
|
dragImageSource: shown,
|
||||||
|
armLanding: this.#landingSupported,
|
||||||
|
});
|
||||||
|
this.#dragImageSpace = copy.closest(".zen-library-space")?.dataset.uuid;
|
||||||
|
this.#styleDragImage(strip, this.#dragImageSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
#styleDragImage(strip, uuid) {
|
||||||
|
const workspace = uuid && gZenWorkspaces.getWorkspaceFromId(uuid);
|
||||||
|
if (!workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const clone of strip.originalDragImageArgs?.[0]?.querySelectorAll(
|
||||||
|
"tab"
|
||||||
|
) ?? []) {
|
||||||
|
clone.toggleAttribute("visuallyselected", true);
|
||||||
|
clone.toggleAttribute("selected", true);
|
||||||
|
}
|
||||||
|
strip._recolorDragImage(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_dragover(event) {
|
||||||
|
const item = this.#draggedItem(event);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
this.#updateDragImageSpace(event);
|
||||||
|
this._handle_tabDragOverToSplit(event);
|
||||||
|
this.#updateDropTarget(event, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recolours the drag image to match the card under the pointer, so it reads
|
||||||
|
* as the space it would drop into.
|
||||||
|
*
|
||||||
|
* @param {DragEvent} event
|
||||||
|
*/
|
||||||
|
#updateDragImageSpace(event) {
|
||||||
|
const uuid = event.target.closest(".zen-library-space")?.dataset.uuid;
|
||||||
|
if (!uuid || uuid === this.#dragImageSpace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#dragImageSpace = uuid;
|
||||||
|
const strip = gBrowser.tabContainer.tabDragAndDrop;
|
||||||
|
this.#styleDragImage(strip, uuid);
|
||||||
|
strip._refreshDragImage(event.dataTransfer);
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_dragleave(event) {
|
||||||
|
if (!event.currentTarget.contains(event.relatedTarget)) {
|
||||||
|
this.clearDragOverVisuals();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_drop(event) {
|
||||||
|
const item = this.#draggedItem(event);
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
const target = this.#target;
|
||||||
|
const uuid = event.currentTarget.closest(".zen-library-space").dataset.uuid;
|
||||||
|
const moving = this.#movingElements(item);
|
||||||
|
const tabs = moving.flatMap(element =>
|
||||||
|
gBrowser.isTab(element) ? [element] : element.tabs
|
||||||
|
);
|
||||||
|
const fromElsewhere = tabs.filter(
|
||||||
|
tab => tab.getAttribute("zen-workspace-id") !== uuid
|
||||||
|
);
|
||||||
|
const strip = gBrowser.tabContainer.tabDragAndDrop;
|
||||||
|
const copyBefore = this.#section.copyForTab(tabs[0]);
|
||||||
|
const placeBefore = copyBefore && strip._placeOf(copyBefore);
|
||||||
|
if (fromElsewhere.length) {
|
||||||
|
gZenWorkspaces.moveTabsToWorkspace(fromElsewhere, uuid);
|
||||||
|
}
|
||||||
|
const split = this._handle_dropCreateSplit(event, { activate: false });
|
||||||
|
this.clearDragOverVisuals();
|
||||||
|
if (!split && target) {
|
||||||
|
const { element, dropBefore, intoFolder } = target;
|
||||||
|
if (intoFolder) {
|
||||||
|
element.addTabs(moving);
|
||||||
|
const firstExisting = element.tabs.find(tab => !tabs.includes(tab));
|
||||||
|
if (firstExisting) {
|
||||||
|
gBrowser.moveTabsBefore(moving, firstExisting);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pinned = gBrowser.isTab(element)
|
||||||
|
? element.pinned
|
||||||
|
: (element.tabs?.[0]?.pinned ?? element.pinned);
|
||||||
|
for (const tab of tabs) {
|
||||||
|
if (pinned && !tab.pinned) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
} else if (!pinned && tab.pinned) {
|
||||||
|
gBrowser.unpinTab(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dropBefore) {
|
||||||
|
gBrowser.moveTabsBefore(moving, element);
|
||||||
|
} else {
|
||||||
|
gBrowser.moveTabsAfter(moving, element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#land(tabs[0], placeBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
get #landingSupported() {
|
||||||
|
return AppConstants.platform === "macosx" && !gReduceMotion;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} tab - The dropped tab
|
||||||
|
* @param {string|null} placeBefore - Where its copy was before the drop;
|
||||||
|
* a copy still there stays in sight under the image
|
||||||
|
*/
|
||||||
|
#land(tab, placeBefore) {
|
||||||
|
if (!this.#landingSupported) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const copy = this.#section.beginTabLanding(tab);
|
||||||
|
if (copy) {
|
||||||
|
gBrowser.tabContainer.tabDragAndDrop._landDragImageOnElements(
|
||||||
|
[copy],
|
||||||
|
new Map([[copy, placeBefore]])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearDragOverVisuals(options) {
|
||||||
|
super.clearDragOverVisuals(options);
|
||||||
|
gZenFolders.highlightGroupOnDragOver(null);
|
||||||
|
this.#highlightedLabel?.removeAttribute("dragover");
|
||||||
|
this.#highlightedLabel = null;
|
||||||
|
this.#target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element|null} strip - A card's tab strip
|
||||||
|
* @returns {Element|null} The last tab or folder label shown in it
|
||||||
|
*/
|
||||||
|
#lastRow(strip) {
|
||||||
|
if (!strip) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rows = strip.querySelectorAll(
|
||||||
|
"tab:not([zen-empty-tab]):not([zen-glance-tab]), .tab-group-label-container"
|
||||||
|
);
|
||||||
|
for (let i = rows.length - 1; i >= 0; i--) {
|
||||||
|
if (rows[i].getBoundingClientRect().height > 0) {
|
||||||
|
return rows[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Works out what a drop at the pointer would do and shows it on the copies.
|
||||||
|
*
|
||||||
|
* @param {DragEvent} event
|
||||||
|
* @param {Element} item - The dragged tab or split view label
|
||||||
|
*/
|
||||||
|
#updateDropTarget(event, item) {
|
||||||
|
if (this._splitDropReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const movingTabs = item._dragData?.movingTabs ?? [item];
|
||||||
|
const moving = this.#movingElements(item);
|
||||||
|
let copy = event.target.closest("tab, .tab-group-label-container");
|
||||||
|
if (!copy) {
|
||||||
|
copy = this.#lastRow(event.target.closest(".zen-library-space-tabs"));
|
||||||
|
}
|
||||||
|
if (copy?.hasAttribute("zen-glance-tab")) {
|
||||||
|
copy = copy.parentElement.closest("tab");
|
||||||
|
}
|
||||||
|
if (copy?.classList.contains("tab-group-label-container")) {
|
||||||
|
copy = copy.parentElement;
|
||||||
|
} else if (copy?.group?.hasAttribute("split-view-group")) {
|
||||||
|
copy = copy.group;
|
||||||
|
}
|
||||||
|
const element = copy && this._targetForDrop(copy);
|
||||||
|
if (!element) {
|
||||||
|
// A card with nothing in it takes the drop at its start.
|
||||||
|
const strip = event.target.closest(".zen-library-space-tabs");
|
||||||
|
if (strip && !this.#lastRow(strip)) {
|
||||||
|
this.#target = null;
|
||||||
|
gZenFolders.highlightGroupOnDragOver(null);
|
||||||
|
this.#placeIndicatorIn(strip, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.clearDragOverVisuals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (moving.some(moved => moved === element || moved.contains(element))) {
|
||||||
|
this.clearDragOverVisuals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isFolder = !!element.isZenFolder;
|
||||||
|
const box = isFolder ? copy.labelContainerElement : copy;
|
||||||
|
const rect = box.getBoundingClientRect();
|
||||||
|
const overlap = (event.clientY - rect.top) / rect.height;
|
||||||
|
let dropBefore;
|
||||||
|
let intoFolder = false;
|
||||||
|
if (isFolder) {
|
||||||
|
const threshold =
|
||||||
|
Services.prefs.getIntPref(
|
||||||
|
"zen.tabs.folder-dragover-threshold-percent"
|
||||||
|
) / 100;
|
||||||
|
if (overlap < threshold) {
|
||||||
|
dropBefore = true;
|
||||||
|
} else if (
|
||||||
|
overlap > 1 - threshold &&
|
||||||
|
(copy.collapsed || element.childGroupsAndTabs.length < 2)
|
||||||
|
) {
|
||||||
|
dropBefore = false;
|
||||||
|
} else {
|
||||||
|
intoFolder = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dropBefore = this._dropsBefore(event, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = this.#target;
|
||||||
|
this.#target = { element, dropBefore, intoFolder };
|
||||||
|
const changed =
|
||||||
|
previous?.element !== element ||
|
||||||
|
previous.dropBefore !== dropBefore ||
|
||||||
|
previous.intoFolder !== intoFolder;
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (previous) {
|
||||||
|
// eslint-disable-next-line mozilla/valid-services
|
||||||
|
Services.zen.playHapticFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#highlightedLabel?.removeAttribute("dragover");
|
||||||
|
this.#highlightedLabel = null;
|
||||||
|
if (intoFolder) {
|
||||||
|
gZenPinnedTabManager.removeTabContainersDragoverClass();
|
||||||
|
gZenFolders.highlightGroupOnDragOver(copy, movingTabs);
|
||||||
|
this.#highlightedLabel = box;
|
||||||
|
box.setAttribute("dragover", "true");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gZenFolders.highlightGroupOnDragOver(null);
|
||||||
|
const strip = copy.closest(".zen-library-space-tabs");
|
||||||
|
this.#placeIndicatorIn(
|
||||||
|
strip,
|
||||||
|
(dropBefore ? rect.top : rect.bottom) - strip.getBoundingClientRect().top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} strip - A card's tab strip
|
||||||
|
* @param {number} offset - Where in the strip's view the drop would go
|
||||||
|
*/
|
||||||
|
#placeIndicatorIn(strip, offset) {
|
||||||
|
const inset = 14;
|
||||||
|
this._placeDropIndicator({
|
||||||
|
parent: strip,
|
||||||
|
left: inset,
|
||||||
|
width: strip.getBoundingClientRect().width - 2 * inset,
|
||||||
|
top: offset + strip.scrollTop,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
611
src/zen/library/ZenLibraryWidget.sys.mjs
Normal file
611
src/zen/library/ZenLibraryWidget.sys.mjs
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
DownloadsCommon:
|
||||||
|
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||||
|
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||||
|
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||||
|
DownloadsViewUI:
|
||||||
|
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||||
|
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
const ENTRIES = 4;
|
||||||
|
const CLOSE_DELAY_MS = 200;
|
||||||
|
const FILE_MIME = "application/x-moz-file";
|
||||||
|
const BADGE_MARKUP = `
|
||||||
|
<span class="zen-library-download-badge no-squircles">
|
||||||
|
<span class="zen-library-download-progress no-squircles"></span>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
|
||||||
|
class ZenLibraryDownloadStack {
|
||||||
|
#button;
|
||||||
|
#badge;
|
||||||
|
#list;
|
||||||
|
#library = null;
|
||||||
|
#entries = [];
|
||||||
|
#data;
|
||||||
|
#downloads = [];
|
||||||
|
#inBatch = false;
|
||||||
|
#loaded = false;
|
||||||
|
#recentNewDownload = false;
|
||||||
|
#closeTimer = null;
|
||||||
|
#contextMenuOpen = false;
|
||||||
|
#secondsLeft = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} button - The library toolbar button
|
||||||
|
*/
|
||||||
|
constructor(button) {
|
||||||
|
this.#button = button;
|
||||||
|
button.setAttribute("command", "cmd_zenToggleLibrary");
|
||||||
|
button.classList.add("toolbarbutton-badge-stack-host");
|
||||||
|
button.appendChild(
|
||||||
|
this.#parse(
|
||||||
|
`<box class="toolbarbutton-badge-stack">${BADGE_MARKUP}</box>`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.#badge = button.querySelector(".zen-library-download-badge");
|
||||||
|
this.#badge.id = "library-button-badge";
|
||||||
|
this.#list = this.#buildList();
|
||||||
|
|
||||||
|
button.addEventListener("mouseenter", this);
|
||||||
|
button.addEventListener("mouseleave", this);
|
||||||
|
this.#list.addEventListener("mouseenter", this);
|
||||||
|
this.#list.addEventListener("mouseleave", this);
|
||||||
|
|
||||||
|
this.#data = lazy.DownloadsCommon.getData(this.#window, true);
|
||||||
|
this.#data.addView(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.#data.removeView(this);
|
||||||
|
this.#window.clearTimeout(this.#closeTimer);
|
||||||
|
this.#list.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
get #window() {
|
||||||
|
return this.#button.documentGlobal;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #footButtons() {
|
||||||
|
return this.#list.parentElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The tab strip, once the window has it. */
|
||||||
|
get #tabs() {
|
||||||
|
return this.#window.gBrowser?.tabContainer ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#parse(markup) {
|
||||||
|
return this.#window.MozXULElement.parseXULToFragment(markup);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvent(event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case "mouseenter":
|
||||||
|
this.#window.clearTimeout(this.#closeTimer);
|
||||||
|
if (event.currentTarget === this.#button) {
|
||||||
|
this.#open();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "mouseleave":
|
||||||
|
this.#scheduleClose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#scheduleClose() {
|
||||||
|
this.#window.clearTimeout(this.#closeTimer);
|
||||||
|
this.#closeTimer = this.#window.setTimeout(() => {
|
||||||
|
if (!this.#contextMenuOpen) {
|
||||||
|
this.#close();
|
||||||
|
}
|
||||||
|
}, CLOSE_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#open() {
|
||||||
|
if (this.#footButtons.hasAttribute("zen-library-stack-open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#aimBadge();
|
||||||
|
this.#tabs?.removeAttribute("zen-library-stack-closing");
|
||||||
|
for (const host of [this.#footButtons, this.#tabs]) {
|
||||||
|
host?.setAttribute("zen-library-stack-open", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#close() {
|
||||||
|
if (!this.#footButtons.hasAttribute("zen-library-stack-open")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#recentNewDownload = false;
|
||||||
|
this.#updateBadgeShowing();
|
||||||
|
for (const host of [this.#footButtons, this.#tabs]) {
|
||||||
|
host?.removeAttribute("zen-library-stack-open");
|
||||||
|
}
|
||||||
|
// The strip's fade stays until its progress is back at zero.
|
||||||
|
const tabs = this.#tabs;
|
||||||
|
if (!tabs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tabs.setAttribute("zen-library-stack-closing", "true");
|
||||||
|
tabs.addEventListener("transitionend", function onEnd(event) {
|
||||||
|
if (event.propertyName === "--zen-library-progress") {
|
||||||
|
tabs.removeEventListener("transitionend", onEnd);
|
||||||
|
tabs.removeAttribute("zen-library-stack-closing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points the button's badge at the newest entry's badge, where it flies
|
||||||
|
* to as the list opens, from wherever it is right now.
|
||||||
|
*/
|
||||||
|
#aimBadge() {
|
||||||
|
const entry = this.#entries.at(-1);
|
||||||
|
if (entry.hidden) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = entry.querySelector(".zen-library-download-badge");
|
||||||
|
const from = this.#badge.getBoundingClientRect();
|
||||||
|
const to = target.getBoundingClientRect();
|
||||||
|
const style = this.#window.getComputedStyle(this.#badge);
|
||||||
|
// The entry is still translated down while closed; land where it ends.
|
||||||
|
const entryTransform = this.#window.getComputedStyle(entry).transform;
|
||||||
|
const rise =
|
||||||
|
entryTransform === "none"
|
||||||
|
? 0
|
||||||
|
: new this.#window.DOMMatrixReadOnly(entryTransform).f;
|
||||||
|
this.#badge.style.setProperty(
|
||||||
|
"--zen-library-badge-to-x",
|
||||||
|
`${parseFloat(style.left) + to.left - from.left}px`
|
||||||
|
);
|
||||||
|
this.#badge.style.setProperty(
|
||||||
|
"--zen-library-badge-to-y",
|
||||||
|
`${parseFloat(style.top) + to.top - from.top - rise}px`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildList() {
|
||||||
|
const document = this.#window.document;
|
||||||
|
const footButtons = document.getElementById("zen-sidebar-foot-buttons");
|
||||||
|
// Not a widget of the toolbar, so its customization leaves it alone.
|
||||||
|
footButtons.appendChild(
|
||||||
|
this.#parse(
|
||||||
|
`<box id="zen-library-download-list" skipintoolbarset="true"></box>`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const list = document.getElementById("zen-library-download-list");
|
||||||
|
for (let i = 0; i < ENTRIES; i++) {
|
||||||
|
list.appendChild(
|
||||||
|
this.#parse(`
|
||||||
|
<div class="zen-library-download-list-download">
|
||||||
|
${BADGE_MARKUP}
|
||||||
|
<vbox class="zen-library-download-list-title-container">
|
||||||
|
<span class="zen-library-download-list-title"></span>
|
||||||
|
<span class="zen-library-download-list-subtitle"></span>
|
||||||
|
</vbox>
|
||||||
|
<toolbarbutton class="toolbarbutton-1 zen-library-download-action no-squircles" />
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.#entries = [...list.children];
|
||||||
|
for (const entry of this.#entries) {
|
||||||
|
entry.setAttribute("draggable", "true");
|
||||||
|
entry.addEventListener("dragstart", event =>
|
||||||
|
this.#onDragStart(event, entry.download)
|
||||||
|
);
|
||||||
|
entry.addEventListener("click", event => {
|
||||||
|
if (event.button === 0) {
|
||||||
|
this.#openDownload(entry.download);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
entry.addEventListener("contextmenu", event =>
|
||||||
|
this.#showContextMenu(event, entry.download)
|
||||||
|
);
|
||||||
|
const action = entry.querySelector(".zen-library-download-action");
|
||||||
|
action.addEventListener("click", event => event.stopPropagation());
|
||||||
|
action.addEventListener("command", () =>
|
||||||
|
this.#cancelDownload(entry.download)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a download under way, partial file and all.
|
||||||
|
*
|
||||||
|
* @param {Download} download
|
||||||
|
*/
|
||||||
|
#cancelDownload(download) {
|
||||||
|
if (!download || download.stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
download.cancel().catch(() => {});
|
||||||
|
download
|
||||||
|
.removePartialData()
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => download.target.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
#openDownload(download) {
|
||||||
|
if (download.succeeded) {
|
||||||
|
lazy.DownloadsCommon.openDownload(download).catch(console.error);
|
||||||
|
} else if (download.source?.url) {
|
||||||
|
this.#window.openTrustedLinkIn(download.source.url, "tab");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onDragStart(event, download) {
|
||||||
|
if (!download?.succeeded || download.deleted || !download.target?.exists) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = new lazy.FileUtils.File(download.target.path);
|
||||||
|
const { dataTransfer } = event;
|
||||||
|
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
|
||||||
|
dataTransfer.effectAllowed = "copyMove";
|
||||||
|
dataTransfer.setData("text/uri-list", Services.io.newFileURI(file).spec);
|
||||||
|
dataTransfer.addElement(event.currentTarget);
|
||||||
|
// eslint-disable-next-line mozilla/valid-services
|
||||||
|
Services.zen.playHapticFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateList() {
|
||||||
|
const shown = this.#downloads.slice(-ENTRIES);
|
||||||
|
const unused = ENTRIES - shown.length;
|
||||||
|
this.#entries.forEach((entry, i) => {
|
||||||
|
const download = shown[i - unused];
|
||||||
|
entry.hidden = !download;
|
||||||
|
entry.download = download ?? null;
|
||||||
|
if (!download) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#updateBadge(
|
||||||
|
entry.querySelector(".zen-library-download-badge"),
|
||||||
|
download
|
||||||
|
);
|
||||||
|
entry.querySelector(".zen-library-download-list-title").textContent =
|
||||||
|
this.#fileName(download);
|
||||||
|
entry.querySelector(".zen-library-download-list-subtitle").textContent =
|
||||||
|
this.#statusText(download);
|
||||||
|
entry.toggleAttribute("downloading", !download.stopped);
|
||||||
|
});
|
||||||
|
this.#tabs?.style.setProperty(
|
||||||
|
"--zen-library-stack-height",
|
||||||
|
`${this.#list.getBoundingClientRect().height}px`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateBadge(badge, download) {
|
||||||
|
const pending = this.#isPending(download);
|
||||||
|
badge.toggleAttribute("downloading", pending);
|
||||||
|
if (badge === this.#badge) {
|
||||||
|
badge.parentElement.toggleAttribute("downloading", pending);
|
||||||
|
}
|
||||||
|
if (download) {
|
||||||
|
badge.style.setProperty(
|
||||||
|
"--download-image",
|
||||||
|
`url('${this.#iconUrl(download)}')`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
badge
|
||||||
|
.querySelector(".zen-library-download-progress")
|
||||||
|
.style.setProperty(
|
||||||
|
"--value",
|
||||||
|
download?.hasProgress ? download.progress : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateBadgeShowing() {
|
||||||
|
this.#footButtons.toggleAttribute(
|
||||||
|
"zen-library-badge",
|
||||||
|
this.#isPending(this.#downloads.at(-1)) || this.#recentNewDownload
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadList view
|
||||||
|
|
||||||
|
onDownloadBatchStarting() {
|
||||||
|
this.#inBatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadBatchEnded() {
|
||||||
|
this.#inBatch = false;
|
||||||
|
this.#loaded = true;
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadAdded(download, { insertBefore } = {}) {
|
||||||
|
const index = insertBefore ? this.#downloads.indexOf(insertBefore) : -1;
|
||||||
|
if (index === -1) {
|
||||||
|
this.#downloads.push(download);
|
||||||
|
} else {
|
||||||
|
this.#downloads.splice(index, 0, download);
|
||||||
|
}
|
||||||
|
if (this.#loaded) {
|
||||||
|
this.#recentNewDownload = true;
|
||||||
|
}
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadChanged() {
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadRemoved(download) {
|
||||||
|
const index = this.#downloads.indexOf(download);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.#downloads.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.#update();
|
||||||
|
}
|
||||||
|
|
||||||
|
#update() {
|
||||||
|
if (this.#inBatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newest = this.#downloads.at(-1);
|
||||||
|
this.#updateList();
|
||||||
|
this.#updateBadge(this.#badge, newest);
|
||||||
|
this.#updateBadgeShowing();
|
||||||
|
this.#applyDownloadState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the newest download's state on the library while it is open, for
|
||||||
|
* its downloads tab to show.
|
||||||
|
*
|
||||||
|
* @param {Element} library
|
||||||
|
*/
|
||||||
|
attachLibrary(library) {
|
||||||
|
this.#library = library;
|
||||||
|
this.#applyDownloadState();
|
||||||
|
}
|
||||||
|
|
||||||
|
detachLibrary(library) {
|
||||||
|
if (this.#library === library) {
|
||||||
|
this.#library = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#applyDownloadState() {
|
||||||
|
if (!this.#library) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newest = this.#downloads.at(-1);
|
||||||
|
this.#library.toggleAttribute(
|
||||||
|
"zen-library-downloading",
|
||||||
|
this.#isPending(newest)
|
||||||
|
);
|
||||||
|
this.#library.style.setProperty(
|
||||||
|
"--zen-library-download-progress",
|
||||||
|
`${newest?.hasProgress ? Math.round(newest.progress) : 0}%`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
#fileName(download) {
|
||||||
|
return download.target.path
|
||||||
|
? PathUtils.filename(download.target.path)
|
||||||
|
: download.source.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
#isPending(download) {
|
||||||
|
return (
|
||||||
|
!!download &&
|
||||||
|
(!download.stopped || (download.canceled && download.hasPartialData))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#joinStatus(...parts) {
|
||||||
|
return parts
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((a, b) => lazy.DownloadsCommon.strings.statusSeparator(a, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#iconUrl(download) {
|
||||||
|
if (!download.target.path) {
|
||||||
|
return "moz-icon://.unknown?size=32";
|
||||||
|
}
|
||||||
|
return `moz-icon://${download.target.path}?size=32${
|
||||||
|
download.succeeded ? "&state=normal" : ""
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusText(download) {
|
||||||
|
const strings = lazy.DownloadsCommon.strings;
|
||||||
|
const totalBytes = download.hasProgress ? download.totalBytes : -1;
|
||||||
|
if (!download.stopped) {
|
||||||
|
const [statusText, secondsLeft] = lazy.DownloadUtils.getDownloadStatus(
|
||||||
|
download.currentBytes,
|
||||||
|
totalBytes,
|
||||||
|
download.speed,
|
||||||
|
this.#secondsLeft.get(download) ?? Infinity
|
||||||
|
);
|
||||||
|
this.#secondsLeft.set(download, secondsLeft);
|
||||||
|
return statusText;
|
||||||
|
}
|
||||||
|
this.#secondsLeft.delete(download);
|
||||||
|
if (download.deleted) {
|
||||||
|
return strings.fileDeleted;
|
||||||
|
}
|
||||||
|
if (download.succeeded) {
|
||||||
|
if (!download.target.exists) {
|
||||||
|
return strings.fileMovedOrMissing;
|
||||||
|
}
|
||||||
|
const parsed = URL.parse(download.source.url);
|
||||||
|
const uri = parsed && Services.io.newURI(parsed.href);
|
||||||
|
const host = uri
|
||||||
|
? lazy.BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true })
|
||||||
|
: "";
|
||||||
|
const [date] = lazy.DownloadUtils.getReadableDates(
|
||||||
|
new Date(download.endTime)
|
||||||
|
);
|
||||||
|
return this.#joinStatus(
|
||||||
|
lazy.DownloadsViewUI.getSizeWithUnits(download),
|
||||||
|
host,
|
||||||
|
date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (download.canceled && download.hasPartialData) {
|
||||||
|
return this.#joinStatus(
|
||||||
|
strings.statePaused,
|
||||||
|
lazy.DownloadUtils.getTransferTotal(download.currentBytes, totalBytes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (download.error?.becauseBlockedByParentalControls) {
|
||||||
|
return strings.stateBlockedParentalControls;
|
||||||
|
}
|
||||||
|
if (download.error?.becauseBlockedByReputationCheck) {
|
||||||
|
return strings.blockedMalware;
|
||||||
|
}
|
||||||
|
return download.canceled ? strings.stateCanceled : strings.stateFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#contextMenuItems(download) {
|
||||||
|
const C = lazy.DownloadsCommon;
|
||||||
|
const state = C.stateOfDownload(download);
|
||||||
|
const isActive =
|
||||||
|
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_PAUSED;
|
||||||
|
const fileExists =
|
||||||
|
state === C.DOWNLOAD_FINISHED &&
|
||||||
|
download.target?.exists !== false &&
|
||||||
|
!download.deleted;
|
||||||
|
const sourceUrl = download.source?.originalUrl || download.source?.url;
|
||||||
|
const items = [];
|
||||||
|
if (state === C.DOWNLOAD_DOWNLOADING) {
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-pause",
|
||||||
|
onClick: () => download.cancel().catch(() => {}),
|
||||||
|
});
|
||||||
|
} else if (state === C.DOWNLOAD_PAUSED) {
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-resume",
|
||||||
|
onClick: () => download.start?.().catch(() => {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (fileExists) {
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-show-menuitem-2",
|
||||||
|
onClick: () =>
|
||||||
|
C.showDownloadedFile(new lazy.FileUtils.File(download.target.path)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sourceUrl) {
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-go-to-download-page",
|
||||||
|
onClick: () => this.#window.openTrustedLinkIn(sourceUrl, "tab"),
|
||||||
|
});
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-copy-download-link",
|
||||||
|
onClick: () =>
|
||||||
|
Cc["@mozilla.org/widget/clipboardhelper;1"]
|
||||||
|
.getService(Ci.nsIClipboardHelper)
|
||||||
|
.copyString(sourceUrl),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items.push({ separator: true });
|
||||||
|
if (fileExists) {
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-delete-file",
|
||||||
|
onClick: () =>
|
||||||
|
C.deleteDownloadFiles(
|
||||||
|
download,
|
||||||
|
lazy.DownloadsViewUI.clearHistoryOnDelete
|
||||||
|
).catch(console.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!isActive) {
|
||||||
|
items.push({
|
||||||
|
l10nId: "downloads-cmd-remove-from-history",
|
||||||
|
onClick: () => C.deleteDownload(download).catch(console.error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
#showContextMenu(event, download) {
|
||||||
|
if (!download) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const document = this.#window.document;
|
||||||
|
const popup = document.createXULElement("menupopup");
|
||||||
|
for (const item of this.#contextMenuItems(download)) {
|
||||||
|
if (item.separator) {
|
||||||
|
if (popup.lastChild && popup.lastChild.tagName !== "menuseparator") {
|
||||||
|
popup.appendChild(document.createXULElement("menuseparator"));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const menuitem = document.createXULElement("menuitem");
|
||||||
|
menuitem.setAttribute("data-l10n-id", item.l10nId);
|
||||||
|
menuitem.addEventListener(
|
||||||
|
"command",
|
||||||
|
() => {
|
||||||
|
try {
|
||||||
|
item.onClick();
|
||||||
|
} catch (ex) {
|
||||||
|
console.error(ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
popup.appendChild(menuitem);
|
||||||
|
}
|
||||||
|
if (popup.lastChild?.tagName === "menuseparator") {
|
||||||
|
popup.lastChild.remove();
|
||||||
|
}
|
||||||
|
if (!popup.childElementCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#contextMenuOpen = true;
|
||||||
|
popup.addEventListener(
|
||||||
|
"popuphidden",
|
||||||
|
() => {
|
||||||
|
this.#contextMenuOpen = false;
|
||||||
|
popup.remove();
|
||||||
|
this.#scheduleClose();
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
document.getElementById("mainPopupSet").appendChild(popup);
|
||||||
|
popup.openPopupAtScreen(event.screenX, event.screenY, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stacks = new WeakMap();
|
||||||
|
|
||||||
|
export const ZenLibraryWidget = {
|
||||||
|
id: "zen-library-button",
|
||||||
|
l10nId: "zen-library-button",
|
||||||
|
_introducedByPref: "zen.library.enabled",
|
||||||
|
|
||||||
|
onCreated(node) {
|
||||||
|
stacks.set(node.ownerDocument, new ZenLibraryDownloadStack(node));
|
||||||
|
},
|
||||||
|
|
||||||
|
onDestroyed(document) {
|
||||||
|
stacks.get(document)?.destroy();
|
||||||
|
stacks.delete(document);
|
||||||
|
},
|
||||||
|
|
||||||
|
attachLibrary(library) {
|
||||||
|
stacks.get(library.ownerDocument)?.attachLibrary(library);
|
||||||
|
},
|
||||||
|
|
||||||
|
detachLibrary(library) {
|
||||||
|
stacks.get(library.ownerDocument)?.detachLibrary(library);
|
||||||
|
},
|
||||||
|
};
|
||||||
5
src/zen/library/jar.inc.mn
Normal file
5
src/zen/library/jar.inc.mn
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
content/browser/zen-styles/zen-library.css (../../zen/library/zen-library.css)
|
||||||
16
src/zen/library/moz.build
Normal file
16
src/zen/library/moz.build
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 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",
|
||||||
|
"ZenLibraryWidget.sys.mjs",
|
||||||
|
]
|
||||||
278
src/zen/library/sections/ZenLibraryBoostsSection.mjs
Normal file
278
src/zen/library/sections/ZenLibraryBoostsSection.mjs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { html, repeat } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import { ZenLibrarySearchSection } from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||||
|
|
||||||
|
let lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
gZenBoostsManager: "resource:///modules/zen/boosts/ZenBoostsManager.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
const BOOST_TOPICS = ["zen-boosts-update", "zen-boosts-active-change"];
|
||||||
|
|
||||||
|
const boostKey = boost => `${boost.domain}/${boost.id}`;
|
||||||
|
|
||||||
|
export class ZenLibraryBoostsSection extends ZenLibrarySearchSection {
|
||||||
|
static id = "boosts";
|
||||||
|
static label = "library-boosts-section-title";
|
||||||
|
|
||||||
|
static render(library) {
|
||||||
|
return html`
|
||||||
|
<zen-library-boosts-section
|
||||||
|
class="zen-library-section"
|
||||||
|
data-section="boosts"
|
||||||
|
.library=${library}
|
||||||
|
></zen-library-boosts-section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#observer = { observe: () => this.requestUpdate() };
|
||||||
|
#glanceBrowser = null;
|
||||||
|
#editor = null;
|
||||||
|
#menu = null;
|
||||||
|
#menuBoost = null;
|
||||||
|
#menuRow = null;
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
for (const topic of BOOST_TOPICS) {
|
||||||
|
Services.obs.addObserver(this.#observer, topic);
|
||||||
|
}
|
||||||
|
this.#menu = this.#buildMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
for (const topic of BOOST_TOPICS) {
|
||||||
|
Services.obs.removeObserver(this.#observer, topic);
|
||||||
|
}
|
||||||
|
this.#menu?.hidePopup();
|
||||||
|
this.#menu?.remove();
|
||||||
|
this.#menu = null;
|
||||||
|
this.#glanceBrowser = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchPlaceholderL10nId() {
|
||||||
|
return "library-boosts-search-placeholder";
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChanged() {
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#boosts() {
|
||||||
|
const boosts = [];
|
||||||
|
const query = this.searchQuery.toLowerCase();
|
||||||
|
for (const [domain, entry] of lazy.gZenBoostsManager.registeredDomains) {
|
||||||
|
for (const [id, boostEntry] of entry.boostEntries) {
|
||||||
|
const { boostData } = boostEntry;
|
||||||
|
if (!boostData.changeWasMade) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
query &&
|
||||||
|
!boostData.boostName.toLowerCase().includes(query) &&
|
||||||
|
!domain.toLowerCase().includes(query)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
boosts.push({
|
||||||
|
id,
|
||||||
|
domain,
|
||||||
|
name: boostData.boostName,
|
||||||
|
enabled: entry.activeBoostId === id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return boosts.sort(
|
||||||
|
(a, b) => a.name.localeCompare(b.name) || a.domain.localeCompare(b.domain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
#toggle(boost) {
|
||||||
|
lazy.gZenBoostsManager.toggleBoostActiveForDomain(boost.domain, boost.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#onRowClick(boost, row) {
|
||||||
|
if (boost.enabled) {
|
||||||
|
this.#edit(boost, row);
|
||||||
|
} else {
|
||||||
|
this.#toggle(boost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #edit(boost, row) {
|
||||||
|
if (this.#glanceBrowser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `https://${boost.domain}/`;
|
||||||
|
const uri = Services.io.newURI(url);
|
||||||
|
if (!lazy.gZenBoostsManager.canBoostSite(uri)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rowRect = row.getBoundingClientRect();
|
||||||
|
const glance = await gZenGlanceManager.openDetachedGlance({
|
||||||
|
url,
|
||||||
|
clientX: rowRect.left + rowRect.width / 2,
|
||||||
|
clientY: rowRect.top + rowRect.height / 2,
|
||||||
|
userContextId: gZenWorkspaces.getActiveWorkspace()?.containerTabId,
|
||||||
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
});
|
||||||
|
if (!glance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { browser } = glance;
|
||||||
|
this.#glanceBrowser = browser;
|
||||||
|
glance.closed.then(() => {
|
||||||
|
this.#glanceBrowser = null;
|
||||||
|
this.#editor?.close();
|
||||||
|
this.#editor = null;
|
||||||
|
});
|
||||||
|
await this.#whenNavigated(browser);
|
||||||
|
if (this.#glanceBrowser !== browser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stored = lazy.gZenBoostsManager.loadBoostFromStore(
|
||||||
|
boost.domain,
|
||||||
|
boost.id
|
||||||
|
);
|
||||||
|
this.#editor = lazy.gZenBoostsManager.openBoostWindow(window, stored, uri, {
|
||||||
|
browser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#whenNavigated(browser) {
|
||||||
|
if (browser.currentURI?.spec !== "about:blank") {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const listener = {
|
||||||
|
QueryInterface: ChromeUtils.generateQI([
|
||||||
|
"nsIWebProgressListener",
|
||||||
|
"nsISupportsWeakReference",
|
||||||
|
]),
|
||||||
|
onLocationChange(webProgress) {
|
||||||
|
if (webProgress.isTopLevel) {
|
||||||
|
browser.removeProgressListener(listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
browser.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_LOCATION);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #export(boost) {
|
||||||
|
const { boostEntry } = lazy.gZenBoostsManager.loadBoostFromStore(
|
||||||
|
boost.domain,
|
||||||
|
boost.id
|
||||||
|
);
|
||||||
|
await lazy.gZenBoostsManager.exportBoost(window, boostEntry.boostData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete(boost) {
|
||||||
|
lazy.gZenBoostsManager.deleteBoost({ domain: boost.domain, id: boost.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
|
||||||
|
#buildMenu() {
|
||||||
|
const menu = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<menupopup class="zen-library-boosts-menu">
|
||||||
|
<menuitem data-action="edit" data-l10n-id="library-boosts-menu-edit"/>
|
||||||
|
<menuitem data-action="export" data-l10n-id="zen-boost-save"/>
|
||||||
|
<menuseparator/>
|
||||||
|
<menuitem data-action="delete" data-l10n-id="zen-boost-edit-delete"/>
|
||||||
|
</menupopup>
|
||||||
|
`).firstElementChild;
|
||||||
|
menu.addEventListener("command", event => {
|
||||||
|
const boost = this.#menuBoost;
|
||||||
|
const row = this.#menuRow;
|
||||||
|
if (!boost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (event.target.dataset.action) {
|
||||||
|
case "edit":
|
||||||
|
this.#edit(boost, row);
|
||||||
|
break;
|
||||||
|
case "export":
|
||||||
|
this.#export(boost);
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
this.#delete(boost);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.addEventListener("popuphidden", () => {
|
||||||
|
this.#menuRow?.removeAttribute("menu-open");
|
||||||
|
this.#menuRow = null;
|
||||||
|
this.#menuBoost = null;
|
||||||
|
});
|
||||||
|
document.getElementById("mainPopupSet").appendChild(menu);
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openMenu(boost, row, event) {
|
||||||
|
this.#menuRow?.removeAttribute("menu-open");
|
||||||
|
this.#menuBoost = boost;
|
||||||
|
this.#menuRow = row;
|
||||||
|
row.setAttribute("menu-open", "true");
|
||||||
|
this.#menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
|
||||||
|
#renderBoost(boost) {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="zen-library-row zen-library-boost-row"
|
||||||
|
?disabled=${!boost.enabled}
|
||||||
|
@click=${event => this.#onRowClick(boost, event.currentTarget)}
|
||||||
|
@contextmenu=${event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#openMenu(boost, event.currentTarget, event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="zen-library-boost-icon 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);
|
||||||
664
src/zen/library/sections/ZenLibraryDownloadsSection.mjs
Normal file
664
src/zen/library/sections/ZenLibraryDownloadsSection.mjs
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { html, repeat, when } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import {
|
||||||
|
MS_PER_DAY,
|
||||||
|
PAGE_SIZE,
|
||||||
|
ZenLibrarySearchSection,
|
||||||
|
whenFilterGroup,
|
||||||
|
} from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||||
|
|
||||||
|
let lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||||
|
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||||
|
DownloadsCommon:
|
||||||
|
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||||
|
DownloadsViewUI:
|
||||||
|
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
|
||||||
|
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
const FILE_MIME = "application/x-moz-file";
|
||||||
|
const OPENING_FEEDBACK_MS = 1500;
|
||||||
|
|
||||||
|
const FILE_TYPES = {
|
||||||
|
images: "png jpg jpeg gif webp svg bmp tif tiff heic heif avif ico",
|
||||||
|
video: "mp4 mkv mov avi webm m4v wmv flv mpg mpeg",
|
||||||
|
audio: "mp3 wav flac aac ogg oga m4a opus wma aiff",
|
||||||
|
documents:
|
||||||
|
"pdf doc docx xls xlsx ppt pptx txt md rtf odt ods odp csv epub pages numbers key",
|
||||||
|
archives: "zip rar 7z tar gz bz2 xz tgz zst",
|
||||||
|
apps: "dmg pkg exe msi app deb rpm appimage apk jar",
|
||||||
|
};
|
||||||
|
const EXTENSION_TYPES = new Map();
|
||||||
|
for (const [type, extensions] of Object.entries(FILE_TYPES)) {
|
||||||
|
for (const extension of extensions.split(" ")) {
|
||||||
|
EXTENSION_TYPES.set(extension, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||||
|
static id = "downloads";
|
||||||
|
static label = "library-downloads-section-title";
|
||||||
|
|
||||||
|
static render(library) {
|
||||||
|
return html`
|
||||||
|
<zen-library-downloads-section
|
||||||
|
class="zen-library-section"
|
||||||
|
data-section="downloads"
|
||||||
|
.library=${library}
|
||||||
|
></zen-library-downloads-section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Oldest to newest, mirroring the order of the underlying download list.
|
||||||
|
#downloads = [];
|
||||||
|
#data = null;
|
||||||
|
#loaded = false;
|
||||||
|
#inBatch = false;
|
||||||
|
#limit = PAGE_SIZE;
|
||||||
|
#visible = [];
|
||||||
|
#refreshed = new WeakSet();
|
||||||
|
#secondsLeft = new WeakMap();
|
||||||
|
|
||||||
|
#menu = null;
|
||||||
|
#menuDownload = null;
|
||||||
|
#menuRow = null;
|
||||||
|
#openingDownload = null;
|
||||||
|
#openingTimer = null;
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.#data = lazy.DownloadsCommon.getData(window, true);
|
||||||
|
this.#data.addView(this);
|
||||||
|
this.#menu = this.#buildMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.#data?.removeView(this);
|
||||||
|
this.#data = null;
|
||||||
|
this.#menu?.hidePopup();
|
||||||
|
this.#menu?.remove();
|
||||||
|
this.#menu = null;
|
||||||
|
clearTimeout(this.#openingTimer);
|
||||||
|
this.#openingTimer = null;
|
||||||
|
this.#openingDownload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchPlaceholderL10nId() {
|
||||||
|
return "places-search-downloads";
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterTitleL10nId() {
|
||||||
|
return "library-downloads-filter-title";
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterGroups() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "type",
|
||||||
|
titleL10nId: "library-downloads-filter-type",
|
||||||
|
options: Object.keys(FILE_TYPES).map(id => ({
|
||||||
|
id,
|
||||||
|
l10nId: `library-downloads-type-${id}`,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
whenFilterGroup("library-downloads-filter-when"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChanged() {
|
||||||
|
this.#limit = PAGE_SIZE;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFiltersChanged() {
|
||||||
|
this.#limit = PAGE_SIZE;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onListScrolledToEnd() {
|
||||||
|
if (this.#visible.length < this.#limit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#limit += PAGE_SIZE;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadList view
|
||||||
|
|
||||||
|
onDownloadBatchStarting() {
|
||||||
|
this.#inBatch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadBatchEnded() {
|
||||||
|
this.#inBatch = false;
|
||||||
|
this.#loaded = true;
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadAdded(download, { insertBefore } = {}) {
|
||||||
|
const index = insertBefore ? this.#downloads.indexOf(insertBefore) : -1;
|
||||||
|
if (index === -1) {
|
||||||
|
this.#downloads.push(download);
|
||||||
|
} else {
|
||||||
|
this.#downloads.splice(index, 0, download);
|
||||||
|
}
|
||||||
|
this.#scheduleUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadChanged() {
|
||||||
|
this.#scheduleUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
onDownloadRemoved(download) {
|
||||||
|
const index = this.#downloads.indexOf(download);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.#downloads.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (this.#menuDownload === download) {
|
||||||
|
this.#menu?.hidePopup();
|
||||||
|
}
|
||||||
|
this.#scheduleUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#scheduleUpdate() {
|
||||||
|
if (!this.#inBatch) {
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
// Check that the files of rendered downloads still exist, like the
|
||||||
|
// downloads panel does when it opens.
|
||||||
|
for (const download of this.#visible) {
|
||||||
|
if (download.succeeded && !this.#refreshed.has(download)) {
|
||||||
|
this.#refreshed.add(download);
|
||||||
|
download.refresh().catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
|
||||||
|
#fileName(download) {
|
||||||
|
return download.target.path
|
||||||
|
? PathUtils.filename(download.target.path)
|
||||||
|
: download.source.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
#hasFile(download) {
|
||||||
|
return download.succeeded && !download.deleted && download.target.exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file(download) {
|
||||||
|
return new lazy.FileUtils.File(download.target.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#formatUrl(url) {
|
||||||
|
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#matchesQuery(download) {
|
||||||
|
const query = this.searchQuery.toLowerCase();
|
||||||
|
return (
|
||||||
|
this.#fileName(download).toLowerCase().includes(query) ||
|
||||||
|
download.source.url.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#fileType(download) {
|
||||||
|
const extension = this.#fileName(download).match(/\.([^.]+)$/)?.[1];
|
||||||
|
return extension ? EXTENSION_TYPES.get(extension.toLowerCase()) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
#activeTypes() {
|
||||||
|
return Object.keys(FILE_TYPES).filter(id =>
|
||||||
|
this.isFilterActive("type", id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#whenCutoff() {
|
||||||
|
const days = this.activeWhenDays;
|
||||||
|
return days ? Date.now() - days * MS_PER_DAY : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#computeVisible() {
|
||||||
|
const visible = [];
|
||||||
|
const types = this.#activeTypes();
|
||||||
|
const cutoff = this.#whenCutoff();
|
||||||
|
for (let i = this.#downloads.length - 1; i >= 0; i--) {
|
||||||
|
const download = this.#downloads[i];
|
||||||
|
if (types.length && !types.includes(this.#fileType(download))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cutoff && !(download.endTime >= cutoff)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!this.searchQuery || this.#matchesQuery(download)) {
|
||||||
|
visible.push(download);
|
||||||
|
if (visible.length >= this.#limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#iconUrl(download) {
|
||||||
|
if (!download.target.path) {
|
||||||
|
return "moz-icon://.unknown?size=32";
|
||||||
|
}
|
||||||
|
return `moz-icon://${download.target.path}?size=32${
|
||||||
|
download.succeeded ? "&state=normal" : ""
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#isPending(download) {
|
||||||
|
return !download.stopped || (download.canceled && download.hasPartialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
#joinStatus(...parts) {
|
||||||
|
return parts
|
||||||
|
.filter(Boolean)
|
||||||
|
.reduce((a, b) => lazy.DownloadsCommon.strings.statusSeparator(a, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
#statusText(download) {
|
||||||
|
const strings = lazy.DownloadsCommon.strings;
|
||||||
|
const totalBytes = download.hasProgress ? download.totalBytes : -1;
|
||||||
|
if (!download.stopped) {
|
||||||
|
const [statusText, secondsLeft] = lazy.DownloadUtils.getDownloadStatus(
|
||||||
|
download.currentBytes,
|
||||||
|
totalBytes,
|
||||||
|
download.speed,
|
||||||
|
this.#secondsLeft.get(download) ?? Infinity
|
||||||
|
);
|
||||||
|
this.#secondsLeft.set(download, secondsLeft);
|
||||||
|
return statusText;
|
||||||
|
}
|
||||||
|
this.#secondsLeft.delete(download);
|
||||||
|
if (download.deleted) {
|
||||||
|
return strings.fileDeleted;
|
||||||
|
}
|
||||||
|
if (download.succeeded) {
|
||||||
|
if (!download.target.exists) {
|
||||||
|
return strings.fileMovedOrMissing;
|
||||||
|
}
|
||||||
|
const parsed = URL.parse(download.source.url);
|
||||||
|
const uri = parsed && Services.io.newURI(parsed.href);
|
||||||
|
const host = uri
|
||||||
|
? lazy.BrowserUtils.formatURIForDisplay(uri, { onlyBaseDomain: true })
|
||||||
|
: "";
|
||||||
|
const [date] = lazy.DownloadUtils.getReadableDates(
|
||||||
|
new Date(download.endTime)
|
||||||
|
);
|
||||||
|
return this.#joinStatus(
|
||||||
|
lazy.DownloadsViewUI.getSizeWithUnits(download),
|
||||||
|
host,
|
||||||
|
date
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (download.canceled && download.hasPartialData) {
|
||||||
|
return this.#joinStatus(
|
||||||
|
strings.statePaused,
|
||||||
|
lazy.DownloadUtils.getTransferTotal(download.currentBytes, totalBytes)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (download.error?.becauseBlockedByParentalControls) {
|
||||||
|
return strings.stateBlockedParentalControls;
|
||||||
|
}
|
||||||
|
if (download.error?.becauseBlockedByReputationCheck) {
|
||||||
|
return strings.blockedMalware;
|
||||||
|
}
|
||||||
|
return download.canceled ? strings.stateCanceled : strings.stateFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
#showInFolder(download) {
|
||||||
|
if (!this.#hasFile(download)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lazy.DownloadsCommon.showDownloadedFile(this.#file(download));
|
||||||
|
}
|
||||||
|
|
||||||
|
#onRowClick(download) {
|
||||||
|
if (!this.#hasFile(download)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#showInFolder(download);
|
||||||
|
clearTimeout(this.#openingTimer);
|
||||||
|
this.#openingDownload = download;
|
||||||
|
this.#openingTimer = setTimeout(() => {
|
||||||
|
this.#openingTimer = null;
|
||||||
|
this.#openingDownload = null;
|
||||||
|
this.requestUpdate();
|
||||||
|
}, OPENING_FEEDBACK_MS);
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#canRetry(download) {
|
||||||
|
return (
|
||||||
|
download.stopped &&
|
||||||
|
!download.succeeded &&
|
||||||
|
(download.canceled || !!download.error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#retryDownload(download) {
|
||||||
|
if (download.start) {
|
||||||
|
// Errors when retrying are already reported as download failures.
|
||||||
|
download.start().catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// History-only entries have no session object, so download the URL again.
|
||||||
|
const targetName = download.target.path
|
||||||
|
? PathUtils.filename(download.target.path)
|
||||||
|
: null;
|
||||||
|
window.DownloadURL(download.source.url, targetName, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
#cancelDownload(download) {
|
||||||
|
download.cancel().catch(() => {});
|
||||||
|
download
|
||||||
|
.removePartialData()
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => download.target.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
#openDownload(download) {
|
||||||
|
if (!this.#hasFile(download)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lazy.DownloadsCommon.openDownload(download, { openWhere: "tab" });
|
||||||
|
}
|
||||||
|
|
||||||
|
#copyFile(download) {
|
||||||
|
if (!this.#hasFile(download)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const transferable = Cc[
|
||||||
|
"@mozilla.org/widget/transferable;1"
|
||||||
|
].createInstance(Ci.nsITransferable);
|
||||||
|
transferable.init(window.docShell.QueryInterface(Ci.nsILoadContext));
|
||||||
|
transferable.addDataFlavor(FILE_MIME);
|
||||||
|
transferable.setTransferData(FILE_MIME, this.#file(download));
|
||||||
|
Services.clipboard.setData(
|
||||||
|
transferable,
|
||||||
|
null,
|
||||||
|
Services.clipboard.kGlobalClipboard
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#hideDownload(download) {
|
||||||
|
lazy.DownloadsCommon.deleteDownload(download).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#trashDownload(download) {
|
||||||
|
lazy.DownloadsCommon.deleteDownloadFiles(
|
||||||
|
download,
|
||||||
|
lazy.DownloadsViewUI.clearHistoryOnDelete
|
||||||
|
).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#isActionEnabled(action, download) {
|
||||||
|
switch (action) {
|
||||||
|
case "open":
|
||||||
|
case "copy":
|
||||||
|
case "show":
|
||||||
|
return this.#hasFile(download);
|
||||||
|
case "copy-link":
|
||||||
|
return !download.source.isDataURICleared;
|
||||||
|
case "trash":
|
||||||
|
return (
|
||||||
|
this.#hasFile(download) ||
|
||||||
|
!download.stopped ||
|
||||||
|
download.hasPartialData
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#doAction(action, download) {
|
||||||
|
switch (action) {
|
||||||
|
case "open":
|
||||||
|
this.#openDownload(download);
|
||||||
|
break;
|
||||||
|
case "copy":
|
||||||
|
this.#copyFile(download);
|
||||||
|
break;
|
||||||
|
case "copy-link":
|
||||||
|
lazy.DownloadsCommon.copyDownloadLink(download);
|
||||||
|
break;
|
||||||
|
case "show":
|
||||||
|
this.#showInFolder(download);
|
||||||
|
break;
|
||||||
|
case "hide":
|
||||||
|
this.#hideDownload(download);
|
||||||
|
break;
|
||||||
|
case "trash":
|
||||||
|
this.#trashDownload(download);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onDragStart(event, download) {
|
||||||
|
if (!this.#hasFile(download) || !this.#file(download).exists()) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const file = this.#file(download);
|
||||||
|
const { dataTransfer } = event;
|
||||||
|
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
|
||||||
|
dataTransfer.effectAllowed = "copyMove";
|
||||||
|
dataTransfer.setData("text/uri-list", Services.io.newFileURI(file).spec);
|
||||||
|
dataTransfer.addElement(event.currentTarget);
|
||||||
|
// eslint-disable-next-line mozilla/valid-services
|
||||||
|
Services.zen.playHapticFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
|
||||||
|
#buildMenu() {
|
||||||
|
const menu = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<menupopup class="zen-library-downloads-menu">
|
||||||
|
<menuitem data-action="open" data-l10n-id="library-downloads-menu-open"/>
|
||||||
|
<menuitem data-action="show" data-l10n-id="downloads-cmd-show-menuitem-2"/>
|
||||||
|
<menuseparator/>
|
||||||
|
<menuitem data-action="copy" data-l10n-id="library-downloads-menu-copy"/>
|
||||||
|
<menuitem data-action="copy-link" data-l10n-id="downloads-cmd-copy-download-link"/>
|
||||||
|
<menuseparator/>
|
||||||
|
<menuitem data-action="hide" data-l10n-id="library-downloads-menu-hide"/>
|
||||||
|
<menuitem data-action="trash" data-l10n-id="library-downloads-menu-trash"/>
|
||||||
|
</menupopup>
|
||||||
|
`).firstElementChild;
|
||||||
|
menu.addEventListener("command", event => {
|
||||||
|
if (this.#menuDownload) {
|
||||||
|
this.#doAction(event.target.dataset.action, this.#menuDownload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
menu.addEventListener("popuphidden", () => {
|
||||||
|
this.#menuRow?.removeAttribute("menu-open");
|
||||||
|
this.#menuRow = null;
|
||||||
|
this.#menuDownload = null;
|
||||||
|
});
|
||||||
|
document.getElementById("mainPopupSet").appendChild(menu);
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openMenu(download, row, anchor, event) {
|
||||||
|
const fileName = this.#fileName(download);
|
||||||
|
for (const item of this.#menu.querySelectorAll("menuitem")) {
|
||||||
|
const { action } = item.dataset;
|
||||||
|
if (action === "open" || action === "copy") {
|
||||||
|
document.l10n.setAttributes(item, item.dataset.l10nId, {
|
||||||
|
name: fileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
item.disabled = !this.#isActionEnabled(action, download);
|
||||||
|
}
|
||||||
|
this.#menuRow?.removeAttribute("menu-open");
|
||||||
|
this.#menuRow = row;
|
||||||
|
this.#menuDownload = download;
|
||||||
|
row.setAttribute("menu-open", "true");
|
||||||
|
if (anchor) {
|
||||||
|
this.#menu.openPopup(anchor, "after_end", 0, 4, false, false, event);
|
||||||
|
} else {
|
||||||
|
this.#menu.openPopupAtScreen(event.screenX, event.screenY, true, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rendering
|
||||||
|
|
||||||
|
#renderSubtitle(download) {
|
||||||
|
const statusNode =
|
||||||
|
download === this.#openingDownload
|
||||||
|
? html`<span
|
||||||
|
class="zen-library-download-status"
|
||||||
|
data-l10n-id="library-downloads-opening-in"
|
||||||
|
></span>`
|
||||||
|
: html`<span class="zen-library-download-status"
|
||||||
|
>${this.#statusText(download)}</span
|
||||||
|
>`;
|
||||||
|
return html`
|
||||||
|
<span class="zen-library-row-subtitle">
|
||||||
|
${statusNode}
|
||||||
|
<span class="zen-library-download-url"
|
||||||
|
>${this.#formatUrl(download.source.url)}</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderCancelButton(download) {
|
||||||
|
return html`
|
||||||
|
<toolbarbutton
|
||||||
|
class="toolbarbutton-1"
|
||||||
|
data-l10n-id="library-downloads-cancel-button"
|
||||||
|
@click=${event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.#cancelDownload(download);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="toolbarbutton-icon"
|
||||||
|
src="chrome://browser/skin/zen-icons/close.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</toolbarbutton>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderRetryButton(download) {
|
||||||
|
return html`
|
||||||
|
<toolbarbutton
|
||||||
|
class="toolbarbutton-1"
|
||||||
|
data-l10n-id="library-downloads-retry-button"
|
||||||
|
@click=${event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.#retryDownload(download);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="toolbarbutton-icon"
|
||||||
|
src="chrome://browser/skin/zen-icons/reload.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</toolbarbutton>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderDownload(download) {
|
||||||
|
const pending = this.#isPending(download);
|
||||||
|
const progress = download.hasProgress ? download.progress : 0;
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="zen-library-row"
|
||||||
|
draggable="true"
|
||||||
|
style="--zen-library-download-progress: ${progress}%"
|
||||||
|
?pending=${pending}
|
||||||
|
?indeterminate=${pending && !download.hasProgress}
|
||||||
|
?paused=${pending && download.stopped}
|
||||||
|
?opening=${download === this.#openingDownload}
|
||||||
|
@click=${() => this.#onRowClick(download)}
|
||||||
|
@contextmenu=${event => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.#openMenu(download, event.currentTarget, null, event);
|
||||||
|
}}
|
||||||
|
@dragstart=${event => this.#onDragStart(event, download)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="zen-library-row-icon"
|
||||||
|
src=${this.#iconUrl(download)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div class="zen-library-row-text">
|
||||||
|
<span class="zen-library-row-title">${this.#fileName(download)}</span>
|
||||||
|
${this.#renderSubtitle(download)}
|
||||||
|
</div>
|
||||||
|
<div class="zen-library-row-actions">
|
||||||
|
${when(!download.stopped, () => this.#renderCancelButton(download))}
|
||||||
|
${when(this.#canRetry(download), () =>
|
||||||
|
this.#renderRetryButton(download)
|
||||||
|
)}
|
||||||
|
<toolbarbutton
|
||||||
|
class="toolbarbutton-1"
|
||||||
|
data-l10n-id="library-downloads-more-button"
|
||||||
|
@click=${event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.#openMenu(
|
||||||
|
download,
|
||||||
|
event.currentTarget.closest(".zen-library-row"),
|
||||||
|
event.currentTarget,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="toolbarbutton-icon"
|
||||||
|
src="chrome://global/skin/icons/more.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</toolbarbutton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItems() {
|
||||||
|
if (!this.#loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
this.#visible = this.#computeVisible();
|
||||||
|
if (!this.#visible.length) {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="zen-library-empty"
|
||||||
|
data-l10n-id="library-downloads-empty"
|
||||||
|
></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<div class="zen-library-group">
|
||||||
|
${repeat(
|
||||||
|
this.#visible,
|
||||||
|
download => download,
|
||||||
|
download => this.#renderDownload(download)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
"zen-library-downloads-section",
|
||||||
|
ZenLibraryDownloadsSection
|
||||||
|
);
|
||||||
361
src/zen/library/sections/ZenLibraryHistorySection.mjs
Normal file
361
src/zen/library/sections/ZenLibraryHistorySection.mjs
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { html, repeat } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import {
|
||||||
|
MS_PER_DAY,
|
||||||
|
PAGE_SIZE,
|
||||||
|
ZenLibrarySearchSection,
|
||||||
|
whenFilterGroup,
|
||||||
|
} from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||||
|
|
||||||
|
let lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
PlacesQuery: "resource://gre/modules/PlacesQuery.sys.mjs",
|
||||||
|
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
ChromeUtils.defineLazyGetter(
|
||||||
|
lazy,
|
||||||
|
"relativeDayFormat",
|
||||||
|
() => new Intl.RelativeTimeFormat(undefined, { numeric: "auto" })
|
||||||
|
);
|
||||||
|
|
||||||
|
ChromeUtils.defineLazyGetter(
|
||||||
|
lazy,
|
||||||
|
"dateFormat",
|
||||||
|
() => new Intl.DateTimeFormat(undefined, { dateStyle: "medium" })
|
||||||
|
);
|
||||||
|
|
||||||
|
const HISTORY_DAYS_OLD = 120;
|
||||||
|
const SORT_OPTIONS = ["date", "site", "mostvisited", "lastvisited"];
|
||||||
|
|
||||||
|
const visitKey = visit => `${visit.guid}-${visit.date.getTime()}`;
|
||||||
|
|
||||||
|
export class ZenLibraryHistorySection extends ZenLibrarySearchSection {
|
||||||
|
static id = "history";
|
||||||
|
static label = "library-history-section-title";
|
||||||
|
|
||||||
|
static render(library) {
|
||||||
|
return html`
|
||||||
|
<zen-library-history-section
|
||||||
|
class="zen-library-section"
|
||||||
|
data-section="history"
|
||||||
|
.library=${library}
|
||||||
|
></zen-library-history-section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static properties = {
|
||||||
|
visits: { state: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
#placesQuery = null;
|
||||||
|
#limit = PAGE_SIZE;
|
||||||
|
#fetchGeneration = 0;
|
||||||
|
#exhausted = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.visits = null;
|
||||||
|
this.activeFilters.add("sort:date");
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.#placesQuery = new lazy.PlacesQuery();
|
||||||
|
this.#placesQuery.observeHistory(() => this.#fetch());
|
||||||
|
this.#fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.#placesQuery?.close();
|
||||||
|
this.#placesQuery = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchPlaceholderL10nId() {
|
||||||
|
return "library-history-search-placeholder";
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterTitleL10nId() {
|
||||||
|
return "library-history-filter-title";
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterGroups() {
|
||||||
|
return [
|
||||||
|
whenFilterGroup("library-history-filter-when"),
|
||||||
|
{
|
||||||
|
id: "sort",
|
||||||
|
titleL10nId: "library-history-filter-sort",
|
||||||
|
exclusive: true,
|
||||||
|
options: [
|
||||||
|
{ id: "date", l10nId: "library-history-sort-date" },
|
||||||
|
{ id: "site", l10nId: "library-history-sort-site" },
|
||||||
|
{ id: "mostvisited", l10nId: "library-history-sort-most-visited" },
|
||||||
|
{ id: "lastvisited", l10nId: "library-history-sort-last-visited" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
get #activeDaysOld() {
|
||||||
|
return this.activeWhenDays ?? HISTORY_DAYS_OLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #activeSort() {
|
||||||
|
return SORT_OPTIONS.find(id => this.isFilterActive("sort", id)) ?? "date";
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChanged() {
|
||||||
|
this.#resetAndFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFiltersChanged() {
|
||||||
|
if (!SORT_OPTIONS.some(id => this.isFilterActive("sort", id))) {
|
||||||
|
this.activeFilters.add("sort:date");
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
this.#resetAndFetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
onListScrolledToEnd() {
|
||||||
|
if (this.#exhausted || !this.#placesQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#limit += PAGE_SIZE;
|
||||||
|
this.#fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
#resetAndFetch() {
|
||||||
|
this.#limit = PAGE_SIZE;
|
||||||
|
this.#exhausted = false;
|
||||||
|
this.#fetch();
|
||||||
|
}
|
||||||
|
|
||||||
|
async #fetch() {
|
||||||
|
if (!this.#placesQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const generation = ++this.#fetchGeneration;
|
||||||
|
const daysOld = this.#activeDaysOld;
|
||||||
|
let visits;
|
||||||
|
if (this.searchQuery) {
|
||||||
|
visits =
|
||||||
|
(await this.#placesQuery.searchHistory(
|
||||||
|
this.searchQuery,
|
||||||
|
this.#limit
|
||||||
|
)) ?? [];
|
||||||
|
} else if (this.#activeSort === "mostvisited") {
|
||||||
|
visits = this.#fetchMostVisited(daysOld, this.#limit);
|
||||||
|
} else {
|
||||||
|
visits = await this.#placesQuery.getHistory({
|
||||||
|
daysOld,
|
||||||
|
limit: this.#limit,
|
||||||
|
sortBy: this.#activeSort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (generation !== this.#fetchGeneration || !this.#placesQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#exhausted = this.#countVisits(visits) < this.#limit;
|
||||||
|
if (this.searchQuery && daysOld !== HISTORY_DAYS_OLD) {
|
||||||
|
const cutoff = Date.now() - daysOld * MS_PER_DAY;
|
||||||
|
visits = visits.filter(visit => visit.date.getTime() >= cutoff);
|
||||||
|
}
|
||||||
|
this.visits = visits;
|
||||||
|
// The cached Map is mutated in place, so its identity may not change.
|
||||||
|
this.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#countVisits(visits) {
|
||||||
|
if (Array.isArray(visits)) {
|
||||||
|
return visits.length;
|
||||||
|
}
|
||||||
|
let count = 0;
|
||||||
|
for (const groupVisits of visits.values()) {
|
||||||
|
count += groupVisits.length;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
#fetchMostVisited(daysOld, limit) {
|
||||||
|
const query = lazy.PlacesUtils.history.getNewQuery();
|
||||||
|
query.beginTime = lazy.PlacesUtils.toPRTime(
|
||||||
|
Date.now() - daysOld * MS_PER_DAY
|
||||||
|
);
|
||||||
|
const options = lazy.PlacesUtils.history.getNewQueryOptions();
|
||||||
|
options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
|
||||||
|
options.maxResults = limit;
|
||||||
|
const root = lazy.PlacesUtils.history.executeQuery(query, options).root;
|
||||||
|
root.containerOpen = true;
|
||||||
|
const visits = [];
|
||||||
|
for (let i = 0; i < root.childCount; i++) {
|
||||||
|
const node = root.getChild(i);
|
||||||
|
visits.push({
|
||||||
|
url: node.uri,
|
||||||
|
title: node.title,
|
||||||
|
date: lazy.PlacesUtils.toDate(node.time),
|
||||||
|
guid: node.pageGuid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
root.containerOpen = false;
|
||||||
|
return visits;
|
||||||
|
}
|
||||||
|
|
||||||
|
#openVisit(visit) {
|
||||||
|
window.openTrustedLinkIn(visit.url, "tab");
|
||||||
|
this.library?.constructor.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
#forgetVisit(visit) {
|
||||||
|
lazy.PlacesUtils.history.remove(visit.url);
|
||||||
|
this.visits = this.#withoutUrl(this.visits, visit.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
#withoutUrl(container, url) {
|
||||||
|
if (Array.isArray(container)) {
|
||||||
|
return container.filter(visit => visit.url !== url);
|
||||||
|
}
|
||||||
|
const remaining = new Map();
|
||||||
|
for (const [key, groupVisits] of container) {
|
||||||
|
const filtered = groupVisits.filter(visit => visit.url !== url);
|
||||||
|
if (filtered.length) {
|
||||||
|
remaining.set(key, filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
#formatDay(day) {
|
||||||
|
const daysAgo = Math.round(
|
||||||
|
(this.#placesQuery.getStartOfDayTimestamp(new Date()) - day) / MS_PER_DAY
|
||||||
|
);
|
||||||
|
const formatted =
|
||||||
|
daysAgo <= 6
|
||||||
|
? lazy.relativeDayFormat.format(-daysAgo, "day")
|
||||||
|
: lazy.dateFormat.format(day);
|
||||||
|
return formatted.charAt(0).toLocaleUpperCase() + formatted.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#formatUrl(url) {
|
||||||
|
return url.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A visit drags as a link, so it can be dropped onto content, the tab
|
||||||
|
* strip or another app.
|
||||||
|
*
|
||||||
|
* @param {DragEvent} event
|
||||||
|
* @param {object} visit
|
||||||
|
*/
|
||||||
|
#onDragStart(event, visit) {
|
||||||
|
const { dataTransfer } = event;
|
||||||
|
const title = visit.title || visit.url;
|
||||||
|
dataTransfer.setData("text/x-moz-url", `${visit.url}\n${title}`);
|
||||||
|
dataTransfer.setData("text/uri-list", visit.url);
|
||||||
|
dataTransfer.setData("text/plain", visit.url);
|
||||||
|
dataTransfer.effectAllowed = "copyLink";
|
||||||
|
dataTransfer.addElement(event.currentTarget);
|
||||||
|
// eslint-disable-next-line mozilla/valid-services
|
||||||
|
Services.zen.playHapticFeedback();
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderVisit(visit) {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="zen-library-row"
|
||||||
|
draggable="true"
|
||||||
|
@click=${() => this.#openVisit(visit)}
|
||||||
|
@dragstart=${event => this.#onDragStart(event, visit)}
|
||||||
|
>
|
||||||
|
<img class="zen-library-row-icon" src="page-icon:${visit.url}" alt="" />
|
||||||
|
<div class="zen-library-row-text">
|
||||||
|
<span class="zen-library-row-title">${visit.title || visit.url}</span>
|
||||||
|
<span class="zen-library-row-subtitle"
|
||||||
|
>${this.#formatUrl(visit.url)}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="zen-library-row-actions">
|
||||||
|
<toolbarbutton
|
||||||
|
class="toolbarbutton-1"
|
||||||
|
data-l10n-id="library-history-forget-button"
|
||||||
|
@click=${event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.#forgetVisit(visit);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="toolbarbutton-icon"
|
||||||
|
src="chrome://browser/skin/zen-icons/trash.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</toolbarbutton>
|
||||||
|
<toolbarbutton
|
||||||
|
class="toolbarbutton-1"
|
||||||
|
data-l10n-id="library-history-reopen-button"
|
||||||
|
@click=${event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.#openVisit(visit);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="toolbarbutton-icon"
|
||||||
|
src="chrome://browser/skin/zen-icons/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);
|
||||||
29
src/zen/library/sections/ZenLibraryMediaSection.mjs
Normal file
29
src/zen/library/sections/ZenLibraryMediaSection.mjs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||||
|
|
||||||
|
export class ZenLibraryMediaSection extends MozLitElement {
|
||||||
|
static id = "media";
|
||||||
|
static label = "library-media-section-title";
|
||||||
|
|
||||||
|
static render(library) {
|
||||||
|
return html`
|
||||||
|
<zen-library-media-section
|
||||||
|
class="zen-library-section"
|
||||||
|
data-section="media"
|
||||||
|
.library=${library}
|
||||||
|
></zen-library-media-section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return html`
|
||||||
|
<div class="zen-library-empty" data-l10n-id="library-media-empty"></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("zen-library-media-section", ZenLibraryMediaSection);
|
||||||
265
src/zen/library/sections/ZenLibrarySearchSection.mjs
Normal file
265
src/zen/library/sections/ZenLibrarySearchSection.mjs
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import { html, when } from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||||
|
|
||||||
|
const SEARCH_DEBOUNCE_MS = 250;
|
||||||
|
|
||||||
|
export const PAGE_SIZE = 100;
|
||||||
|
export const MS_PER_DAY = 86400000;
|
||||||
|
const WHEN_DAYS = { today: 1, week: 7, month: 30 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the exclusive "when" filter group shared by time-based sections.
|
||||||
|
*
|
||||||
|
* @param {string} titleL10nId - Fluent id of the group title
|
||||||
|
*/
|
||||||
|
export function whenFilterGroup(titleL10nId) {
|
||||||
|
return {
|
||||||
|
id: "when",
|
||||||
|
titleL10nId,
|
||||||
|
exclusive: true,
|
||||||
|
options: Object.keys(WHEN_DAYS).map(id => ({
|
||||||
|
id,
|
||||||
|
l10nId: `library-filter-${id}`,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ZenLibrarySearchSection extends MozLitElement {
|
||||||
|
static properties = {
|
||||||
|
searchQuery: { type: String, state: true },
|
||||||
|
filtersOpen: { type: Boolean, state: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
#searchDebounce = null;
|
||||||
|
#sentinelObserver = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.searchQuery = "";
|
||||||
|
this.filtersOpen = false;
|
||||||
|
this.activeFilters = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchPlaceholderL10nId() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
get filterTitleL10nId() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter groups shown in the filter panel. Sections without filters keep
|
||||||
|
* this empty, which also hides the filter button. An `exclusive` group
|
||||||
|
* behaves like a radio group where the active option can also be toggled
|
||||||
|
* back off.
|
||||||
|
*
|
||||||
|
* @returns {{id: string, titleL10nId: string, exclusive?: boolean,
|
||||||
|
* options: {id: string, l10nId?: string, label?: string,
|
||||||
|
* icon?: string}[]}[]}
|
||||||
|
*/
|
||||||
|
get filterGroups() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderItems() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchChanged() {}
|
||||||
|
onFiltersChanged() {}
|
||||||
|
onListScrolledToEnd() {}
|
||||||
|
|
||||||
|
isFilterActive(groupId, optionId) {
|
||||||
|
return this.activeFilters.has(`${groupId}:${optionId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of days selected in the "when" filter group, or null when no
|
||||||
|
* option is active.
|
||||||
|
*/
|
||||||
|
get activeWhenDays() {
|
||||||
|
for (const [id, days] of Object.entries(WHEN_DAYS)) {
|
||||||
|
if (this.isFilterActive("when", id)) {
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
if (this.#searchDebounce) {
|
||||||
|
clearTimeout(this.#searchDebounce);
|
||||||
|
this.#searchDebounce = null;
|
||||||
|
}
|
||||||
|
this.#sentinelObserver?.disconnect();
|
||||||
|
this.#sentinelObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
if (super.firstUpdated) {
|
||||||
|
super.firstUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = this.querySelector(".zen-library-search-results");
|
||||||
|
this.#sentinelObserver = new IntersectionObserver(
|
||||||
|
entries => {
|
||||||
|
if (entries.some(entry => entry.isIntersecting)) {
|
||||||
|
this.onListScrolledToEnd();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ root: results, rootMargin: "200px" }
|
||||||
|
);
|
||||||
|
this.#sentinelObserver.observe(
|
||||||
|
this.querySelector(".zen-library-search-sentinel")
|
||||||
|
);
|
||||||
|
|
||||||
|
results.addEventListener(
|
||||||
|
"scroll",
|
||||||
|
() => results.toggleAttribute("scrolled", results.scrollTop > 0),
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
if (super.updated) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
}
|
||||||
|
if (changedProperties.has("filtersOpen") && this.filtersOpen) {
|
||||||
|
const inner = this.querySelector(".zen-library-filter-panel-inner");
|
||||||
|
this.style.setProperty(
|
||||||
|
"--zen-library-filter-height",
|
||||||
|
`${inner.scrollHeight + 8}px`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onSearchInput(event) {
|
||||||
|
const { value } = event.target;
|
||||||
|
if (this.#searchDebounce) {
|
||||||
|
clearTimeout(this.#searchDebounce);
|
||||||
|
}
|
||||||
|
this.#searchDebounce = setTimeout(() => {
|
||||||
|
this.#searchDebounce = null;
|
||||||
|
const query = value.trim();
|
||||||
|
if (query !== this.searchQuery) {
|
||||||
|
this.searchQuery = query;
|
||||||
|
this.onSearchChanged();
|
||||||
|
}
|
||||||
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggleFilter(group, optionId) {
|
||||||
|
const key = `${group.id}:${optionId}`;
|
||||||
|
const wasActive = this.activeFilters.has(key);
|
||||||
|
if (group.exclusive) {
|
||||||
|
for (const option of group.options) {
|
||||||
|
this.activeFilters.delete(`${group.id}:${option.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wasActive) {
|
||||||
|
this.activeFilters.delete(key);
|
||||||
|
} else {
|
||||||
|
this.activeFilters.add(key);
|
||||||
|
}
|
||||||
|
this.requestUpdate();
|
||||||
|
this.onFiltersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderFilterOption(group, option) {
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="zen-library-filter-chip"
|
||||||
|
?active=${this.isFilterActive(group.id, option.id)}
|
||||||
|
@click=${() => this.#toggleFilter(group, option.id)}
|
||||||
|
>
|
||||||
|
${when(option.icon, () => html`<img src=${option.icon} alt="" />`)}
|
||||||
|
${
|
||||||
|
option.l10nId
|
||||||
|
? html`<span data-l10n-id=${option.l10nId}></span>`
|
||||||
|
: html`<span>${option.label}</span>`
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderFilterPanel() {
|
||||||
|
return html`
|
||||||
|
<div class="zen-library-filter-header" ?inert=${!this.filtersOpen}>
|
||||||
|
<h2 data-l10n-id=${this.filterTitleL10nId}></h2>
|
||||||
|
<button
|
||||||
|
class="zen-library-filter-done"
|
||||||
|
data-l10n-id="library-filter-done"
|
||||||
|
@click=${() => {
|
||||||
|
this.filtersOpen = false;
|
||||||
|
}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="zen-library-filter-panel">
|
||||||
|
<div class="zen-library-filter-panel-inner" ?inert=${!this.filtersOpen}>
|
||||||
|
${this.filterGroups.map(
|
||||||
|
group => html`
|
||||||
|
<div class="zen-library-filter-group">
|
||||||
|
<h3 data-l10n-id=${group.titleL10nId}></h3>
|
||||||
|
<div class="zen-library-filter-options">
|
||||||
|
${group.options.map(option =>
|
||||||
|
this.#renderFilterOption(group, option)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const hasFilters = !!this.filterGroups.length;
|
||||||
|
return html`
|
||||||
|
<div class="zen-library-search-top" ?open=${this.filtersOpen}>
|
||||||
|
<div class="zen-library-search-header" ?inert=${this.filtersOpen}>
|
||||||
|
<div class="zen-library-search-box">
|
||||||
|
<img
|
||||||
|
src="chrome://browser/skin/zen-icons/search-glass.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
data-l10n-id=${this.searchPlaceholderL10nId}
|
||||||
|
@input=${this.#onSearchInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
${when(
|
||||||
|
hasFilters,
|
||||||
|
() => html`
|
||||||
|
<button
|
||||||
|
class="zen-library-filter-button"
|
||||||
|
@click=${() => {
|
||||||
|
this.filtersOpen = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src="chrome://browser/skin/zen-icons/sliders.svg" alt="" />
|
||||||
|
<span data-l10n-id="library-filter-button"></span>
|
||||||
|
</button>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
${when(hasFilters, () => this.#renderFilterPanel())}
|
||||||
|
</div>
|
||||||
|
<div class="zen-library-search-results">
|
||||||
|
${this.renderItems()}
|
||||||
|
<div class="zen-library-search-sentinel"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
837
src/zen/library/sections/ZenLibrarySpacesSection.mjs
Normal file
837
src/zen/library/sections/ZenLibrarySpacesSection.mjs
Normal file
@@ -0,0 +1,837 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import {
|
||||||
|
html,
|
||||||
|
repeat,
|
||||||
|
styleMap,
|
||||||
|
} from "chrome://global/content/vendor/lit.all.mjs";
|
||||||
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||||
|
import { ZenLibraryDragAndDrop } from "moz-src:///zen/library/ZenLibraryDragAndDrop.mjs";
|
||||||
|
|
||||||
|
const GRADIENT_TOPIC = "zen-space-gradient-update";
|
||||||
|
const SCROLL_EDGE_PX = 48;
|
||||||
|
const SCROLL_STEP_PX = 12;
|
||||||
|
|
||||||
|
// Events the copies fire while being built that the rest of the browser must
|
||||||
|
// not mistake for real tab strip changes.
|
||||||
|
// Changes to the real strips that the copies must follow. Attribute changes
|
||||||
|
// count only for what a copy shows.
|
||||||
|
const STRIP_EVENTS = [
|
||||||
|
"TabOpen",
|
||||||
|
"TabClose",
|
||||||
|
"TabMove",
|
||||||
|
"TabPinned",
|
||||||
|
"TabUnpinned",
|
||||||
|
"TabHide",
|
||||||
|
"TabShow",
|
||||||
|
"TabAttrModified",
|
||||||
|
"TabGrouped",
|
||||||
|
"TabUngrouped",
|
||||||
|
"TabGroupCreate",
|
||||||
|
"TabGroupRemoved",
|
||||||
|
"TabGroupMoved",
|
||||||
|
];
|
||||||
|
const SHOWN_ATTRIBUTES = ["label", "image", "pending", "muted", "soundplaying"];
|
||||||
|
|
||||||
|
const CONTAINED_EVENTS = [
|
||||||
|
"TabGrouped",
|
||||||
|
"TabUngrouped",
|
||||||
|
"TabGroupCreate",
|
||||||
|
"TabGroupRemovedFromDOM",
|
||||||
|
"FolderGrouped",
|
||||||
|
"FolderUngrouped",
|
||||||
|
|
||||||
|
"TabGroupCollapse",
|
||||||
|
"TabGroupExpand",
|
||||||
|
];
|
||||||
|
const GROUP_TAGS = ["tab-group", "zen-folder", "tab-split-view-wrapper"];
|
||||||
|
|
||||||
|
export class ZenLibrarySpacesSection extends MozLitElement {
|
||||||
|
static id = "spaces";
|
||||||
|
static label = "library-spaces-section-title";
|
||||||
|
|
||||||
|
static render(library) {
|
||||||
|
return html`
|
||||||
|
<zen-library-spaces-section
|
||||||
|
class="zen-library-section"
|
||||||
|
data-section="spaces"
|
||||||
|
.library=${library}
|
||||||
|
></zen-library-spaces-section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static properties = {
|
||||||
|
renaming: { state: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
#observer = { observe: () => this.requestUpdate() };
|
||||||
|
#onDataChanged = () => this.requestUpdate();
|
||||||
|
#onStripEvent = event => {
|
||||||
|
// The copies raise the same events as they are built; those are not
|
||||||
|
// changes to the real strips.
|
||||||
|
if (this.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.type === "TabAttrModified" &&
|
||||||
|
!event.detail?.changed?.some(attr => SHOWN_ATTRIBUTES.includes(attr))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#scheduleStripsRefresh(
|
||||||
|
event.type === "TabAttrModified"
|
||||||
|
? event.target.getAttribute("zen-workspace-id")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
#refreshTimer = null;
|
||||||
|
/** @type {Set<string>|null} The spaces to rebuild, or null for every card */
|
||||||
|
#pendingSpaces = new Set();
|
||||||
|
#resizeObserver = new ResizeObserver(() => this.#updateLibraryWidth());
|
||||||
|
#onScroll = event => {
|
||||||
|
if (event.target.classList.contains("zen-library-space-body")) {
|
||||||
|
this.#updateScrollBorders(event.target);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#onDragEnd = () => {
|
||||||
|
this.#landing = false;
|
||||||
|
this.#dnd.clearDragOverVisuals();
|
||||||
|
this.#refreshStrips();
|
||||||
|
};
|
||||||
|
// A drag from a copy ran on the strip's drag and drop, whose own drag end
|
||||||
|
// only hears drags from the strip; it gets to finish this one too. The
|
||||||
|
// event comes to the copy itself, which the drop's rebuild has taken out
|
||||||
|
// of the document by then, so it never reaches the window.
|
||||||
|
#onCopyDragEnd = event => {
|
||||||
|
gBrowser.tabContainer.tabDragAndDrop.handle_dragend(event);
|
||||||
|
this.#onDragEnd();
|
||||||
|
};
|
||||||
|
#landing = false;
|
||||||
|
/** @type {WeakMap<Element, Element>} copied tab or group to the real one */
|
||||||
|
#realElements = new WeakMap();
|
||||||
|
#dnd = new ZenLibraryDragAndDrop(this);
|
||||||
|
#dragIndex = -1;
|
||||||
|
#dropIndex = -1;
|
||||||
|
#dragStartX = 0;
|
||||||
|
#dragCenterX = 0;
|
||||||
|
#dragStartScroll = 0;
|
||||||
|
#lastPointerX = 0;
|
||||||
|
#scrollFrame = null;
|
||||||
|
#slotCenters = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.renaming = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
window.addEventListener("ZenWorkspaceDataChanged", this.#onDataChanged);
|
||||||
|
window.addEventListener("ZenWorkspacesUIUpdate", this.#onDataChanged);
|
||||||
|
Services.obs.addObserver(this.#observer, GRADIENT_TOPIC);
|
||||||
|
this.#resizeObserver.observe(this);
|
||||||
|
window.addEventListener("dragend", this.#onDragEnd);
|
||||||
|
this.addEventListener("scroll", this.#onScroll, true);
|
||||||
|
for (const type of CONTAINED_EVENTS) {
|
||||||
|
this.addEventListener(type, this.#containEvent, true);
|
||||||
|
}
|
||||||
|
for (const type of STRIP_EVENTS) {
|
||||||
|
window.addEventListener(type, this.#onStripEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
window.removeEventListener("ZenWorkspaceDataChanged", this.#onDataChanged);
|
||||||
|
window.removeEventListener("ZenWorkspacesUIUpdate", this.#onDataChanged);
|
||||||
|
Services.obs.removeObserver(this.#observer, GRADIENT_TOPIC);
|
||||||
|
this.#resizeObserver.disconnect();
|
||||||
|
window.removeEventListener("dragend", this.#onDragEnd);
|
||||||
|
this.removeEventListener("scroll", this.#onScroll, true);
|
||||||
|
for (const type of CONTAINED_EVENTS) {
|
||||||
|
this.removeEventListener(type, this.#containEvent, true);
|
||||||
|
}
|
||||||
|
for (const type of STRIP_EVENTS) {
|
||||||
|
window.removeEventListener(type, this.#onStripEvent);
|
||||||
|
}
|
||||||
|
clearTimeout(this.#refreshTimer);
|
||||||
|
this.#refreshTimer = null;
|
||||||
|
this.library?.style.removeProperty("--zen-library-content-width");
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
super.updated(changedProperties);
|
||||||
|
this.#fillStrips();
|
||||||
|
this.#updateLibraryWidth();
|
||||||
|
for (const card of this.#cards) {
|
||||||
|
this.#updateScrollBorders(card.querySelector(".zen-library-space-body"));
|
||||||
|
}
|
||||||
|
if (changedProperties.has("renaming") && this.renaming) {
|
||||||
|
const input = this.querySelector(".zen-library-space-name-input");
|
||||||
|
input?.focus();
|
||||||
|
input?.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateLibraryWidth() {
|
||||||
|
const side = this.library?.querySelector("#zen-library-side");
|
||||||
|
const list = this.querySelector(".zen-library-spaces");
|
||||||
|
if (!side || !list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sideWidth = window.windowUtils.getBoundsWithoutFlushing(side).width;
|
||||||
|
this.library.style.setProperty(
|
||||||
|
"--zen-library-content-width",
|
||||||
|
`${sideWidth + this.#cardsWidth(list)}px`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
strip.textContent = "";
|
||||||
|
const space = gZenWorkspaces.workspaceElement(card.dataset.uuid);
|
||||||
|
if (!space) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const section of [space.pinnedTabsContainer, space.tabsContainer]) {
|
||||||
|
this.#appendCopy(strip, section);
|
||||||
|
}
|
||||||
|
// Rows slide to their new places, unless the drag image is landing on
|
||||||
|
// the moved one, which is motion enough.
|
||||||
|
if (before && !this.#landing) {
|
||||||
|
this.#animateRows(strip, before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top-left of every row in a strip, keyed by the copy's stable id, so a
|
||||||
|
* row can be matched across a rebuild.
|
||||||
|
*
|
||||||
|
* @param {Element} strip
|
||||||
|
* @returns {Map<string, {left: number, top: number}>}
|
||||||
|
*/
|
||||||
|
#rowPositions(strip) {
|
||||||
|
const positions = new Map();
|
||||||
|
for (const row of strip.querySelectorAll(
|
||||||
|
"tab, .tab-group-label-container"
|
||||||
|
)) {
|
||||||
|
const id = row.id || row.closest("[id]")?.id;
|
||||||
|
if (id) {
|
||||||
|
const rect = row.getBoundingClientRect();
|
||||||
|
positions.set(id, { left: rect.left, top: rect.top });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slides each rebuilt row from where its old copy was to where it is now.
|
||||||
|
*
|
||||||
|
* @param {Element} strip
|
||||||
|
* @param {Map<string, {left: number, top: number}>} before - Old positions
|
||||||
|
*/
|
||||||
|
#animateRows(strip, before) {
|
||||||
|
if (gReduceMotion) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const row of strip.querySelectorAll(
|
||||||
|
"tab, .tab-group-label-container"
|
||||||
|
)) {
|
||||||
|
const id = row.id || row.closest("[id]")?.id;
|
||||||
|
const old = id && before.get(id);
|
||||||
|
if (!old) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const rect = row.getBoundingClientRect();
|
||||||
|
const dx = old.left - rect.left;
|
||||||
|
const dy = old.top - rect.top;
|
||||||
|
if (dx || dy) {
|
||||||
|
gZenUIManager.elementAnimate(
|
||||||
|
row,
|
||||||
|
{ x: [dx, 0], y: [dy, 0] },
|
||||||
|
{ duration: 180, easing: "ease-out" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies must never be mistaken for the originals by id lookups, so every
|
||||||
|
* id in a copy gets a suffix.
|
||||||
|
*
|
||||||
|
* @param {Element} copy - The copied element, before it is connected
|
||||||
|
*/
|
||||||
|
#renameIds(copy) {
|
||||||
|
for (const element of [copy, ...copy.querySelectorAll("[id]")]) {
|
||||||
|
if (element.id) {
|
||||||
|
element.id = `${element.id}-copy`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#appendCopy(container, node) {
|
||||||
|
// A closing tab stays in the strip through its animation, but is gone.
|
||||||
|
if (
|
||||||
|
node.nodeType !== Node.ELEMENT_NODE ||
|
||||||
|
node.hasAttribute("hidden") ||
|
||||||
|
node.closing
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (gBrowser.isTab(node)) {
|
||||||
|
const copy = node.cloneNode(false);
|
||||||
|
this.#renameIds(copy);
|
||||||
|
for (const attribute of [
|
||||||
|
"selected",
|
||||||
|
"visuallyselected",
|
||||||
|
"multiselected",
|
||||||
|
]) {
|
||||||
|
copy.removeAttribute(attribute);
|
||||||
|
}
|
||||||
|
container.appendChild(copy);
|
||||||
|
this.#realElements.set(copy, node);
|
||||||
|
if (node.glanceTab) {
|
||||||
|
this.#appendCopy(copy.querySelector(".tab-content"), node.glanceTab);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (GROUP_TAGS.includes(node.localName)) {
|
||||||
|
const copy = node.cloneNode(false);
|
||||||
|
this.#renameIds(copy);
|
||||||
|
container.appendChild(copy);
|
||||||
|
this.#realElements.set(copy, node);
|
||||||
|
const icon = copy.querySelector(".tab-group-folder-icon");
|
||||||
|
const realIcon = node.querySelector(".tab-group-folder-icon");
|
||||||
|
if (icon && realIcon) {
|
||||||
|
icon.replaceChildren(
|
||||||
|
...[...realIcon.children].map(child => child.cloneNode(true))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const inner = node.querySelector(":scope > .tab-group-container") ?? node;
|
||||||
|
for (const child of inner.children) {
|
||||||
|
if (!child.classList.contains("zen-tab-group-start")) {
|
||||||
|
this.#appendCopy(copy, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!node.querySelector("tab, tab-group, zen-folder")) {
|
||||||
|
const copy = node.cloneNode(true);
|
||||||
|
this.#renameIds(copy);
|
||||||
|
container.appendChild(copy);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const copy = node.cloneNode(false);
|
||||||
|
this.#renameIds(copy);
|
||||||
|
container.appendChild(copy);
|
||||||
|
for (const child of node.children) {
|
||||||
|
this.#appendCopy(copy, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#containEvent = event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
#containHover = {
|
||||||
|
handleEvent: event => event.stopPropagation(),
|
||||||
|
capture: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
#onStripClick = event => {
|
||||||
|
const label = event.target.closest(".tab-group-label-container");
|
||||||
|
if (label) {
|
||||||
|
const copy = label.closest(GROUP_TAGS.join());
|
||||||
|
const group = this.#realElements.get(copy);
|
||||||
|
if (group) {
|
||||||
|
event.stopPropagation();
|
||||||
|
const collapsed = !group.collapsed;
|
||||||
|
group.collapsed = collapsed;
|
||||||
|
copy.collapsed = collapsed;
|
||||||
|
if (collapsed) {
|
||||||
|
gZenFolders.animateCollapse(copy);
|
||||||
|
} else {
|
||||||
|
gZenFolders.animateExpand(copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tab = this.#realElements.get(event.target.closest("tab"));
|
||||||
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.target.closest(".tab-close-button")) {
|
||||||
|
gBrowser.removeTab(tab, { animate: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gBrowser.selectedTab = tab;
|
||||||
|
this.library?.constructor.toggle();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Element} copy - A copied tab, group or folder
|
||||||
|
* @returns {Element|undefined} The real element it stands for
|
||||||
|
*/
|
||||||
|
realElementFor(copy) {
|
||||||
|
return this.#realElements.get(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds every card's copies immediately, so a just-moved tab's new copy
|
||||||
|
* exists to read or animate.
|
||||||
|
*/
|
||||||
|
refreshStripsNow() {
|
||||||
|
this.#fillStrips(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
beginTabLanding(tab) {
|
||||||
|
this.#landing = true;
|
||||||
|
this.refreshStripsNow();
|
||||||
|
const copy = this.copyForTab(tab);
|
||||||
|
if (!copy?.isConnected) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rect = copy.getBoundingClientRect();
|
||||||
|
return rect.width && rect.height ? copy : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
copyForTab(tab) {
|
||||||
|
for (const card of this.#cards) {
|
||||||
|
for (const copy of card.querySelectorAll(".zen-library-space-tabs tab")) {
|
||||||
|
if (this.#realElements.get(copy) === tab) {
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#onStripDragStart = event => {
|
||||||
|
const target =
|
||||||
|
event.target.nodeType === Node.TEXT_NODE
|
||||||
|
? event.target.parentElement
|
||||||
|
: event.target;
|
||||||
|
const copy = target.closest("tab");
|
||||||
|
if (copy) {
|
||||||
|
this.#dnd.startTabDrag(event, copy);
|
||||||
|
copy.addEventListener("dragend", this.#onCopyDragEnd, { once: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#onStripDrop = event => {
|
||||||
|
this.#dnd.handle_drop(event);
|
||||||
|
this.#refreshStrips();
|
||||||
|
};
|
||||||
|
|
||||||
|
#renderStrip() {
|
||||||
|
return html`
|
||||||
|
<div class="zen-library-space-body" zen-sidebar-expanded="true">
|
||||||
|
<div
|
||||||
|
class="zen-library-space-tabs"
|
||||||
|
id="tabbrowser-tabs"
|
||||||
|
orient="vertical"
|
||||||
|
expanded="true"
|
||||||
|
@click=${this.#onStripClick}
|
||||||
|
@mouseover=${this.#containHover}
|
||||||
|
@mouseout=${this.#containHover}
|
||||||
|
@dragstart=${this.#onStripDragStart}
|
||||||
|
@dragover=${event => this.#dnd.handle_dragover(event)}
|
||||||
|
@dragleave=${event => this.#dnd.handle_dragleave(event)}
|
||||||
|
@drop=${this.#onStripDrop}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme
|
||||||
|
|
||||||
|
#themeStyles(workspace) {
|
||||||
|
const { gradient, isDarkMode, isExplicitMode, toolbarColor, primaryColor } =
|
||||||
|
gZenThemePicker.getGradientForWorkspace(workspace);
|
||||||
|
let colorScheme = "";
|
||||||
|
if (isExplicitMode) {
|
||||||
|
colorScheme = isDarkMode ? "dark" : "light";
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"--zen-library-space-gradient": gradient,
|
||||||
|
"--zen-primary-color": primaryColor,
|
||||||
|
"--toolbox-textcolor": `rgba(${toolbarColor.join(",")})`,
|
||||||
|
colorScheme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#openThemePicker(workspace, event) {
|
||||||
|
gZenThemePicker.openThemePickerForWorkspace(
|
||||||
|
workspace,
|
||||||
|
event.currentTarget,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#openActions(event) {
|
||||||
|
document
|
||||||
|
.getElementById("zenWorkspaceMoreActions")
|
||||||
|
.openPopup(event.currentTarget, "after_end");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon and name
|
||||||
|
|
||||||
|
#changeIcon(workspace, event) {
|
||||||
|
gZenEmojiPicker.open(event.currentTarget, {
|
||||||
|
closeOnSelect: false,
|
||||||
|
allowNone: gZenWorkspaces.workspaceHasIcon(workspace),
|
||||||
|
onSelect: async icon => {
|
||||||
|
workspace.icon = icon;
|
||||||
|
await gZenWorkspaces.saveWorkspace(workspace);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#commitRename(workspace, event) {
|
||||||
|
if (this.renaming !== workspace.uuid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newName = event.target.value.trim();
|
||||||
|
this.renaming = null;
|
||||||
|
if (newName && newName !== workspace.name) {
|
||||||
|
workspace.name = newName;
|
||||||
|
gZenWorkspaces.saveWorkspace(workspace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onRenameKeyDown(workspace, event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
this.#commitRename(workspace, event);
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
this.renaming = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#onPointerDown(event) {
|
||||||
|
if (event.button !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cards = this.#cards;
|
||||||
|
const card = event.currentTarget.closest(".zen-library-space");
|
||||||
|
this.#dragIndex = cards.indexOf(card);
|
||||||
|
this.#dropIndex = this.#dragIndex;
|
||||||
|
this.#dragStartX = event.clientX;
|
||||||
|
this.#lastPointerX = event.clientX;
|
||||||
|
this.#dragStartScroll = this.#list.scrollLeft;
|
||||||
|
const cardRect = window.windowUtils.getBoundsWithoutFlushing(card);
|
||||||
|
this.#dragCenterX = cardRect.left + cardRect.width / 2;
|
||||||
|
this.#slotCenters = cards
|
||||||
|
.filter((other, i) => i !== this.#dragIndex)
|
||||||
|
.map(other => {
|
||||||
|
const rect = window.windowUtils.getBoundsWithoutFlushing(other);
|
||||||
|
return rect.left + rect.width / 2;
|
||||||
|
});
|
||||||
|
card.setAttribute("dragging", "true");
|
||||||
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
|
event.preventDefault();
|
||||||
|
this.#autoScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
get #list() {
|
||||||
|
return this.querySelector(".zen-library-spaces");
|
||||||
|
}
|
||||||
|
|
||||||
|
#onPointerMove(event) {
|
||||||
|
if (this.#dragIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#lastPointerX = event.clientX;
|
||||||
|
this.#updateDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateDrag() {
|
||||||
|
const cards = this.#cards;
|
||||||
|
const scrolled = this.#list.scrollLeft - this.#dragStartScroll;
|
||||||
|
const travel = this.#lastPointerX - this.#dragStartX + scrolled;
|
||||||
|
cards[this.#dragIndex].style.translate = `${travel}px 0`;
|
||||||
|
const centerX = this.#dragCenterX + travel;
|
||||||
|
const index = this.#slotCenters.filter(center => centerX > center).length;
|
||||||
|
if (index !== this.#dropIndex) {
|
||||||
|
this.#dropIndex = index;
|
||||||
|
this.#shiftCards(cards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#autoScroll() {
|
||||||
|
if (this.#dragIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = this.#list;
|
||||||
|
const rect = window.windowUtils.getBoundsWithoutFlushing(list);
|
||||||
|
let step = 0;
|
||||||
|
if (this.#lastPointerX < rect.left + SCROLL_EDGE_PX) {
|
||||||
|
step = -SCROLL_STEP_PX;
|
||||||
|
} else if (this.#lastPointerX > rect.right - SCROLL_EDGE_PX) {
|
||||||
|
step = SCROLL_STEP_PX;
|
||||||
|
}
|
||||||
|
if (step) {
|
||||||
|
const before = list.scrollLeft;
|
||||||
|
list.scrollLeft += step;
|
||||||
|
if (list.scrollLeft !== before) {
|
||||||
|
this.#updateDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.#scrollFrame = requestAnimationFrame(() => this.#autoScroll());
|
||||||
|
}
|
||||||
|
|
||||||
|
#shiftCards(cards) {
|
||||||
|
cards.forEach((card, i) => {
|
||||||
|
let shift = "";
|
||||||
|
if (this.#dragIndex < i && i <= this.#dropIndex) {
|
||||||
|
shift = "left";
|
||||||
|
} else if (this.#dropIndex <= i && i < this.#dragIndex) {
|
||||||
|
shift = "right";
|
||||||
|
}
|
||||||
|
if (shift) {
|
||||||
|
card.setAttribute("shift", shift);
|
||||||
|
} else {
|
||||||
|
card.removeAttribute("shift");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async #onPointerUp(event, workspace) {
|
||||||
|
if (this.#dragIndex === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
cancelAnimationFrame(this.#scrollFrame);
|
||||||
|
this.#scrollFrame = null;
|
||||||
|
const moved =
|
||||||
|
this.#dropIndex !== this.#dragIndex && event.type === "pointerup";
|
||||||
|
const dropIndex = this.#dropIndex;
|
||||||
|
this.#dragIndex = -1;
|
||||||
|
this.#dropIndex = -1;
|
||||||
|
|
||||||
|
const list = this.#list;
|
||||||
|
list.setAttribute("no-transition", "true");
|
||||||
|
if (moved) {
|
||||||
|
gZenWorkspaces.reorderWorkspace(workspace.uuid, dropIndex);
|
||||||
|
await this.updateComplete;
|
||||||
|
}
|
||||||
|
for (const card of this.#cards) {
|
||||||
|
card.removeAttribute("dragging");
|
||||||
|
card.removeAttribute("shift");
|
||||||
|
card.style.translate = "";
|
||||||
|
}
|
||||||
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
list.removeAttribute("no-transition");
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderIcon(workspace) {
|
||||||
|
const hasIcon = gZenWorkspaces.workspaceHasIcon(workspace);
|
||||||
|
let content = "";
|
||||||
|
if (hasIcon) {
|
||||||
|
const icon = gZenWorkspaces.getWorkspaceIcon(workspace);
|
||||||
|
content = icon.endsWith(".svg") ? html`<img src=${icon} alt="" />` : icon;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="zen-library-space-icon"
|
||||||
|
?no-icon=${!hasIcon}
|
||||||
|
data-l10n-id="library-spaces-icon-button"
|
||||||
|
@click=${event => this.#changeIcon(workspace, event)}
|
||||||
|
>
|
||||||
|
${content}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderName(workspace) {
|
||||||
|
if (this.renaming === workspace.uuid) {
|
||||||
|
return html`
|
||||||
|
<input
|
||||||
|
class="zen-library-space-name-input"
|
||||||
|
.value=${workspace.name}
|
||||||
|
@keydown=${event => this.#onRenameKeyDown(workspace, event)}
|
||||||
|
@blur=${event => this.#commitRename(workspace, event)}
|
||||||
|
/>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
<button
|
||||||
|
class="zen-library-space-name"
|
||||||
|
data-l10n-id="library-spaces-name-button"
|
||||||
|
@click=${() => {
|
||||||
|
this.renaming = workspace.uuid;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${workspace.name}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderSpace(workspace) {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="zen-library-space zen-squircle-before"
|
||||||
|
data-uuid=${workspace.uuid}
|
||||||
|
style=${styleMap(this.#themeStyles(workspace))}
|
||||||
|
?active=${workspace.uuid === gZenWorkspaces.activeWorkspace}
|
||||||
|
>
|
||||||
|
<div class="zen-library-space-header 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);
|
||||||
1016
src/zen/library/zen-library.css
Normal file
1016
src/zen/library/zen-library.css
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ DIRS += [
|
|||||||
"compact-mode",
|
"compact-mode",
|
||||||
"drag-and-drop",
|
"drag-and-drop",
|
||||||
"glance",
|
"glance",
|
||||||
|
"library",
|
||||||
"live-folders",
|
"live-folders",
|
||||||
"mods",
|
"mods",
|
||||||
"tests",
|
"tests",
|
||||||
|
|||||||
@@ -383,6 +383,12 @@ class nsZenWindowSync {
|
|||||||
|
|
||||||
handleEvent(aEvent) {
|
handleEvent(aEvent) {
|
||||||
const window = aEvent.currentTarget.documentGlobal ?? aEvent.currentTarget;
|
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 (
|
if (
|
||||||
!window.gZenStartup.isReady ||
|
!window.gZenStartup.isReady ||
|
||||||
!window.gZenWorkspaces?.shouldHaveWorkspaces ||
|
!window.gZenWorkspaces?.shouldHaveWorkspaces ||
|
||||||
|
|||||||
@@ -437,17 +437,7 @@ class nsZenShareManager extends nsZenDOMOperatedFeature {
|
|||||||
// Shake the new split group in the sidebar.
|
// Shake the new split group in the sidebar.
|
||||||
const group = tabs[0]?.group;
|
const group = tabs[0]?.group;
|
||||||
if (group?.hasAttribute("split-view-group")) {
|
if (group?.hasAttribute("split-view-group")) {
|
||||||
requestAnimationFrame(() => {
|
gZenUIManager.shakeElement(group);
|
||||||
gZenUIManager.motion
|
|
||||||
.animate(
|
|
||||||
group,
|
|
||||||
{ x: [-32, 0] },
|
|
||||||
{ type: "spring", bounce: 0.5, duration: 1.5 }
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
group.style.removeProperty("transform");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1367,8 +1367,8 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
if (themedColors.length === 2) {
|
if (themedColors.length === 2) {
|
||||||
if (!forToolbar) {
|
if (!forToolbar) {
|
||||||
return [
|
return [
|
||||||
`linear-gradient(${rotation}deg, ${this.#getSingleRGBColor(themedColors[1], forToolbar)} 20%, transparent 100%)`,
|
`linear-gradient(${rotation}deg, ${this.#getSingleRGBColor(themedColors[1], forToolbar)} 30%, transparent 120%)`,
|
||||||
`linear-gradient(${rotation + 180}deg, ${this.#getSingleRGBColor(themedColors[0], forToolbar)} 20%, transparent 100%)`,
|
`linear-gradient(${rotation + 180}deg, ${this.#getSingleRGBColor(themedColors[0], forToolbar)} 30%, transparent 120%)`,
|
||||||
]
|
]
|
||||||
.reverse()
|
.reverse()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
@@ -1542,14 +1542,21 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
browser.gZenThemePicker.invalidateGradientCache(uuid);
|
browser.gZenThemePicker.invalidateGradientCache(uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not rebuild if the workspace is not the same as the current one
|
// Only rebuild for the workspace the window shows, or the one its
|
||||||
|
// picker is editing without showing it.
|
||||||
const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace();
|
const windowWorkspace = browser.gZenWorkspaces.getActiveWorkspace();
|
||||||
if (windowWorkspace.uuid !== uuid) {
|
const appliesToWindow = windowWorkspace.uuid === uuid;
|
||||||
|
if (
|
||||||
|
!appliesToWindow &&
|
||||||
|
browser.gZenThemePicker.editingWorkspaceId !== uuid
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the theme from the window
|
// get the theme from the window
|
||||||
workspaceTheme = this.fixTheme(theme || windowWorkspace.theme);
|
workspaceTheme = this.fixTheme(
|
||||||
|
theme || (appliesToWindow ? windowWorkspace : workspace).theme
|
||||||
|
);
|
||||||
const docElement = browser.document.documentElement;
|
const docElement = browser.document.documentElement;
|
||||||
|
|
||||||
if (!skipUpdate) {
|
if (!skipUpdate) {
|
||||||
@@ -1560,7 +1567,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (theme) {
|
if (theme && appliesToWindow) {
|
||||||
const workspaceElement = browser.gZenWorkspaces.workspaceElement(
|
const workspaceElement = browser.gZenWorkspaces.workspaceElement(
|
||||||
windowWorkspace.uuid
|
windowWorkspace.uuid
|
||||||
);
|
);
|
||||||
@@ -1569,7 +1576,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipUpdate) {
|
if (!skipUpdate && appliesToWindow) {
|
||||||
let backgroundElement =
|
let backgroundElement =
|
||||||
browser.gZenThemePicker.browserBackgroundElement;
|
browser.gZenThemePicker.browserBackgroundElement;
|
||||||
let toolbarElement = browser.gZenThemePicker.toolbarBackgroundElement;
|
let toolbarElement = browser.gZenThemePicker.toolbarBackgroundElement;
|
||||||
@@ -1724,7 +1731,9 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
workspaceTheme.gradientColors,
|
workspaceTheme.gradientColors,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
if (appliesToWindow) {
|
||||||
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
|
browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
|
||||||
|
}
|
||||||
|
|
||||||
browser.gZenThemePicker.customColorList.innerHTML = "";
|
browser.gZenThemePicker.customColorList.innerHTML = "";
|
||||||
for (const dot of workspaceTheme.gradientColors) {
|
for (const dot of workspaceTheme.gradientColors) {
|
||||||
@@ -1733,6 +1742,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (appliesToWindow) {
|
||||||
browser.gZenThemePicker.toolbarBackgroundElement.style.setProperty(
|
browser.gZenThemePicker.toolbarBackgroundElement.style.setProperty(
|
||||||
"--zen-main-browser-background-toolbar",
|
"--zen-main-browser-background-toolbar",
|
||||||
gradientToolbar
|
gradientToolbar
|
||||||
@@ -1751,15 +1761,20 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
// Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
|
// 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.
|
// so instead get the pref value directly.
|
||||||
browser.gZenThemePicker.isLegacyVersion =
|
browser.gZenThemePicker.isLegacyVersion =
|
||||||
Services.prefs.getIntPref("zen.theme.gradient-legacy-version", 1) ===
|
Services.prefs.getIntPref(
|
||||||
0;
|
"zen.theme.gradient-legacy-version",
|
||||||
|
1
|
||||||
|
) === 0;
|
||||||
|
|
||||||
let isDarkMode = isDarkModeWindow;
|
let isDarkMode = isDarkModeWindow;
|
||||||
if (!isDefaultTheme && !this.isLegacyVersion) {
|
if (!isDefaultTheme && !this.isLegacyVersion) {
|
||||||
// Check for the primary color
|
// Check for the primary color
|
||||||
isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
|
isDarkMode =
|
||||||
|
browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
|
||||||
docElement.setAttribute("zen-should-be-dark-mode", isDarkMode);
|
docElement.setAttribute("zen-should-be-dark-mode", isDarkMode);
|
||||||
browser.gZenThemePicker.panel.removeAttribute("invalidate-controls");
|
browser.gZenThemePicker.panel.removeAttribute(
|
||||||
|
"invalidate-controls"
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
docElement.removeAttribute("zen-should-be-dark-mode");
|
docElement.removeAttribute("zen-should-be-dark-mode");
|
||||||
if (!this.isLegacyVersion) {
|
if (!this.isLegacyVersion) {
|
||||||
@@ -1787,6 +1802,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
isDarkMode ? "dark" : "light"
|
isDarkMode ? "dark" : "light"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!skipUpdate) {
|
if (!skipUpdate) {
|
||||||
browser.gZenThemePicker.dots = [];
|
browser.gZenThemePicker.dots = [];
|
||||||
@@ -1878,6 +1894,36 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editingWorkspaceId = null;
|
||||||
|
|
||||||
|
get editingWorkspaceId() {
|
||||||
|
return this.#editingWorkspaceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get workspaceBeingEdited() {
|
||||||
|
return (
|
||||||
|
gZenWorkspaces.getWorkspaceFromId(this.#editingWorkspaceId) ??
|
||||||
|
gZenWorkspaces.getActiveWorkspace()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the picker for a space without switching to it.
|
||||||
|
*
|
||||||
|
* @param {object} workspace - The space to edit
|
||||||
|
* @param {Element} anchor - Element to anchor the panel to
|
||||||
|
* @param {Event} event - The triggering event
|
||||||
|
*/
|
||||||
|
openThemePickerForWorkspace(workspace, anchor, event) {
|
||||||
|
this.#editingWorkspaceId = workspace.uuid;
|
||||||
|
this.onWorkspaceChange(workspace);
|
||||||
|
this.panel.removeAttribute("hidepopovertail");
|
||||||
|
PanelMultiView.openPopup(this.panel, anchor, {
|
||||||
|
position: "bottomleft topleft",
|
||||||
|
triggerEvent: event,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateCurrentWorkspace(skipSave = true) {
|
updateCurrentWorkspace(skipSave = true) {
|
||||||
this.updated = skipSave;
|
this.updated = skipSave;
|
||||||
const dots = this.panel.querySelectorAll(".zen-theme-picker-dot");
|
const dots = this.panel.querySelectorAll(".zen-theme-picker-dot");
|
||||||
@@ -1916,7 +1962,7 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
this.currentOpacity,
|
this.currentOpacity,
|
||||||
this.currentTexture
|
this.currentTexture
|
||||||
);
|
);
|
||||||
let currentWorkspace = gZenWorkspaces.getActiveWorkspace();
|
let currentWorkspace = this.workspaceBeingEdited;
|
||||||
|
|
||||||
currentWorkspace.theme = gradient;
|
currentWorkspace.theme = gradient;
|
||||||
if (!skipSave) {
|
if (!skipSave) {
|
||||||
@@ -1928,6 +1974,10 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
skipSave,
|
skipSave,
|
||||||
skipSave ? gradient : null
|
skipSave ? gradient : null
|
||||||
);
|
);
|
||||||
|
if (currentWorkspace.uuid !== gZenWorkspaces.activeWorkspace) {
|
||||||
|
this.invalidateGradientCache(currentWorkspace.uuid);
|
||||||
|
Services.obs.notifyObservers(null, "zen-space-gradient-update");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePanelClose() {
|
handlePanelClose() {
|
||||||
@@ -1935,6 +1985,11 @@ export class nsZenThemePicker extends nsZenMultiWindowFeature {
|
|||||||
this.updateCurrentWorkspace(false);
|
this.updateCurrentWorkspace(false);
|
||||||
}
|
}
|
||||||
this.uninitThemePicker();
|
this.uninitThemePicker();
|
||||||
|
if (this.#editingWorkspaceId) {
|
||||||
|
this.#editingWorkspaceId = null;
|
||||||
|
this.panel.setAttribute("hidepopovertail", "true");
|
||||||
|
this.onWorkspaceChange(gZenWorkspaces.getActiveWorkspace());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePanelOpen() {
|
handlePanelOpen() {
|
||||||
|
|||||||
@@ -757,6 +757,9 @@ class nsZenWorkspaces {
|
|||||||
: [this.#createWorkspaceData("Space", undefined)];
|
: [this.#createWorkspaceData("Space", undefined)];
|
||||||
this.activeWorkspace =
|
this.activeWorkspace =
|
||||||
aWinData.activeZenSpace || this._workspaceCache[0].uuid;
|
aWinData.activeZenSpace || this._workspaceCache[0].uuid;
|
||||||
|
if (aWinData.selected) {
|
||||||
|
this._sessionSelected = aWinData.selected;
|
||||||
|
}
|
||||||
let promise = this.#initializeWorkspaces();
|
let promise = this.#initializeWorkspaces();
|
||||||
for (const workspace of spacesFromStore) {
|
for (const workspace of spacesFromStore) {
|
||||||
const element = this.workspaceElement(workspace.uuid);
|
const element = this.workspaceElement(workspace.uuid);
|
||||||
@@ -834,6 +837,7 @@ class nsZenWorkspaces {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
|
delete this._sessionSelected;
|
||||||
delete this._tabToSelect;
|
delete this._tabToSelect;
|
||||||
delete this._tabToRemoveForEmpty;
|
delete this._tabToRemoveForEmpty;
|
||||||
delete this._shouldOverrideTabs;
|
delete this._shouldOverrideTabs;
|
||||||
@@ -862,6 +866,11 @@ class nsZenWorkspaces {
|
|||||||
!this._shouldOverrideTabs
|
!this._shouldOverrideTabs
|
||||||
) {
|
) {
|
||||||
const tabs = gBrowser.tabs.filter(tab => !tab.collapsed);
|
const tabs = gBrowser.tabs.filter(tab => !tab.collapsed);
|
||||||
|
if (
|
||||||
|
Services.prefs.getBoolPref("zen.workspaces.continue-where-left-off")
|
||||||
|
) {
|
||||||
|
this._tabToSelect = this._sessionSelected - 1;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
typeof this._tabToSelect === "number" &&
|
typeof this._tabToSelect === "number" &&
|
||||||
this._tabToSelect >= 0 &&
|
this._tabToSelect >= 0 &&
|
||||||
|
|||||||
@@ -12,15 +12,23 @@ ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
|
|||||||
return document.getElementById("zen-toolbar-background");
|
return document.getElementById("zen-toolbar-background");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(
|
||||||
|
lazy,
|
||||||
|
{ ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs" },
|
||||||
|
{ global: "current" }
|
||||||
|
);
|
||||||
|
|
||||||
export class ZenSpacesSwipe {
|
export class ZenSpacesSwipe {
|
||||||
_swipeState = {
|
_swipeState = {
|
||||||
isGestureActive: false,
|
isGestureActive: false,
|
||||||
lastDelta: 0,
|
lastDelta: 0,
|
||||||
direction: null,
|
direction: null,
|
||||||
|
isSwipingLibrary: false,
|
||||||
|
beforeLibraryState: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.#attachWorkspaceSwipeGestures(gNavToolbox);
|
this.attachWorkspaceSwipeGestures(gNavToolbox);
|
||||||
this._popupOpenHandler = this._popupOpenHandler.bind(this);
|
this._popupOpenHandler = this._popupOpenHandler.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,20 +43,39 @@ export class ZenSpacesSwipe {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#attachWorkspaceSwipeGestures(element) {
|
#readySwipeOpenLibrary() {
|
||||||
|
const spaces = gZenWorkspaces.getWorkspaces();
|
||||||
|
const current = gZenWorkspaces.getActiveWorkspaceFromCache();
|
||||||
|
const libraryEnabled = Services.prefs.getBoolPref("zen.library.enabled");
|
||||||
|
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
|
||||||
|
return (
|
||||||
|
spaces.indexOf(current) === (libraryOnRight ? spaces.length - 1 : 0) &&
|
||||||
|
libraryEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attachWorkspaceSwipeGestures(element) {
|
||||||
|
const gestureControl = {
|
||||||
|
_handleSwipeMayStart: this._handleSwipeMayStart.bind(this),
|
||||||
|
_handleSwipeStart: this._handleSwipeStart.bind(this),
|
||||||
|
_handleSwipeUpdate: this._handleSwipeUpdate.bind(this),
|
||||||
|
_handleSwipeEnd: this._handleSwipeEnd.bind(this),
|
||||||
|
_handleSwipeAnimationEnd: this.onSwipeGestureAnimationEnd.bind(this),
|
||||||
|
};
|
||||||
|
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
"MozSwipeGestureMayStart",
|
"MozSwipeGestureMayStart",
|
||||||
this._handleSwipeMayStart.bind(this),
|
gestureControl._handleSwipeMayStart,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
"MozSwipeGestureStart",
|
"MozSwipeGestureStart",
|
||||||
this._handleSwipeStart.bind(this),
|
gestureControl._handleSwipeStart,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
"MozSwipeGestureUpdate",
|
"MozSwipeGestureUpdate",
|
||||||
this._handleSwipeUpdate.bind(this),
|
gestureControl._handleSwipeUpdate,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,15 +83,47 @@ export class ZenSpacesSwipe {
|
|||||||
// while MozSwipeGesture is fired immediately after swipe ends.
|
// while MozSwipeGesture is fired immediately after swipe ends.
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
"MozSwipeGesture",
|
"MozSwipeGesture",
|
||||||
this._handleSwipeEnd.bind(this),
|
gestureControl._handleSwipeEnd,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
element.addEventListener(
|
element.addEventListener(
|
||||||
"MozSwipeGestureEnd",
|
"MozSwipeGestureEnd",
|
||||||
() => {
|
gestureControl._handleSwipeAnimationEnd,
|
||||||
this.onSwipeGestureAnimationEnd();
|
true
|
||||||
},
|
);
|
||||||
|
|
||||||
|
return gestureControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
detachWorkspaceSwipeGestures(element, gestureControl) {
|
||||||
|
element.removeEventListener(
|
||||||
|
"MozSwipeGestureMayStart",
|
||||||
|
gestureControl._handleSwipeMayStart,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
element.removeEventListener(
|
||||||
|
"MozSwipeGestureStart",
|
||||||
|
gestureControl._handleSwipeStart,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
element.removeEventListener(
|
||||||
|
"MozSwipeGestureUpdate",
|
||||||
|
gestureControl._handleSwipeUpdate,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use MozSwipeGesture instead of MozSwipeGestureEnd because MozSwipeGestureEnd is fired after animation ends,
|
||||||
|
// while MozSwipeGesture is fired immediately after swipe ends.
|
||||||
|
element.removeEventListener(
|
||||||
|
"MozSwipeGesture",
|
||||||
|
gestureControl._handleSwipeEnd,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
element.removeEventListener(
|
||||||
|
"MozSwipeGestureEnd",
|
||||||
|
gestureControl._handleSwipeAnimationEnd,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -115,6 +174,8 @@ export class ZenSpacesSwipe {
|
|||||||
isGestureActive: true,
|
isGestureActive: true,
|
||||||
lastDelta: 0,
|
lastDelta: 0,
|
||||||
direction: null,
|
direction: null,
|
||||||
|
isSwipingLibrary: false,
|
||||||
|
beforeLibraryState: 0,
|
||||||
};
|
};
|
||||||
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", true);
|
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", true);
|
||||||
}
|
}
|
||||||
@@ -154,6 +215,55 @@ export class ZenSpacesSwipe {
|
|||||||
this._swipeState.direction = delta > 0 ? "left" : "right";
|
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
|
// Apply a translateX to the tab strip to give the user feedback on the swipe
|
||||||
const currentWorkspace = ws.getActiveWorkspaceFromCache();
|
const currentWorkspace = ws.getActiveWorkspaceFromCache();
|
||||||
ws._organizeWorkspaceStripLocations(currentWorkspace, true, translateX);
|
ws._organizeWorkspaceStripLocations(currentWorkspace, true, translateX);
|
||||||
@@ -173,17 +283,29 @@ export class ZenSpacesSwipe {
|
|||||||
|
|
||||||
const rawDirection = moveForward ? 1 : -1;
|
const rawDirection = moveForward ? 1 : -1;
|
||||||
const direction = ws.naturalScroll ? -1 : 1;
|
const direction = ws.naturalScroll ? -1 : 1;
|
||||||
|
|
||||||
|
if (this._swipeState.isSwipingLibrary) {
|
||||||
|
lazy.ZenLibrary.stopSwipe(rawDirection * direction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await ws.changeWorkspaceShortcut(rawDirection * direction, true);
|
await ws.changeWorkspaceShortcut(rawDirection * direction, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSwipeGestureAnimationEnd() {
|
onSwipeGestureAnimationEnd() {
|
||||||
const ws = gZenWorkspaces;
|
const ws = gZenWorkspaces;
|
||||||
|
|
||||||
|
if (this._swipeState.isSwipingLibrary) {
|
||||||
|
lazy.ZenLibrary.stopSwipe(null);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset swipe state
|
// Reset swipe state
|
||||||
this._swipeState = {
|
this._swipeState = {
|
||||||
isGestureActive: false,
|
isGestureActive: false,
|
||||||
lastDelta: 0,
|
lastDelta: 0,
|
||||||
direction: null,
|
direction: null,
|
||||||
|
isSwipingLibrary: false,
|
||||||
|
beforeLibraryState: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);
|
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);
|
||||||
|
|||||||
@@ -1425,9 +1425,15 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
|||||||
* use -1 to avoid selecting any tab.
|
* use -1 to avoid selecting any tab.
|
||||||
* @param {object} options - Additional options.
|
* @param {object} options - Additional options.
|
||||||
* @param {string|null} options.groupFetchId - An optional group fetch ID.
|
* @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.
|
* @returns {object|undefined} The split view data or undefined if the split was not performed.
|
||||||
*/
|
*/
|
||||||
splitTabs(tabs, gridType, initialIndex = 0, { groupFetchId = null } = {}) {
|
splitTabs(
|
||||||
|
tabs,
|
||||||
|
gridType,
|
||||||
|
initialIndex = 0,
|
||||||
|
{ groupFetchId = null, activate = true } = {}
|
||||||
|
) {
|
||||||
const tabIndexToUse = Math.max(0, initialIndex);
|
const tabIndexToUse = Math.max(0, initialIndex);
|
||||||
return this.#withoutSplitViewTransition(() => {
|
return this.#withoutSplitViewTransition(() => {
|
||||||
// TODO: Add support for splitting essential tabs
|
// TODO: Add support for splitting essential tabs
|
||||||
@@ -1443,6 +1449,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
|||||||
|
|
||||||
const existingSplitTab = tabs.find(tab => tab.splitView);
|
const existingSplitTab = tabs.find(tab => tab.splitView);
|
||||||
let shouldActivateSplit =
|
let shouldActivateSplit =
|
||||||
|
activate &&
|
||||||
(initialIndex >= 0 || tabs.includes(window.gBrowser.selectedTab)) &&
|
(initialIndex >= 0 || tabs.includes(window.gBrowser.selectedTab)) &&
|
||||||
!this._sessionRestoring;
|
!this._sessionRestoring;
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,22 @@ export function recordDigest(kind, data) {
|
|||||||
class nsZenSpacesSyncModel {
|
class nsZenSpacesSyncModel {
|
||||||
#file = null;
|
#file = null;
|
||||||
#cache = null;
|
#cache = null;
|
||||||
|
/**
|
||||||
|
* Ids applied from an incoming batch, keyed to the projection generation
|
||||||
|
* (sidebar lastCollected) that was current when they were applied. The
|
||||||
|
* projection only refreshes on the delayed session collection that runs
|
||||||
|
* after the sync, so within the same sync the just-applied ids are in the
|
||||||
|
* uploaded snapshot (noteApplied) but not yet in the projection. Without
|
||||||
|
* this, the outgoing diff would read that gap as local deletions and
|
||||||
|
* upload tombstones for records it just downloaded, wiping them on every
|
||||||
|
* other device (gh-15426).
|
||||||
|
*/
|
||||||
|
#appliedStamp = new Map();
|
||||||
|
|
||||||
|
/** The projection generation the current sidebar data belongs to. */
|
||||||
|
#currentStamp() {
|
||||||
|
return lazy.ZenSessionStore.getSidebarData()?.lastCollected || 0;
|
||||||
|
}
|
||||||
|
|
||||||
#data() {
|
#data() {
|
||||||
if (!this.#file) {
|
if (!this.#file) {
|
||||||
@@ -743,6 +759,7 @@ class nsZenSpacesSyncModel {
|
|||||||
const uploaded = this.#data().uploaded;
|
const uploaded = this.#data().uploaded;
|
||||||
const current = this.#digestAll();
|
const current = this.#digestAll();
|
||||||
const pending = this.#pendingIds();
|
const pending = this.#pendingIds();
|
||||||
|
const stamp = this.#currentStamp();
|
||||||
const now = Date.now() / 1000;
|
const now = Date.now() / 1000;
|
||||||
const changes = {};
|
const changes = {};
|
||||||
for (const [id, digest] of current) {
|
for (const [id, digest] of current) {
|
||||||
@@ -751,7 +768,11 @@ class nsZenSpacesSyncModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const id of Object.keys(uploaded)) {
|
for (const id of Object.keys(uploaded)) {
|
||||||
if (!current.has(id) && !pending.has(id)) {
|
if (
|
||||||
|
!current.has(id) &&
|
||||||
|
!pending.has(id) &&
|
||||||
|
this.#appliedStamp.get(id) !== stamp
|
||||||
|
) {
|
||||||
changes[id] = now;
|
changes[id] = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -774,13 +795,18 @@ class nsZenSpacesSyncModel {
|
|||||||
const uploaded = this.#data().uploaded;
|
const uploaded = this.#data().uploaded;
|
||||||
const current = this.#digestAll();
|
const current = this.#digestAll();
|
||||||
const pending = this.#pendingIds();
|
const pending = this.#pendingIds();
|
||||||
|
const stamp = this.#currentStamp();
|
||||||
for (const [id, digest] of current) {
|
for (const [id, digest] of current) {
|
||||||
if (uploaded[id] !== digest) {
|
if (uploaded[id] !== digest) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const id of Object.keys(uploaded)) {
|
for (const id of Object.keys(uploaded)) {
|
||||||
if (!current.has(id) && !pending.has(id)) {
|
if (
|
||||||
|
!current.has(id) &&
|
||||||
|
!pending.has(id) &&
|
||||||
|
this.#appliedStamp.get(id) !== stamp
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -825,8 +851,12 @@ class nsZenSpacesSyncModel {
|
|||||||
const data = this.#data();
|
const data = this.#data();
|
||||||
if (!cleartext) {
|
if (!cleartext) {
|
||||||
delete data.uploaded[id];
|
delete data.uploaded[id];
|
||||||
|
this.#appliedStamp.delete(id);
|
||||||
} else {
|
} else {
|
||||||
data.uploaded[id] = recordDigest(cleartext.kind, cleartext.data);
|
data.uploaded[id] = recordDigest(cleartext.kind, cleartext.data);
|
||||||
|
// Hold back a tombstone for this id until the projection is recollected
|
||||||
|
// past the generation it was applied in (gh-15426).
|
||||||
|
this.#appliedStamp.set(id, this.#currentStamp());
|
||||||
}
|
}
|
||||||
syncLog(
|
syncLog(
|
||||||
`acknowledged incoming ${cleartext ? cleartext.kind : "tombstone"} ${id}`
|
`acknowledged incoming ${cleartext ? cleartext.kind : "tombstone"} ${id}`
|
||||||
|
|||||||
@@ -83,7 +83,8 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#browser {
|
#browser,
|
||||||
|
zen-library {
|
||||||
--zen-min-toolbox-padding: 5px;
|
--zen-min-toolbox-padding: 5px;
|
||||||
@media (-moz-platform: macos) {
|
@media (-moz-platform: macos) {
|
||||||
--zen-min-toolbox-padding: 6px;
|
--zen-min-toolbox-padding: 6px;
|
||||||
@@ -229,7 +230,8 @@
|
|||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
Navigator Toolbox (Main Sidebar Container) Base Styles
|
Navigator Toolbox (Main Sidebar Container) Base Styles
|
||||||
========================================================================== */
|
========================================================================== */
|
||||||
#navigator-toolbox {
|
#navigator-toolbox,
|
||||||
|
.zen-library-space-body {
|
||||||
/* Define theme variables, including platform specifics for native look */
|
/* Define theme variables, including platform specifics for native look */
|
||||||
--zen-toolbox-min-width: 1px;
|
--zen-toolbox-min-width: 1px;
|
||||||
--border-radius-medium: 14px;
|
--border-radius-medium: 14px;
|
||||||
@@ -452,7 +454,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (-moz-platform: macos) {
|
||||||
|
#zen-sidebar-foot-buttons,
|
||||||
|
#zen-library-footer {
|
||||||
|
--zen-toolbar-button-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#zen-sidebar-foot-buttons {
|
#zen-sidebar-foot-buttons {
|
||||||
|
position: relative;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -460,6 +470,10 @@
|
|||||||
& > toolbarbutton:not(#zen-workspaces-button) {
|
& > toolbarbutton:not(#zen-workspaces-button) {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[zen-right-side="true"][zen-sidebar-expanded="true"] & {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#tabbrowser-arrowscrollbox {
|
#tabbrowser-arrowscrollbox {
|
||||||
@@ -508,7 +522,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#navigator-toolbox[zen-sidebar-expanded="true"] {
|
:is(#navigator-toolbox, .zen-library-space-body)[zen-sidebar-expanded="true"] {
|
||||||
--zen-toolbox-min-width: fit-content;
|
--zen-toolbox-min-width: fit-content;
|
||||||
|
|
||||||
padding: var(--zen-toolbox-padding);
|
padding: var(--zen-toolbox-padding);
|
||||||
@@ -564,7 +578,6 @@
|
|||||||
|
|
||||||
& #zen-sidebar-foot-buttons {
|
& #zen-sidebar-foot-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -877,6 +890,10 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[zen-library-open="true"] #zen-sidebar-splitter {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
#zen-sidebar-splitter {
|
#zen-sidebar-splitter {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
max-width: var(--zen-toolbox-padding) !important;
|
max-width: var(--zen-toolbox-padding) !important;
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ add_task(async function test_Issue_9981() {
|
|||||||
|
|
||||||
const tab2ResetButton = tab2.querySelector(".tab-reset-button");
|
const tab2ResetButton = tab2.querySelector(".tab-reset-button");
|
||||||
tab2ResetButton.style.display = "flex";
|
tab2ResetButton.style.display = "flex";
|
||||||
await TestUtils.waitForCondition(
|
await clickWhenReady(tab2ResetButton);
|
||||||
() => tab2ResetButton.getBoundingClientRect().width != 0
|
|
||||||
);
|
|
||||||
EventUtils.synthesizeMouseAtCenter(tab2ResetButton, {});
|
|
||||||
|
|
||||||
await TestUtils.waitForCondition(
|
await TestUtils.waitForCondition(
|
||||||
() => !tab2.hasAttribute("folder-active"),
|
() => !tab2.hasAttribute("folder-active"),
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ add_task(async function test_Issue_() {
|
|||||||
"Tab 2 should be in the active folder"
|
"Tab 2 should be in the active folder"
|
||||||
);
|
);
|
||||||
|
|
||||||
EventUtils.synthesizeMouseAtCenter(folder.resetButton, {});
|
EventUtils.synthesizeMouseAtCenter(folder.labelElement, {
|
||||||
|
type: "mousemove",
|
||||||
|
});
|
||||||
|
await clickWhenReady(folder.resetButton);
|
||||||
|
|
||||||
await new Promise(resolve =>
|
await new Promise(resolve =>
|
||||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||||
|
|||||||
@@ -26,3 +26,175 @@ add_task(async function test_Create_Folder() {
|
|||||||
await removeFolder(subfolder);
|
await removeFolder(subfolder);
|
||||||
await removeFolder(parent);
|
await removeFolder(parent);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
add_task(async function test_Collapsed_Subfolder_Stays_Collapsed() {
|
||||||
|
const originalTab = gBrowser.selectedTab;
|
||||||
|
const activeTab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab1");
|
||||||
|
const hiddenTab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab2");
|
||||||
|
const parentTab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab3");
|
||||||
|
const subfolder = await gZenFolders.createFolder([activeTab, hiddenTab], {
|
||||||
|
renameFolder: false,
|
||||||
|
label: "subfolder",
|
||||||
|
});
|
||||||
|
const parent = await gZenFolders.createFolder([parentTab], {
|
||||||
|
renameFolder: false,
|
||||||
|
label: "parent",
|
||||||
|
});
|
||||||
|
parentTab.after(subfolder);
|
||||||
|
gBrowser.selectedTab = activeTab;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const height = tab => tab.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
subfolder.collapsed = true;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => !height(hiddenTab),
|
||||||
|
"Unselected tab is hidden when the subfolder collapses"
|
||||||
|
);
|
||||||
|
|
||||||
|
parent.collapsed = true;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => !height(parentTab),
|
||||||
|
"Parent tab is hidden when the parent folder collapses"
|
||||||
|
);
|
||||||
|
|
||||||
|
parent.collapsed = false;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => height(parentTab) && !parent.hasAttribute("has-active"),
|
||||||
|
"Parent folder is expanded again"
|
||||||
|
);
|
||||||
|
await TestUtils.waitForTick();
|
||||||
|
|
||||||
|
ok(subfolder.collapsed, "Subfolder is still collapsed");
|
||||||
|
ok(height(activeTab), "Active tab of the subfolder is still visible");
|
||||||
|
Assert.equal(
|
||||||
|
height(hiddenTab),
|
||||||
|
0,
|
||||||
|
"Unselected tab of the collapsed subfolder stays hidden"
|
||||||
|
);
|
||||||
|
|
||||||
|
gBrowser.selectedTab = originalTab;
|
||||||
|
await removeFolder(subfolder);
|
||||||
|
await removeFolder(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_Spam_Toggle_Parent_Folder() {
|
||||||
|
const originalTab = gBrowser.selectedTab;
|
||||||
|
const activeTab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab1");
|
||||||
|
const hiddenTab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab2");
|
||||||
|
const parentTab = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab3");
|
||||||
|
const subfolder = await gZenFolders.createFolder([activeTab, hiddenTab], {
|
||||||
|
renameFolder: false,
|
||||||
|
label: "subfolder",
|
||||||
|
});
|
||||||
|
const parent = await gZenFolders.createFolder([parentTab], {
|
||||||
|
renameFolder: false,
|
||||||
|
label: "parent",
|
||||||
|
});
|
||||||
|
parentTab.after(subfolder);
|
||||||
|
gBrowser.selectedTab = activeTab;
|
||||||
|
// createFolder sets the initial collapsed state on a timeout.
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const height = tab => tab.getBoundingClientRect().height;
|
||||||
|
const indent = tab => tab.style.getPropertyValue("--zen-folder-indent");
|
||||||
|
|
||||||
|
subfolder.collapsed = true;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => !height(hiddenTab),
|
||||||
|
"Unselected tab is hidden when the subfolder collapses"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
parent.collapsed = !parent.collapsed;
|
||||||
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
}
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => !height(parentTab),
|
||||||
|
"Parent tab is hidden when the parent folder ends up collapsed"
|
||||||
|
);
|
||||||
|
|
||||||
|
ok(parent.collapsed, "Parent folder is collapsed");
|
||||||
|
ok(parent.hasAttribute("has-active"), "Parent folder is still active");
|
||||||
|
Assert.deepEqual(parent.activeTabs, [activeTab], "Active tab is kept");
|
||||||
|
ok(height(activeTab), "Active tab is still visible");
|
||||||
|
Assert.equal(indent(activeTab), "0px", "Active tab lost its indentation");
|
||||||
|
Assert.equal(height(hiddenTab), 0, "Unselected tab stays hidden");
|
||||||
|
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
parent.collapsed = !parent.collapsed;
|
||||||
|
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||||
|
}
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => height(parentTab),
|
||||||
|
"Parent tab is visible when the parent folder ends up expanded"
|
||||||
|
);
|
||||||
|
|
||||||
|
ok(!parent.hasAttribute("has-active"), "Parent folder is not active");
|
||||||
|
ok(subfolder.hasAttribute("has-active"), "Subfolder is still active");
|
||||||
|
Assert.equal(indent(activeTab), "14px", "Active tab is indented again");
|
||||||
|
Assert.equal(height(hiddenTab), 0, "Unselected tab stays hidden");
|
||||||
|
|
||||||
|
gBrowser.selectedTab = originalTab;
|
||||||
|
await removeFolder(subfolder);
|
||||||
|
await removeFolder(parent);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_Select_All_Tabs_Expands_Subfolder() {
|
||||||
|
const originalTab = gBrowser.selectedTab;
|
||||||
|
const tab1 = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab1");
|
||||||
|
const tab2 = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab2");
|
||||||
|
const tab3 = BrowserTestUtils.addTab(gBrowser, "data:text/html,tab3");
|
||||||
|
const subfolder = await gZenFolders.createFolder([tab2, tab3], {
|
||||||
|
renameFolder: false,
|
||||||
|
label: "subfolder",
|
||||||
|
});
|
||||||
|
const parent = await gZenFolders.createFolder([tab1], {
|
||||||
|
renameFolder: false,
|
||||||
|
label: "parent",
|
||||||
|
});
|
||||||
|
tab1.after(subfolder);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 0));
|
||||||
|
|
||||||
|
const height = tab => tab.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
subfolder.collapsed = true;
|
||||||
|
gBrowser.selectedTab = tab1;
|
||||||
|
parent.collapsed = true;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => !height(tab2) && !height(tab3),
|
||||||
|
"Subfolder tabs are hidden when the parent folder collapses"
|
||||||
|
);
|
||||||
|
|
||||||
|
gBrowser.selectedTab = tab3;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => height(tab3) && !parent.collapsed,
|
||||||
|
"Parent folder is expanded when tab 3 is selected"
|
||||||
|
);
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => !height(tab2),
|
||||||
|
"Tab 2 stays hidden inside of the collapsed subfolder"
|
||||||
|
);
|
||||||
|
ok(!parent.hasAttribute("has-active"), "Parent folder is not active");
|
||||||
|
ok(subfolder.collapsed, "Subfolder is still collapsed");
|
||||||
|
ok(subfolder.hasAttribute("has-active"), "Subfolder is active");
|
||||||
|
Assert.deepEqual(subfolder.activeTabs, [tab3], "Tab 3 is the active tab");
|
||||||
|
ok(!tab1.hasAttribute("folder-active"), "Tab 1 is not folder-active");
|
||||||
|
|
||||||
|
gBrowser.selectedTab = tab2;
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => height(tab2),
|
||||||
|
"Tab 2 is visible when selected"
|
||||||
|
);
|
||||||
|
ok(!parent.collapsed, "Parent folder is expanded");
|
||||||
|
ok(!subfolder.collapsed, "Subfolder is expanded");
|
||||||
|
ok(!parent.hasAttribute("has-active"), "Parent folder is not active");
|
||||||
|
ok(!subfolder.hasAttribute("has-active"), "Subfolder is not active");
|
||||||
|
for (const tab of [tab1, tab2, tab3]) {
|
||||||
|
ok(!tab.hasAttribute("folder-active"), "Tab is not folder-active");
|
||||||
|
}
|
||||||
|
|
||||||
|
gBrowser.selectedTab = originalTab;
|
||||||
|
await removeFolder(subfolder);
|
||||||
|
await removeFolder(parent);
|
||||||
|
});
|
||||||
|
|||||||
@@ -32,3 +32,17 @@ async function addTabTo(
|
|||||||
await BrowserTestUtils.browserLoaded(browser);
|
await BrowserTestUtils.browserLoaded(browser);
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clickWhenReady(button) {
|
||||||
|
await TestUtils.waitForCondition(() => {
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.width &&
|
||||||
|
document.elementFromPoint(
|
||||||
|
rect.x + rect.width / 2,
|
||||||
|
rect.y + rect.height / 2
|
||||||
|
) === button
|
||||||
|
);
|
||||||
|
}, "Button should be clickable");
|
||||||
|
EventUtils.synthesizeMouseAtCenter(button, {});
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,12 +38,12 @@ https_first_disabled = true
|
|||||||
|
|
||||||
["browser_mute.js"]
|
["browser_mute.js"]
|
||||||
|
|
||||||
|
["browser_mute2.js"]
|
||||||
|
|
||||||
["browser_mute_persist_navigation.js"]
|
["browser_mute_persist_navigation.js"]
|
||||||
|
|
||||||
["browser_mute_restore_closed_audible_tab.js"]
|
["browser_mute_restore_closed_audible_tab.js"]
|
||||||
|
|
||||||
["browser_mute2.js"]
|
|
||||||
|
|
||||||
["browser_mute_webAudio.js"]
|
["browser_mute_webAudio.js"]
|
||||||
|
|
||||||
["browser_sound_indicator_silent_video.js"]
|
["browser_sound_indicator_silent_video.js"]
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const globalActionsTemplate = [
|
|||||||
{
|
{
|
||||||
l10nId: "zen-action-open-theme-picker",
|
l10nId: "zen-action-open-theme-picker",
|
||||||
command: "cmd_zenOpenZenThemePicker",
|
command: "cmd_zenOpenZenThemePicker",
|
||||||
icon: "chrome://browser/skin/zen-icons/edit-theme.svg",
|
icon: "chrome://browser/skin/zen-icons/paintbrush-fill.svg",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
l10nId: "zen-action-new-split-view",
|
l10nId: "zen-action-new-split-view",
|
||||||
@@ -302,6 +302,10 @@ const globalActionsTemplate = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export function formatValueSync(l10nId) {
|
||||||
|
return lazy.l10n.formatValueSync(l10nId);
|
||||||
|
}
|
||||||
|
|
||||||
export const globalActions = globalActionsTemplate.map(action => ({
|
export const globalActions = globalActionsTemplate.map(action => ({
|
||||||
isAvailable: window => {
|
isAvailable: window => {
|
||||||
return (
|
return (
|
||||||
@@ -317,6 +321,6 @@ export const globalActions = globalActionsTemplate.map(action => ({
|
|||||||
extraPayload: {},
|
extraPayload: {},
|
||||||
...action,
|
...action,
|
||||||
get label() {
|
get label() {
|
||||||
return lazy.l10n.formatValueSync(action.l10nId);
|
return formatValueSync(action.l10nId);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
|||||||
/* eslint-disable mozilla/valid-lazy */
|
/* eslint-disable mozilla/valid-lazy */
|
||||||
ZenUrlbarProviderGlobalActions:
|
ZenUrlbarProviderGlobalActions:
|
||||||
"resource:///modules/ZenUBActionsProvider.sys.mjs",
|
"resource:///modules/ZenUBActionsProvider.sys.mjs",
|
||||||
|
ZenUrlbarProviderSidebar: "resource:///modules/ZenUBSidebarProvider.sys.mjs",
|
||||||
});
|
});
|
||||||
|
|
||||||
export function registerZenUrlbarProviders() {
|
export function registerZenUrlbarProviders() {
|
||||||
|
|||||||
234
src/zen/urlbar/ZenUBSidebarProvider.sys.mjs
Normal file
234
src/zen/urlbar/ZenUBSidebarProvider.sys.mjs
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/* 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 {
|
||||||
|
UrlbarProvider,
|
||||||
|
UrlbarUtils,
|
||||||
|
} from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
|
||||||
|
import { UrlbarShared } from "chrome://browser/content/urlbar/UrlbarShared.mjs";
|
||||||
|
import { formatValueSync } from "resource:///modules/ZenUBGlobalActions.sys.mjs";
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
UrlbarResult: "chrome://browser/content/urlbar/UrlbarResult.mjs",
|
||||||
|
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs",
|
||||||
|
UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
|
||||||
|
UrlbarProviderOpenTabs:
|
||||||
|
"moz-src:///browser/components/urlbar/UrlbarProviderOpenTabs.sys.mjs",
|
||||||
|
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
const DYNAMIC_TYPE_NAME = "zen-folders";
|
||||||
|
|
||||||
|
export class ZenUrlbarProviderSidebar extends UrlbarProvider {
|
||||||
|
get name() {
|
||||||
|
return "ZenUrlbarProviderSidebar";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Values<typeof UrlbarShared.PROVIDER_TYPE>}
|
||||||
|
*/
|
||||||
|
get type() {
|
||||||
|
return UrlbarShared.PROVIDER_TYPE.PROFILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isActive(queryContext) {
|
||||||
|
return (
|
||||||
|
!queryContext.isPrivate &&
|
||||||
|
!!queryContext.tokens.length &&
|
||||||
|
queryContext.sources.includes(UrlbarShared.RESULT_SOURCE.TABS) &&
|
||||||
|
lazy.UrlbarPrefs.get("suggest.openpage")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async startQuery(queryContext, addCallback) {
|
||||||
|
const sidebar = lazy.ZenSessionStore.getSidebarData();
|
||||||
|
if (!sidebar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tokens = queryContext.tokens.map(t => t.lowerCaseValue);
|
||||||
|
const matches = (percentage, text) => {
|
||||||
|
text = text.toLowerCase();
|
||||||
|
const matchCount = tokens.filter(token => text.includes(token)).length;
|
||||||
|
return matchCount / tokens.length >= percentage;
|
||||||
|
};
|
||||||
|
this.#addFolders(sidebar, matches.bind(undefined, 0.7), addCallback);
|
||||||
|
this.#addTabs(
|
||||||
|
sidebar,
|
||||||
|
matches.bind(undefined, 0.4),
|
||||||
|
queryContext,
|
||||||
|
addCallback
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#addTabs(sidebar, matches, queryContext, addCallback) {
|
||||||
|
const openTabUrls = lazy.UrlbarProviderOpenTabs.getOpenTabUrls();
|
||||||
|
const activeSpace =
|
||||||
|
lazy.BrowserWindowTracker.getTopWindow().gZenWorkspaces.activeWorkspace;
|
||||||
|
let resultsAdded = 0;
|
||||||
|
for (const tabData of sidebar.tabs || []) {
|
||||||
|
// Essentials have no space, they are part of every one of them.
|
||||||
|
if (!tabData.zenEssential && tabData.zenWorkspace != activeSpace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const entries = tabData.entries || [];
|
||||||
|
const entry = entries[(tabData.index || entries.length) - 1];
|
||||||
|
const label = tabData.zenStaticLabel || entry?.title;
|
||||||
|
if (typeof label !== "string" || !label || !matches(label)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url = entry?.url;
|
||||||
|
if (!url || url == queryContext.currentPage) {
|
||||||
|
// Don't suggest switching to the current tab.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const userContextId = tabData.userContextId || 0;
|
||||||
|
// The sidebar data can lag behind a navigation, only offer tabs that
|
||||||
|
// are still registered as open with this url.
|
||||||
|
const openTab = [...(openTabUrls.get(url) || [])].find(
|
||||||
|
([contextId]) => contextId == userContextId
|
||||||
|
);
|
||||||
|
if (!openTab) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addCallback(
|
||||||
|
this,
|
||||||
|
new lazy.UrlbarResult({
|
||||||
|
type: UrlbarShared.RESULT_TYPE.TAB_SWITCH,
|
||||||
|
source: UrlbarShared.RESULT_SOURCE.TABS,
|
||||||
|
payload: {
|
||||||
|
url,
|
||||||
|
title: label,
|
||||||
|
icon: UrlbarShared.getIconForUrl(url),
|
||||||
|
userContext: UrlbarUtils.getUserContextData(userContextId),
|
||||||
|
tabGroup: openTab[1],
|
||||||
|
action: lazy.UrlbarPrefs.get("secondaryActions.switchToTab")
|
||||||
|
? UrlbarUtils.createTabSwitchSecondaryAction(userContextId)
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
highlights: {
|
||||||
|
url: UrlbarShared.HIGHLIGHT.TYPED,
|
||||||
|
title: UrlbarShared.HIGHLIGHT.TYPED,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (++resultsAdded == queryContext.maxResults) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#addFolders(sidebar, matches, addCallback) {
|
||||||
|
const folders = new Map(
|
||||||
|
(sidebar.folders || []).map(folder => [folder.id, folder])
|
||||||
|
);
|
||||||
|
for (const folder of folders.values()) {
|
||||||
|
if (folder.splitViewGroup || !folder.name || !matches(folder.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const space = sidebar.spaces?.find(s => s.uuid == folder.workspaceId);
|
||||||
|
const path = [folder.name];
|
||||||
|
for (
|
||||||
|
let parent = folders.get(folder.parentId);
|
||||||
|
parent;
|
||||||
|
parent = folders.get(parent.parentId)
|
||||||
|
) {
|
||||||
|
path.unshift(parent.name);
|
||||||
|
}
|
||||||
|
if (space?.name) {
|
||||||
|
path.unshift(space.name);
|
||||||
|
}
|
||||||
|
addCallback(
|
||||||
|
this,
|
||||||
|
new lazy.UrlbarResult({
|
||||||
|
type: UrlbarShared.RESULT_TYPE.DYNAMIC,
|
||||||
|
source: UrlbarShared.RESULT_SOURCE.TABS,
|
||||||
|
payload: {
|
||||||
|
dynamicType: DYNAMIC_TYPE_NAME,
|
||||||
|
zenFolderId: folder.id,
|
||||||
|
titleL10n: "zen-action-open",
|
||||||
|
userIcon: folder.userIcon,
|
||||||
|
icon: "chrome://browser/skin/zen-icons/folder.svg",
|
||||||
|
path: path.join(" / "),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewUpdate(result) {
|
||||||
|
return {
|
||||||
|
icon: { attributes: { src: result.payload.icon } },
|
||||||
|
titleStrong: {
|
||||||
|
textContent: formatValueSync(result.payload.titleL10n),
|
||||||
|
attributes: { dir: "ltr" },
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
textContent: result.payload.path,
|
||||||
|
attributes: { dir: "ltr", hidden: !result.payload.path },
|
||||||
|
},
|
||||||
|
userIcon: {
|
||||||
|
attributes: {
|
||||||
|
src: result.payload.userIcon,
|
||||||
|
hidden: !result.payload.userIcon,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewTemplate() {
|
||||||
|
return {
|
||||||
|
attributes: {
|
||||||
|
selectable: true,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "icon",
|
||||||
|
tag: "img",
|
||||||
|
classList: ["urlbarView-favicon", "urlbarView-action-favicon"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "title",
|
||||||
|
tag: "span",
|
||||||
|
classList: ["urlbarView-title"],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "titleStrong",
|
||||||
|
tag: "strong",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: "span",
|
||||||
|
classList: ["urlbarView-prettyName"],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: "userIcon",
|
||||||
|
tag: "img",
|
||||||
|
attributes: { hidden: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
tag: "span",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onEngagement(queryContext, controller, details) {
|
||||||
|
const { zenFolderId } = details.result.payload;
|
||||||
|
if (!zenFolderId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ownerGlobal = details.element.documentGlobal;
|
||||||
|
ownerGlobal.gBrowser.selectedBrowser.focus();
|
||||||
|
const folder = ownerGlobal.document.getElementById(zenFolderId);
|
||||||
|
if (folder?.isZenFolder) {
|
||||||
|
ownerGlobal.gZenFolders.revealFolder(folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ EXTRA_JS_MODULES += [
|
|||||||
"ZenUBGlobalActions.sys.mjs",
|
"ZenUBGlobalActions.sys.mjs",
|
||||||
"ZenUBProvider.sys.mjs",
|
"ZenUBProvider.sys.mjs",
|
||||||
"ZenUBResultsLearner.sys.mjs",
|
"ZenUBResultsLearner.sys.mjs",
|
||||||
|
"ZenUBSidebarProvider.sys.mjs",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user