Compare commits

...

59 Commits

Author SHA1 Message Date
mr. m
0e213d54db Merge branch 'dev' into library
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-08-28 08:50:11 +02:00
mr. m
2c98d5849a Merge branch 'library' of https://github.com/zen-browser/desktop into library 2026-08-28 08:48:28 +02:00
mr. m
cccdcc960b Merge branch 'dev' into library
# Conflicts:
#	src/browser/base/content/navigator-toolbox-inc-xhtml.patch
#	src/browser/components/urlbar/content/UrlbarInput-mjs.patch
#	src/zen/common/sys/ZenUIMigration.sys.mjs
2026-08-28 08:48:25 +02:00
Nikola Hristov
8df6fef1ac gh-15115: Tests for urlbar close and split view (gh-15118)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-08-27 23:45:37 +02:00
mr. m
4746d36699 gh-15088: Prevent sync if engine is already applying (gh-15137) 2026-08-27 23:38:47 +02:00
Nikola Hristov
9e0c790602 gh-15134: Fixes tab removed from group getting focused (gh-15135)
Closes https://github.com/zen-browser/desktop/issues/15134

Fix video:

https://github.com/user-attachments/assets/1c06ea7f-d482-4d4a-8bbe-df41daa27616
2026-08-27 22:40:43 +02:00
mr. m
3d53b37e8b gh-15119: Update windows registry with old certificates (gh-15120) 2026-08-26 17:51:09 +02:00
Nikola Hristov
e7ddfb52db gh-15115: Fix creating empty split view with url bar open (gh-15116) 2026-08-26 13:16:13 +02:00
mr. m
ba70449bdd gh-15088: Add debug log for space sync (gh-15117) 2026-08-26 12:23:52 +02:00
mr. m
bf40c31149 gh-14710: Fixed minimizing PiP window failing to collapse into the sidebar (gh-15111) 2026-08-26 08:30:00 +02:00
mr. m
7a093beae4 gh-15084: Fixed incoming tab icons applying to active tabs (gh-15107) 2026-08-25 22:31:50 +02:00
mr. m
03b4e934fe gh-15091: Fixed race condition when syncing new space and container (gh-15106) 2026-08-25 22:21:19 +02:00
mr. m
b93d19b873 gh-15088: Fixed syncing essentials with different containers (gh-15105) 2026-08-25 22:05:54 +02:00
mr. m
b9c4000fe2 gh-15101: Sync upstream Firefox to version 154.0.1 (gh-15100) 2026-08-25 19:37:07 +02:00
mr. m
0b7687ec6d gh-14990: Prevent policyContainer from bloating the session file (gh-15098) 2026-08-25 13:41:10 +02:00
mr. m
d70b3d902c gh-15085: Import mochitests for remote settings (gh-15086) 2026-08-24 22:52:33 +02:00
mr. m
f9f8f2f372 gh-15075: Fixed syncing new tabs not using static labels (gh-15079)
Fixes https://github.com/zen-browser/desktop/issues/15075
2026-08-24 18:27:50 +02:00
mr. m
99dca42723 gh-14470: Prevent state override if url changed (gh-15070) 2026-08-23 16:40:30 +02:00
Henrik Nyh
ed0a6fd447 gh-15039: Use Title Case for split view labels (gh-15059) 2026-08-23 12:21:34 +02:00
CocoLng
88c6611f1e gh-15039: Fix New Blank Window menu entry not in Title Case (gh-15056) 2026-08-22 10:08:37 +02:00
mr. m
e66e763e63 gh-14470: Fixed wrong data image being used for sync (gh-15048) 2026-08-21 09:44:49 +02:00
mr. m
721135f576 gh-14470: Fixed moz-remote-urls not being synced correctly (gh-15043) 2026-08-21 01:24:25 +02:00
mr. m
d8a934d2ec gh-15029: Fixed urlbar suggestion context not working (gh-15037) 2026-08-20 21:57:35 +02:00
mr. m
91a1d4d2ca gh-14470: Run sweep animation on first sync (gh-15036) 2026-08-20 21:38:09 +02:00
mr. m
97577dac26 gh-15031: Fixed duplicate separator on tab context menu (gh-15035) 2026-08-20 21:18:13 +02:00
Kristijan Ribarić
5723a89bcd gh-14470: Add tab and folder sync support (gh-13984)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-08-20 19:16:13 +02:00
Mr. M
123a7923af no-bug: Fix some conflicts 2026-07-10 13:04:45 +02:00
mr. m
cd71a36765 no-bug: Properly animate library icons 2026-07-03 14:34:17 +02:00
mr. m
80f424f68d Merge branch 'dev' into library 2026-07-03 12:22:25 +02:00
mr. m
7ce5fb77b9 no-bug: Update patches 2026-06-21 11:05:33 +02:00
mr. m
08acc6a39e Merge branch 'library' of https://github.com/zen-browser/desktop into library 2026-06-21 10:42:24 +02:00
mr. m
8ae271d982 Animate icons 2026-06-21 10:42:20 +02:00
mr. m
ba26c902fb Merge branch 'dev' into library
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-06-21 10:36:00 +02:00
JustAdumbPrsn
89a50770a9 Adds library toolbar icon & adjustments to existing keyframes (#13950)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-06-17 13:48:56 +02:00
JustAdumbPrsn
036f764d2b Add Boosts animated icons and minor adjustments (#13938)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-05-29 16:05:49 +02:00
mr. m
2c11b9295e no-bug: Add animated icons 2026-05-29 14:35:48 +02:00
mr. m
d7839cdb91 Merge branch 'dev' into library 2026-05-28 09:14:19 +02:00
mr. m
67cef284dd Merge branch 'dev' into library
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-05-18 16:50:30 +02:00
mr. m
36df7e718f no-bug: Continue library work 2026-05-18 16:46:43 +02:00
mr. m
d5b88ead22 Merge branch 'dev' into library 2026-05-13 20:06:51 +02:00
mr. m
7261eb1688 no-bug: Added history search 2026-05-10 15:54:12 +02:00
mr. m
b5a251a92f Merge branch 'dev' into library
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-05-10 12:27:42 +02:00
JustAdumbPrsn
a217cacfef no-bug: Add basic UI, search implimentation (gh-12755)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-03-31 22:06:17 +02:00
mr. m
a56bcf3d9c no-bug: Small touches 2026-03-28 14:28:12 +01:00
mr. m
5963de1486 Merge branch 'library' of https://github.com/zen-browser/desktop into library 2026-03-28 10:27:18 +01:00
mr. m
57ad5c45e7 Merge branch 'dev' into library
Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com>
2026-03-28 10:26:32 +01:00
JustAdumbPrsn
e6bebab10a change tab margin (i literally forgot to add 2px at bottom too)
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-03-13 12:34:32 +05:30
JustAdumbPrsn
a8820ed409 en-US locale .ftl for Zen-library
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-03-13 04:02:53 +05:30
JustAdumbPrsn
a82a7fb055 CSS for ZenLibrary but needs more changes :/
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-03-13 03:58:51 +05:30
JustAdumbPrsn
2be85a2890 UI, search (clicking item does nothing)
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-03-13 03:57:11 +05:30
JustAdumbPrsn
9301e1b64c Merge branch 'zen-browser:library' into library 2026-03-13 03:53:15 +05:30
mr. m
1e04f394a5 Merge branch 'dev' into library 2026-03-09 18:18:07 +01:00
JustAdumbPrsn
665f500925 feat: Implement Zen Library data backend (#12701) 2026-03-09 17:58:03 +01:00
JustAdumbPrsn
88ebf6e5da feat: improve section data backend
Signed-off-by: JustAdumbPrsn <73780892+JustAdumbPrsn@users.noreply.github.com>
2026-03-09 20:38:20 +05:30
mr. m
556beb01b0 feat: Add basic search component, b=no-bug, c=common 2026-03-08 12:09:31 +01:00
mr. m
48d5b32a5a feat: Improve transform detection, b=no-bug, c=common 2026-03-07 16:43:41 +01:00
mr. m
4682c8d545 feat: Add components to use, b=no-bug, c=no-component 2026-03-07 16:22:19 +01:00
mr. m
2c5fc47e74 Merge branch 'dev' into library 2026-03-07 12:29:53 +01:00
mr. m
237f0c0ea5 feat: Start implementing Zen Library, b=no-bug, c=common, folders 2026-03-06 10:16:05 +01:00
626 changed files with 95193 additions and 1044 deletions

2
.gitattributes vendored
View File

@@ -5,4 +5,4 @@
*.patch linguist-language=C++
*.d.ts linguist-language=TypeScript
src/zen/tests/*.js linguist-language=Test
src/zen/tests/** linguist-language=C++

View File

@@ -105,6 +105,8 @@ jobs:
SURFER_COMPAT: ${{ matrix.arch }}
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
run: |
. "$HOME/.cargo/env"
npm run import

View File

@@ -122,6 +122,8 @@ jobs:
SURFER_COMPAT: ${{ matrix.arch }}
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
SURFER_PLATFORM: darwin
run: |
. "$HOME/.cargo/env"

View File

@@ -166,6 +166,8 @@ jobs:
SURFER_COMPAT: ${{ matrix.arch }}
SURFER_CERT_PATCH_ISSUER: ${{ secrets.SURFER_CERT_PATCH_ISSUER }}
SURFER_CERT_PATCH_NAME: ${{ secrets.SURFER_CERT_PATCH_NAME }}
SURFER_CERT_PATCH_NAME_PREV: ${{ secrets.SURFER_CERT_PATCH_NAME_PREV }}
SURFER_CERT_PATCH_ISSUER_PREV: ${{ secrets.SURFER_CERT_PATCH_ISSUER_PREV }}
run: |
. "$HOME/.cargo/env"
npm run import -- --verbose

View File

@@ -34,8 +34,8 @@ Zen is a firefox-based browser with the aim of pushing your productivity to a ne
### Firefox Versions
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0`! 🚀
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 154.0`!
- [`Release`](https://zen-browser.app/download) - Is currently built using Firefox version `154.0.1`!
- [`Twilight`](https://zen-browser.app/download?twilight) - Is currently built using Firefox version `RC 154.0.1`!
### Contributing

View File

@@ -1 +1 @@
1fb0703fa9723fd9c400ad60d18e64ee4f9bf125
bf4b57fd7fe94054457be7018e8e7beaf754c520

View File

@@ -3,8 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
param(
[string][Parameter(Mandatory=$true)]$SignIdentity,
[string][Parameter(Mandatory=$true)]$SignIdentityIssuer,
[string][Parameter(Mandatory=$true)]$GithubRunId
)
@@ -24,11 +22,15 @@ mkdir windsign-temp -ErrorAction SilentlyContinue
# echo "Downloaded git objects repo to"
#} -Verbose -ArgumentList $PWD -Debug
$token = gh auth token
$env:SURFER_MOZCONFIG_ONLY="1"
$env:SURFER_SIGNING_MODE=""
$env:SURFER_CERT_PATCH_ISSUER=$SignIdentityIssuer
$env:SURFER_CERT_PATCH_NAME=$SignIdentity
get-content "$PSScriptRoot/../.env" | foreach {
$name, $value = $_.split('=')
set-content env:\$name $value
}
Start-Job -Name "DownloadGitl10n" -ScriptBlock {
param($PWD)
@@ -46,7 +48,6 @@ npm run build
echo "Downloading artifacts info"
$artifactsInfo=gh api repos/zen-browser/desktop/actions/runs/$GithubRunId/artifacts
$token = gh auth token
function New-TemporaryDirectory {
$tmp = [System.IO.Path]::GetTempPath() # Not $env:TEMP, see https://stackoverflow.com/a/946017

View File

@@ -20,6 +20,8 @@ files:
translation: browser/browser/preferences/zen-preferences.ftl
- source: en-US/browser/browser/zen-folders.ftl
translation: browser/browser/zen-folders.ftl
- source: en-US/browser/browser/zen-library.ftl
translation: browser/browser/zen-library.ftl
- source: en-US/browser/browser/zen-boosts.ftl
translation: browser/browser/zen-boosts.ftl
- source: en-US/browser/browser/zen-space-routing.ftl

View File

@@ -164,5 +164,5 @@ zen-window-sync-migration-dialog-learn-more = Learn More
zen-window-sync-migration-dialog-accept = Got It
zen-appmenu-new-blank-window =
.label = New blank window
.label = New Blank Window

View File

@@ -0,0 +1,94 @@
# 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/.
library-spaces-section-title = Spaces
library-downloads-section-title = Downloads
library-history-section-title = History
library-boosts-section-title = Boosts
library-search-placeholder =
.placeholder = Search…
library-history-search-placeholder =
.placeholder = Search History…
library-downloads-search-placeholder =
.placeholder = Search Downloads…
library-history-today = Today
library-history-yesterday = Yesterday
library-history-empty = No history found
library-downloads-empty = No downloads found
library-spaces-empty = No spaces available
library-boosts-empty = No boosts yet
library-search-no-results = No results
library-boosts-search-placeholder =
.placeholder = Search Boosts…
library-boost-toggle =
.tooltiptext = Toggle boost for this site
library-boost-context-edit =
.label = Edit Boost
library-boost-context-export =
.label = Export Boost
library-boost-context-delete =
.label = Delete Boost
library-filter-button = Filter
library-history-filter-all =
.label = All time
library-history-filter-today =
.label = Today
library-history-filter-yesterday =
.label = Yesterday
library-history-filter-last-7-days =
.label = Last 7 days
library-history-filter-last-30-days =
.label = Last 30 days
library-downloads-filter-all =
.label = All
library-downloads-filter-completed =
.label = Completed
library-downloads-filter-in-progress =
.label = In progress
library-downloads-filter-failed =
.label = Failed
library-downloads-filter-paused =
.label = Paused
library-item-context-open =
.label = Open
library-item-context-open-glance =
.label = Open in Glance
library-item-context-open-new-tab =
.label = Open in New Tab
library-item-context-open-new-window =
.label = Open in New Window
library-item-context-copy-url =
.label = Copy URL
library-item-context-delete-history =
.label = Forget About This Page
library-item-context-open-source =
.label = Open Source URL
library-item-context-remove =
.label = Remove from History
library-item-context-delete-file =
.label = Delete File
library-history-action-remove =
.tooltiptext = Forget About This Page
library-history-action-open-tab =
.tooltiptext = Open in New Tab
library-downloads-state-downloading = Downloading…
library-downloads-state-canceled = Canceled
library-downloads-state-failed = Failed
library-downloads-state-incomplete = Incomplete
library-close-button =
.tooltiptext = Close Library
library-donate-button =
.tooltiptext = Support Zen

View File

@@ -5,14 +5,14 @@
tab-zen-split-tabs =
.label =
{ $tabCount ->
[-1] Split out tab
[1] Add split view...
[-1] Split Out Tab
[1] Add Split View...
*[other] Join { $tabCount } Tabs
}
.accesskey = S
zen-split-link =
.label = Split link to new tab
.label = Split Link to New Tab
.accesskey = S
zen-split-view-modifier-header = Split View

View File

@@ -86,6 +86,8 @@ zen-workspace-default-profile = Default
zen-workspaces-delete-workspace-title = Delete Space?
zen-workspaces-delete-workspace-body = Are you sure you want to delete { $name }? This action cannot be undone.
zen-workspaces-remote-delete-title = Delete Synced Space?
zen-workspaces-remote-delete-body = { $name } was deleted on another device. Delete it here too? Keeping it will restore it on your other devices.
# Note that the html tag MUST not be changed or removed, as it is used to better
# display the shortcut in the toast notification.

8
package-lock.json generated
View File

@@ -11,7 +11,7 @@
"devDependencies": {
"@babel/preset-typescript": "^7.27.0",
"@zen-browser/prettier": "^3.9.3",
"@zen-browser/surfer": "^1.14.7",
"@zen-browser/surfer": "^1.14.8",
"formal-git": "^1.2.9",
"globals": "^16.3.0",
"husky": "^9.1.7",
@@ -860,9 +860,9 @@
}
},
"node_modules/@zen-browser/surfer": {
"version": "1.14.7",
"resolved": "https://registry.npmjs.org/@zen-browser/surfer/-/surfer-1.14.7.tgz",
"integrity": "sha512-Ui2Chqvr4/+FmHM54eKOv7cQu8DRAK/uCX9o1GNi81hszxo/RSQwaw71eNW43Qcj7giK4U4x0IdCMht98iQ0rA==",
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/@zen-browser/surfer/-/surfer-1.14.8.tgz",
"integrity": "sha512-5HHcfibwHIK5n3huhJL2TLBX2+8odj6ebCMA6U2EcUj9ROSrcz7/bjY2n5MIa9oIRL9gtV3ott0jwJj6SlapxA==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {

View File

@@ -49,7 +49,7 @@
"devDependencies": {
"@babel/preset-typescript": "^7.27.0",
"@zen-browser/prettier": "^3.9.3",
"@zen-browser/surfer": "^1.14.7",
"@zen-browser/surfer": "^1.14.8",
"formal-git": "^1.2.9",
"globals": "^16.3.0",
"husky": "^9.1.7",

View File

@@ -0,0 +1,6 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
- name: zen.library.enabled
value: false

View File

@@ -10,3 +10,6 @@
value: false
locked: true
condition: "!@IS_TWILIGHT@"
- name: zen.spaces-sync.debug
value: false

View File

@@ -8,4 +8,4 @@ packaging==24.2
pathspec==0.12.1
platformdirs==4.3.6
pycodestyle==2.12.1
requests==2.33.0
requests==2.34.2

View File

@@ -1,8 +1,8 @@
diff --git a/.prettierignore b/.prettierignore
index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073afaebeb8a 100644
index bd8ab46ff7175f5039b69b04c1afe53ab8efe746..7ae12a1ff3724e99aeec2e93518640a2fa9611e6 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -1803,3 +1803,12 @@ tools/ts/test/baselines/
@@ -1807,3 +1807,12 @@ tools/ts/test/baselines/
try_task_config.json
xpcom/idl-parser/xpidl/fixtures/xpctest.d.json
**/package-lock.json
@@ -12,6 +12,6 @@ index 949896ff064ae0b54b6a657ea074bc88e12820f7..5249f420972667bece4d85fe8d35073a
+*.min.js
+*.min.mjs
+*.inc
+*/mochitests/*
+**/mochitests/**
+*.svg
+

View File

@@ -1,5 +1,5 @@
diff --git a/browser/base/content/navigator-toolbox.inc.xhtml b/browser/base/content/navigator-toolbox.inc.xhtml
index dd2f391096968564086911c532f696dc7ffb8ae1..3de617bf4fb7fa35f7d4d7b618e4673b72b953ee 100644
index dd2f391096968564086911c532f696dc7ffb8ae1..c3e809a543397ab360fb83b3f61ef52d3c3bb738 100644
--- a/browser/base/content/navigator-toolbox.inc.xhtml
+++ b/browser/base/content/navigator-toolbox.inc.xhtml
@@ -2,7 +2,7 @@
@@ -55,3 +55,11 @@ index dd2f391096968564086911c532f696dc7ffb8ae1..3de617bf4fb7fa35f7d4d7b618e4673b
<toolbar id="nav-bar"
class="browser-toolbar chromeclass-location"
@@ -177,7 +185,6 @@
<toolbartabstop/>
<html:moz-urlbar id="urlbar"
class="urlbar"
- popover="manual"
pageproxystate="invalid"
unifiedsearchbutton-available=""
sap-name="urlbar">

View File

@@ -20,4 +20,5 @@
#include ../../../zen/fonts/jar.inc.mn
#include ../../../zen/boosts/jar.inc.mn
#include ../../../zen/live-folders/jar.inc.mn
#include ../../../zen/library/jar.inc.mn
#include ../../../zen/space-routing/jar.inc.mn

View File

@@ -69,5 +69,7 @@
<command id="cmd_zenNewLiveFolder" />
<command id="cmd_zenToggleLibrary" />
<command id="cmd_zenDuplicateTab" />
</commandset>

View File

@@ -9,6 +9,7 @@
<link rel="localization" href="browser/zen-menubar.ftl"/>
<link rel="localization" href="browser/zen-vertical-tabs.ftl"/>
<link rel="localization" href="browser/zen-folders.ftl"/>
<link rel="localization" href="browser/zen-library.ftl"/>
<link rel="localization" href="browser/zen-boosts.ftl"/>
<link rel="localization" href="browser/zen-live-folders.ftl"/>
<link rel="localization" href="browser/zen-space-routing.ftl"/>

View File

@@ -13,6 +13,7 @@
skipintoolbarset="true"
context="toolbar-context-menu"
mode="icons">
<toolbarbutton removable="true" class="chromeclass-toolbar-additional toolbarbutton-1 zen-sidebar-action-button" id="zen-library-button" command="cmd_zenToggleLibrary"></toolbarbutton>
<toolbarbutton removable="true" class="chromeclass-toolbar-additional toolbarbutton-1 zen-sidebar-action-button" id="zen-expand-sidebar-button" command="cmd_zenToggleSidebar" data-l10n-id="sidebar-zen-expand"></toolbarbutton>
<zen-workspace-icons id="zen-workspaces-button" overflows="false" removable="false"></zen-workspace-icons>
<toolbarbutton removable="true" class="chromeclass-toolbar-additional toolbarbutton-1 zen-sidebar-action-button" id="zen-create-new-button" context="zenCreateNewPopup" data-l10n-id="sidebar-zen-create-new"></toolbarbutton>

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/preferences/preferences.js b/browser/components/preferences/preferences.js
index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a9c5bb3f8 100644
index a76699af75252bec31e17b562463c37236cbbfe5..6355a6ee92754846ffcdd123ac49545d520470c7 100644
--- a/browser/components/preferences/preferences.js
+++ b/browser/components/preferences/preferences.js
@@ -131,6 +131,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
@@ -132,6 +132,7 @@ ChromeUtils.defineLazyGetter(this, "gSubDialog", function () {
styleSheets: [
"chrome://browser/skin/preferences/dialog.css",
"chrome://browser/skin/preferences/preferences.css",
@@ -10,7 +10,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
],
resizeCallback: async ({ title, frame }) => {
// Search within main document and highlight matched keyword.
@@ -437,6 +438,8 @@ const CONFIG_PANES = Object.freeze({
@@ -462,6 +463,8 @@ const CONFIG_PANES = Object.freeze({
tabsBrowsing: {
l10nId: "tabs-browsing-section",
groupIds: [
@@ -19,7 +19,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
"browserLayout",
"tabs",
"pageNavigation",
@@ -483,7 +486,7 @@ function register_module(categoryName, categoryObject) {
@@ -509,7 +512,7 @@ function register_module(categoryName, categoryObject) {
}
this._initted = true;
let template = document.getElementById("template-" + categoryName);
@@ -28,7 +28,7 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
// Replace the template element with the nodes inside of it.
template.replaceWith(template.content);
@@ -527,6 +530,10 @@ function init_all() {
@@ -553,6 +556,10 @@ function init_all() {
register_module("paneHome", gHomePane);
register_module("paneSearch", gSearchPane);
register_module("panePrivacy", gPrivacyPane);
@@ -39,15 +39,3 @@ index c76a0647e14bac806d9428483bb565b3e10c31e7..2188f7808aca090f92574c7f5c55a28a
// Restore the cached Firefox Labs nav button visibility so it shows
// immediately when recipes are expected to be available, before
@@ -656,9 +663,9 @@ async function gotoPref(
let redesignEnabled = srdSectionPrefs.all;
let categories = document.getElementById("categories");
const kDefaultCategoryInternalName = redesignEnabled
- ? "paneSync"
+ ? "paneTabsBrowsing"
: "paneGeneral";
- const kDefaultCategory = redesignEnabled ? "sync" : "general";
+ const kDefaultCategory = redesignEnabled ? "tabsBrowsing" : "general";
let hash = document.location.hash;
let category = aCategory || hash.substring(1) || kDefaultCategoryInternalName;

View File

@@ -0,0 +1,15 @@
diff --git a/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs b/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs
index 9359a485ed6212c7df49a8d4b1e4a8febbc1c9b6..132fea440cfa535e78b35f706e7ef2d6881f9ba6 100644
--- a/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs
+++ b/browser/components/preferences/widgets/sync-engine-list/sync-engines-list.mjs
@@ -50,6 +50,10 @@ const engineTypeToMetadata = {
iconSrc: "chrome://global/skin/icons/settings.svg",
l10nId: "sync-currently-syncing-settings",
},
+ workspaces: {
+ iconSrc: "chrome://devtools/skin/images/tool-storage.svg",
+ l10nId: "sync-currently-syncing-workspaces",
+ },
};
/**

View File

@@ -1,18 +1,20 @@
diff --git a/browser/components/sessionstore/SessionStore.sys.mjs b/browser/components/sessionstore/SessionStore.sys.mjs
index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee4aa3be28 100644
index 536e06eec9f35bad3b8366733b23b5a8697df9c8..631a16bc7518108641993836f0c52e2ab6e6c967 100644
--- a/browser/components/sessionstore/SessionStore.sys.mjs
+++ b/browser/components/sessionstore/SessionStore.sys.mjs
@@ -129,6 +129,9 @@ const TAB_EVENTS = [
@@ -129,6 +129,11 @@ const TAB_EVENTS = [
"TabSplitViewActivate",
"SplitViewRemoved",
"SplitViewCreated",
+ "TabAddedToEssentials",
+ "TabRemovedFromEssentials",
+ "TabMove",
+ "TabGroupMoved",
+ "ZenWorkspaceDataChanged",
];
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
@@ -201,6 +204,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
@@ -201,6 +206,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
TabStateFlusher: "resource:///modules/sessionstore/TabStateFlusher.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
@@ -21,7 +23,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
});
ChromeUtils.defineLazyGetter(lazy, "blankURI", () => {
@@ -1300,10 +1305,7 @@ var SessionStoreInternal = {
@@ -1317,10 +1324,7 @@ var SessionStoreInternal = {
*/
get willAutoRestore() {
return (
@@ -33,17 +35,19 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
);
},
@@ -1990,6 +1992,9 @@ var SessionStoreInternal = {
@@ -2008,6 +2012,11 @@ var SessionStoreInternal = {
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
+ case "TabRemovedFromEssentials":
+ case "TabAddedToEssentials":
+ case "TabMove":
+ case "TabGroupMoved":
+ case "ZenWorkspaceDataChanged":
this.saveStateDelayed(win);
break;
case "TabGroupCreate":
@@ -2099,6 +2104,10 @@ var SessionStoreInternal = {
@@ -2117,6 +2126,10 @@ var SessionStoreInternal = {
this._windows[aWindow.__SSi].isTaskbarTab = true;
}
@@ -54,7 +58,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
if (lazy.AIWindow.isAIWindowActiveAndEnabled(aWindow)) {
this._windows[aWindow.__SSi].isAIWindow = true;
}
@@ -2135,7 +2144,7 @@ var SessionStoreInternal = {
@@ -2153,7 +2166,7 @@ var SessionStoreInternal = {
let isTaskbarTab = this._windows[aWindow.__SSi].isTaskbarTab;
// A regular window is not a private window, taskbar tab window, or popup window
let isRegularWindow =
@@ -63,7 +67,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// perform additional initialization when the first window is loading
if (lazy.RunState.isStopped) {
@@ -2147,7 +2156,7 @@ var SessionStoreInternal = {
@@ -2165,7 +2178,7 @@ var SessionStoreInternal = {
// to disk to NOW() to enforce a full interval before the next write.
lazy.SessionSaver.updateLastSaveTime();
@@ -72,7 +76,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
this._log.debug(
"initializeWindow, the window is private or a web app. Saving SessionStartup.state for possibly restoring later"
);
@@ -2191,6 +2200,7 @@ var SessionStoreInternal = {
@@ -2209,6 +2222,7 @@ var SessionStoreInternal = {
null,
"sessionstore-one-or-no-tab-restored"
);
@@ -80,7 +84,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
this._deferredAllWindowsRestored.resolve();
}
// this window was opened by _openWindowWithState
@@ -2240,7 +2250,6 @@ var SessionStoreInternal = {
@@ -2258,7 +2272,6 @@ var SessionStoreInternal = {
if (closedWindowState) {
let newWindowState;
if (
@@ -88,7 +92,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
!lazy.SessionStartup.willRestore()
) {
// We want to split the window up into pinned tabs and unpinned tabs.
@@ -2276,6 +2285,7 @@ var SessionStoreInternal = {
@@ -2294,6 +2307,7 @@ var SessionStoreInternal = {
}
if (newWindowState) {
@@ -96,7 +100,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// Ensure that the window state isn't hidden
this._restoreCount = 1;
let state = { windows: [newWindowState] };
@@ -2312,6 +2322,9 @@ var SessionStoreInternal = {
@@ -2330,6 +2344,9 @@ var SessionStoreInternal = {
});
this._shouldRestoreLastSession = false;
}
@@ -106,7 +110,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
if (this._restoreLastWindow && aWindow.toolbar.visible) {
// always reset (if not a popup window)
@@ -2462,7 +2475,7 @@ var SessionStoreInternal = {
@@ -2480,7 +2497,7 @@ var SessionStoreInternal = {
var tabbrowser = aWindow.gBrowser;
@@ -115,7 +119,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
TAB_EVENTS.forEach(function (aEvent) {
tabbrowser.tabContainer.removeEventListener(aEvent, this, true);
@@ -2513,7 +2526,7 @@ var SessionStoreInternal = {
@@ -2531,7 +2548,7 @@ var SessionStoreInternal = {
let isLastRegularWindow =
Object.values(this._windows).filter(
@@ -124,7 +128,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
).length == 1;
this._log.debug(
`onClose, closing window isLastRegularWindow? ${isLastRegularWindow}`
@@ -2570,8 +2583,8 @@ var SessionStoreInternal = {
@@ -2588,8 +2605,8 @@ var SessionStoreInternal = {
// 2) Flush the window.
// 3) When the flush is complete, revisit our decision to store the window
// in _closedWindows, and add/remove as necessary.
@@ -135,7 +139,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
}
completionPromise = lazy.TabStateFlusher.flushWindow(aWindow).then(() => {
@@ -2591,8 +2604,9 @@ var SessionStoreInternal = {
@@ -2609,8 +2626,9 @@ var SessionStoreInternal = {
// Save non-private windows if they have at
// least one saveable tab or are the last window.
@@ -147,7 +151,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
if (!isLastWindow && winData.closedId > -1) {
this._addClosedAction(
@@ -2668,7 +2682,7 @@ var SessionStoreInternal = {
@@ -2686,7 +2704,7 @@ var SessionStoreInternal = {
* to call this method again asynchronously (for example, after
* a window flush).
*/
@@ -156,7 +160,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// Make sure SessionStore is still running, and make sure that we
// haven't chosen to forget this window.
if (
@@ -2685,6 +2699,7 @@ var SessionStoreInternal = {
@@ -2703,6 +2721,7 @@ var SessionStoreInternal = {
// _closedWindows from a previous call to this function.
let winIndex = this._closedWindows.indexOf(winData);
let alreadyStored = winIndex != -1;
@@ -164,7 +168,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// If sidebar command is truthy, i.e. sidebar is open, store sidebar settings
let shouldStore = hasSaveableTabs || isLastWindow;
@@ -3507,7 +3522,7 @@ var SessionStoreInternal = {
@@ -3525,7 +3544,7 @@ var SessionStoreInternal = {
if (!isPrivateWindow && tabState.isPrivate) {
return;
}
@@ -173,7 +177,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
return;
}
@@ -4246,6 +4261,12 @@ var SessionStoreInternal = {
@@ -4264,6 +4283,12 @@ var SessionStoreInternal = {
Math.min(tabState.index, tabState.entries.length)
);
tabState.pinned = false;
@@ -186,7 +190,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
if (inBackground === false) {
aWindow.gBrowser.selectedTab = newTab;
@@ -4679,6 +4700,8 @@ var SessionStoreInternal = {
@@ -4697,6 +4722,8 @@ var SessionStoreInternal = {
// Append the tab if we're opening into a different window,
tabIndex: aSource == aTargetWindow ? pos : Infinity,
pinned: state.pinned,
@@ -195,7 +199,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
userContextId: state.userContextId,
skipLoad: true,
preferredRemoteType,
@@ -5176,9 +5199,10 @@ var SessionStoreInternal = {
@@ -5194,9 +5221,10 @@ var SessionStoreInternal = {
if (activePageData.title && activePageData.title != activePageData.url) {
win.gBrowser.setInitialTabTitle(tab, activePageData.title, {
isContentTitle: true,
@@ -207,7 +211,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
}
}
@@ -5543,7 +5567,7 @@ var SessionStoreInternal = {
@@ -5561,7 +5589,7 @@ var SessionStoreInternal = {
for (let i = tabbrowser.pinnedTabCount; i < tabbrowser.tabs.length; i++) {
let tab = tabbrowser.tabs[i];
@@ -216,7 +220,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
removableTabs.push(tab);
}
}
@@ -5656,7 +5680,7 @@ var SessionStoreInternal = {
@@ -5674,7 +5702,7 @@ var SessionStoreInternal = {
// collect the data for all windows
for (ix in this._windows) {
@@ -225,7 +229,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// window data is still in _statesToRestore
continue;
}
@@ -5800,11 +5824,12 @@ var SessionStoreInternal = {
@@ -5818,11 +5846,12 @@ var SessionStoreInternal = {
}
let tabbrowser = aWindow.gBrowser;
@@ -239,7 +243,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// update the internal state data for this window
for (let tab of tabs) {
if (tab == aWindow.FirefoxViewHandler.tab) {
@@ -5815,6 +5840,9 @@ var SessionStoreInternal = {
@@ -5833,6 +5862,9 @@ var SessionStoreInternal = {
tabsData.push(tabData);
}
@@ -249,7 +253,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// update tab group state for this window
winData.groups = [];
for (let tabGroup of aWindow.gBrowser.tabGroups) {
@@ -5831,7 +5859,7 @@ var SessionStoreInternal = {
@@ -5849,7 +5881,7 @@ var SessionStoreInternal = {
// a window is closed, point to the first item in the tab strip instead (it will never be the Firefox View tab,
// since it's only inserted into the tab strip after it's selected).
if (aWindow.FirefoxViewHandler.tab?.selected) {
@@ -258,7 +262,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
winData.title = tabbrowser.tabs[0].label;
}
winData.selected = selectedIndex;
@@ -6033,8 +6061,8 @@ var SessionStoreInternal = {
@@ -6051,8 +6083,8 @@ var SessionStoreInternal = {
// selectTab represents.
let selectTab = 0;
if (overwriteTabs) {
@@ -269,7 +273,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
selectTab = Math.min(selectTab, winData.tabs.length);
}
@@ -6056,6 +6084,7 @@ var SessionStoreInternal = {
@@ -6074,6 +6106,7 @@ var SessionStoreInternal = {
if (overwriteTabs) {
for (let i = tabbrowser.browsers.length - 1; i >= 0; i--) {
if (!tabbrowser.tabs[i].selected) {
@@ -277,7 +281,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
tabbrowser.removeTab(tabbrowser.tabs[i]);
}
}
@@ -6090,6 +6119,12 @@ var SessionStoreInternal = {
@@ -6108,6 +6141,12 @@ var SessionStoreInternal = {
savedTabGroup => !openTabGroupIdsInWindow.has(savedTabGroup.id)
);
}
@@ -290,7 +294,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// Move the originally open tabs to the end.
if (initialTabs) {
@@ -6656,6 +6691,25 @@ var SessionStoreInternal = {
@@ -6674,6 +6713,25 @@ var SessionStoreInternal = {
// Most of tabData has been restored, now continue with restoring
// attributes that may trigger external events.
@@ -316,7 +320,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
if (tabData.pinned) {
tabbrowser.pinTab(tab);
@@ -6838,6 +6892,9 @@ var SessionStoreInternal = {
@@ -6859,6 +6917,9 @@ var SessionStoreInternal = {
aWindow.gURLBar.readOnly = false;
}
}
@@ -326,7 +330,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
let promiseParts = Promise.withResolvers();
aWindow.setTimeout(() => {
@@ -7633,7 +7690,7 @@ var SessionStoreInternal = {
@@ -7654,7 +7715,7 @@ var SessionStoreInternal = {
let groupsToSave = new Map();
for (let tIndex = 0; tIndex < window.tabs.length; ) {
@@ -335,7 +339,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// Adjust window.selected
if (tIndex + 1 < window.selected) {
window.selected -= 1;
@@ -7648,7 +7705,7 @@ var SessionStoreInternal = {
@@ -7669,7 +7730,7 @@ var SessionStoreInternal = {
);
// We don't want to increment tIndex here.
continue;
@@ -344,7 +348,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
// Convert any open groups into saved groups.
let groupStateToSave = window.groups.find(
groupState => groupState.id == window.tabs[tIndex].groupId
@@ -8193,7 +8250,6 @@ var SessionStoreInternal = {
@@ -8233,7 +8294,6 @@ var SessionStoreInternal = {
timer.initWithCallback(
function () {
if (beats <= 0) {
@@ -352,7 +356,7 @@ index 11191a82a0f0dc6eee60f9de52a63bc6085c3981..f995c3b297c53aa6ee2219bfc09515ee
Glean.sessionRestore.shutdownFlushAllOutcomes.timed_out.add(1);
deferred.resolve();
}
@@ -8671,6 +8727,7 @@ var SessionStoreInternal = {
@@ -8711,6 +8771,7 @@ var SessionStoreInternal = {
if (
!savedTabGroupState.tabs.length ||
this.getSavedTabGroup(savedTabGroupState.id)

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/tabbrowser/content/tab-context-menu.js b/browser/components/tabbrowser/content/tab-context-menu.js
index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..2e2404b78deb428e57d01b8b929231f675a21323 100644
index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..d7f75471a207cccac6ca3dec49037ac30dd76163 100644
--- a/browser/components/tabbrowser/content/tab-context-menu.js
+++ b/browser/components/tabbrowser/content/tab-context-menu.js
@@ -839,7 +839,8 @@ var TabContextMenu = {
@@ -839,13 +839,15 @@ var TabContextMenu = {
);
contextUnpinSelectedTabs.hidden =
!this.contextTab.pinned || !this.multiselected;
@@ -12,3 +12,10 @@ index 4a7cb33a4d9067d469bbe09d4cd8844581642f7a..2e2404b78deb428e57d01b8b929231f6
// Build Ask Chat items. The classic layout shows the full ask-chat submenu
// (#context_askChat); the alt layout shows only a flat "Summarize Page" item
// (#context_askChatSummarize). Hide whichever one this layout doesn't use so
// a pref toggle reverses cleanly.
if (!this._altTabContextMenu) {
document.getElementById("context_askChatSummarize").hidden = true;
+ document.getElementById("context_aiSeparator").hidden = true;
TabContextMenu.GenAI.buildTabMenu(
document.getElementById("context_askChat"),
this

View File

@@ -1,8 +1,18 @@
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489fb04e8b7a 100644
index 89f0d44d462de3d7d0f581d6371606f475443822..0d45ce5f6455e9c7276c54f504906bade5b3d286 100644
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
@@ -462,6 +462,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
@@ -163,6 +163,9 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
// Feature gate pref for flight status suggestions in the urlbar.
["flightStatus.featureGate", false],
+ // Whether to close the urlbar when blurring the window while the urlbar is focused.
+ ["closeOnWindowBlur", true],
+
// The minimum prefix length of a flight status keyword the user must type to
// trigger the suggestion. 0 means the min length should be taken from Nimbus
// or remote settings.
@@ -482,6 +485,7 @@ const PREF_URLBAR_DEFAULTS = /** @type {PreferenceDefinition[]} */ ([
["shortcuts.tabs", true],
["shortcuts.history", true],
["shortcuts.actions", true],
@@ -10,7 +20,7 @@ index 2d21248256c6c2bfb8dac958133c10e3251ef564..f788bd10ec2c08e4b27b77cd3bb0489f
// Boolean to determine if the providers defined in `exposureResults`
// should be displayed in search results. This can be set by a
@@ -799,6 +800,8 @@ function makeDefaultResultGroups({ showSearchSuggestionsFirst }) {
@@ -840,6 +844,8 @@ function makeDefaultResultGroups({
*/
let rootGroup = {
children: [

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a6182579cbaf4d 100644
index e7e9495e56076cd112beafb8297dece4ae4cea58..afdc80c034e2a3c9a50e6e5d2cc650e59bf2eb91 100644
--- a/browser/components/urlbar/content/UrlbarInput.mjs
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
@@ -101,6 +101,13 @@ const lazy = XPCOMUtils.declareLazy({
@@ -117,7 +117,8 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
startLayoutExtend() {
if (!this.#allowBreakout || this.hasAttribute("breakout-extend")) {
// Do not expand if the Urlbar does not support being expanded or it is
@@ -2988,6 +3057,13 @@ ${
@@ -2988,6 +3057,14 @@ ${
+ this.setAttribute("popover", "manual");
this.toggleAttribute("breakout-extend", true);
this.#updateTextboxPosition();
@@ -131,7 +132,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
// Enable the animation only after the first extend call to ensure it
// doesn't run when opening a new window.
if (!this.hasAttribute("breakout-extend-animate")) {
@@ -3011,6 +3087,29 @@ ${
@@ -3011,6 +3088,30 @@ ${
return;
}
@@ -159,6 +160,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
+ this.removeAttribute("zen-floating-urlbar");
+
this.toggleAttribute("breakout-extend", false);
+ this.removeAttribute("popover");
this.#updateTextboxPosition();
}
@@ -3049,7 +3148,7 @@ ${
@@ -280,7 +282,20 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
l10nId,
l10nId == "urlbar-placeholder-with-name"
? { name: engineName }
@@ -5424,6 +5540,11 @@ ${
@@ -5347,6 +5463,12 @@ ${
}
lazy.logger.debug("Blur Event");
+ if (
+ this.document.commandDispatcher.focusedElement == this.inputField &&
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
+ ) {
+ return;
+ }
// We cannot count every blur events after a missed engagement as abandoment
// because the user may have clicked on some view element that executes
// a command causing a focus change. For example opening preferences from
@@ -5424,6 +5546,11 @@ ${
}
_on_click(event) {
@@ -292,7 +307,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
switch (event.target) {
case this.inputField:
case this._inputContainer:
@@ -5513,10 +5634,11 @@ ${
@@ -5513,10 +5640,11 @@ ${
}
if (untrim) {
this.setValue(this._untrimmedValue);
@@ -305,7 +320,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
this.view.autoOpen({ event });
} else {
if (this._untrimOnFocusAfterKeydown) {
@@ -5556,9 +5678,16 @@ ${
@@ -5556,9 +5684,16 @@ ${
}
_on_mousedown(event) {
@@ -323,7 +338,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
if (
event.composedTarget != this.inputField &&
event.composedTarget != this._inputContainer
@@ -5568,6 +5697,10 @@ ${
@@ -5568,6 +5703,10 @@ ${
this.focusedViaMousedown = !this.focused;
this.#preventClickSelectsAll = this.focused;
@@ -334,7 +349,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
// Keep the focus status, since the attribute may be changed
// upon calling this.focus().
@@ -5605,7 +5738,7 @@ ${
@@ -5605,7 +5744,7 @@ ${
// view open on tab switch, and the TabSelect event arrived earlier.
// Also ignore mousedown on the urlbarView context menu: opening/closing the
// view is already handled by the result opening flow.
@@ -343,7 +358,7 @@ index e7e9495e56076cd112beafb8297dece4ae4cea58..330104d245ddd0956b95377082a61825
break;
}
@@ -5894,7 +6027,7 @@ ${
@@ -5894,7 +6033,7 @@ ${
// When we are in actions search mode we can show more results so
// increase the limit.
let maxResults =

View File

@@ -1,8 +1,8 @@
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac823fedcb 100644
index e465806c4ae4fc372d9aa2b71aa7f0e74c65d026..c0b3781901298bedee2736819a93edbf545507a7 100644
--- a/browser/components/urlbar/content/UrlbarView.mjs
+++ b/browser/components/urlbar/content/UrlbarView.mjs
@@ -674,7 +674,7 @@ export class UrlbarView {
@@ -803,7 +803,7 @@ export class UrlbarView {
!this.input.value ||
this.input.getAttribute("pageproxystate") == "valid"
) {
@@ -11,7 +11,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
// Try to reuse the cached top-sites context. If it's not cached, then
// there will be a gap of time between when the input is focused and
// when the view opens that can be perceived as flicker.
@@ -812,10 +812,6 @@ export class UrlbarView {
@@ -941,10 +941,6 @@ export class UrlbarView {
}
// If search mode isn't active, close the view.
@@ -22,7 +22,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
// Search mode is active. If the one-offs should be shown, make sure they
// are enabled and show the view.
@@ -3031,6 +3027,8 @@ export class UrlbarView {
@@ -3203,6 +3199,8 @@ export class UrlbarView {
if (row?.hasAttribute("row-selectable")) {
row?.toggleAttribute("selected", true);
}
@@ -31,7 +31,7 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
if (element != row) {
row?.toggleAttribute("descendant-selected", true);
}
@@ -3544,7 +3542,7 @@ export class UrlbarView {
@@ -3715,7 +3713,7 @@ export class UrlbarView {
}
#enableOrDisableRowWrap() {
@@ -40,3 +40,16 @@ index 8afb2bf4a757b3ef7902f5a6e6eed6b70ca2845e..440dea1e66540ca1998cb7464b6695ac
this.#rows.toggleAttribute("wrap", wrap);
this.oneOffSearchButtons?.container.toggleAttribute("wrap", wrap);
}
@@ -4144,6 +4142,12 @@ export class UrlbarView {
}
on_blur() {
+ if (
+ this.document.commandDispatcher.focusedElement == this.input.inputField &&
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
+ ) {
+ return;
+ }
// If the view is open without the input being focused, it will not close
// automatically when the window loses focus. We might be in this state
// after a Search Tip is shown on an engine homepage.

View File

@@ -0,0 +1,69 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-library-icon" width="18" height="18" viewBox="4 4 18 18" fill="context-fill" fill-opacity="context-fill-opacity" xmlns="http://www.w3.org/2000/svg">
<style>
@property --zen-triangle-anim { syntax: '&lt;number&gt;'; initial-value: 0; inherits: false; }
@property --zen-circle-anim { syntax: '&lt;number&gt;'; initial-value: 0; inherits: false; }
@keyframes StarForward {
0% { transform: rotate(0deg); }
100% { transform: rotate(120deg); }
}
@keyframes TriangleForward {
0% { --zen-triangle-anim: 0; }
100% { --zen-triangle-anim: 1; }
}
@keyframes CircleForward {
0% { --zen-circle-anim: 0; }
100% { --zen-circle-anim: 1; }
}
@keyframes GlowForward {
0% { opacity: 0; }
100% { opacity: 0.4; }
}
@keyframes GlowElementsForward {
0% { opacity: 0; }
100% { opacity: 0.5; }
}
.zen-library-star { transform-origin: 10px 10.2px; animation: StarForward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.zen-library-triangle-group { transform-origin: 17px 9.2px; animation: TriangleForward 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: CircleForward 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: GlowForward 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
.zen-library-glow-element { filter: blur(2px); animation: GlowElementsForward 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; }
.zen-library-star:not(.zen-library-glow-element), .zen-library-circle:not(.zen-library-glow-element), .zen-library-triangle:not(.zen-library-glow-element) { fill: white !important; }
}
</style>
<defs>
<mask id="zen-library-box-mask">
<rect x="-10" y="-10" width="40" height="40" fill="white"/>
<rect x="-10" y="11.635" width="40" height="40" fill="black"/>
</mask>
</defs>
<g fill-rule="evenodd">
<g mask="url(#zen-library-box-mask)">
<path class="zen-library-star zen-library-glow-element" d="M 7.773 11.635 C 8.136 11.863 8.24 12.075 8.218 12.294 L 8.036 14.082 C 7.984 14.598 8.584 14.917 8.982 14.585 L 10.362 13.432 C 10.53 13.291 10.764 13.258 10.966 13.347 L 12.61 14.072 C 13.085 14.281 13.574 13.808 13.38 13.327 L 12.701 11.635 C 12.632 11.437 12.675 11.216 12.816 11.058 L 14.014 9.718 C 14.36 9.331 14.061 8.72 13.544 8.756 L 11.75 8.877 C 11.531 8.892 11.322 8.781 11.211 8.591 L 10.307 7.038 C 10.046 6.59 9.373 6.685 9.247 7.188 L 8.808 8.931 C 8.754 9.144 8.585 9.309 8.37 9.355 L 6.613 9.735 C 6.106 9.845 5.988 10.514 6.427 10.79 L 7.773 11.635 Z"/>
<path class="zen-library-star" d="M 7.773 11.635 C 8.136 11.863 8.24 12.075 8.218 12.294 L 8.036 14.082 C 7.984 14.598 8.584 14.917 8.982 14.585 L 10.362 13.432 C 10.53 13.291 10.764 13.258 10.966 13.347 L 12.61 14.072 C 13.085 14.281 13.574 13.808 13.38 13.327 L 12.701 11.635 C 12.632 11.437 12.675 11.216 12.816 11.058 L 14.014 9.718 C 14.36 9.331 14.061 8.72 13.544 8.756 L 11.75 8.877 C 11.531 8.892 11.322 8.781 11.211 8.591 L 10.307 7.038 C 10.046 6.59 9.373 6.685 9.247 7.188 L 8.808 8.931 C 8.754 9.144 8.585 9.309 8.37 9.355 L 6.613 9.735 C 6.106 9.845 5.988 10.514 6.427 10.79 L 7.773 11.635 Z"/>
</g>
<g transform="translate(14, 7.3)">
<circle class="zen-library-circle zen-library-glow-element" cx="0" cy="0" r="1.4"/>
<circle class="zen-library-circle" cx="0" cy="0" r="1"/>
</g>
<g class="zen-library-triangle-group">
<g transform="translate(17, 9.2)">
<path class="zen-library-triangle zen-library-glow-element" d="M -1.1 1.8 C -1.4 2.0, -1.7 1.8, -1.7 1.5 L -1.7 -1.5 C -1.7 -1.8, -1.4 -2.0, -1.1 -1.8 L 1.4 -0.4 C 1.7 -0.2, 1.7 0.2, 1.4 0.4 Z"/>
<path class="zen-library-triangle" d="M -1.1 1.8 C -1.4 2.0, -1.7 1.8, -1.7 1.5 L -1.7 -1.5 C -1.7 -1.8, -1.4 -2.0, -1.1 -1.8 L 1.4 -0.4 C 1.7 -0.2, 1.7 0.2, 1.4 0.4 Z"/>
</g>
</g>
<g class="zen-library-glow">
<path class="zen-library-glow-bg" d="M 6.6 6.6 L 8.04 12.7 L 17.96 12.7 L 19.4 6.6 Z"/>
<path class="zen-library-glow-rays" d="M 6.1 6.6 L 8.04 12.7 L 17.96 12.7 L 19.9 6.6"/>
</g>
<path class="zen-library-box" d="M 7.218 13.287 C 7.218 12.831 7.588 12.461 8.044 12.461 H 17.957 C 18.414 12.461 18.783 12.831 18.783 13.287 V 17.418 C 18.783 19.243 17.304 20.722 15.479 20.722 H 10.523 C 8.698 20.722 7.218 19.243 7.218 17.418 V 13.287 Z M 10.609 14.526 C 10.609 14.07 10.979 13.7 11.435 13.7 H 14.739 C 15.196 13.7 15.565 14.07 15.565 14.526 C 15.565 14.982 15.196 15.352 14.739 15.352 H 11.435 C 10.979 15.352 10.609 14.982 10.609 14.526 Z"/>
</g>
</svg>

View File

@@ -0,0 +1,81 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-boosts-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes zenBoostsCardTilt {
0%, 13.33% { transform: translate(61.889px, 63.143px) scale(1.1) rotate(-15deg); animation-timing-function: cubic-bezier(0.35, 0, 0.685, 1); }
37.77% { transform: translate(61.889px, 68.143px) scale(1.1) rotate(-23deg); animation-timing-function: cubic-bezier(0.25, 0, 0.7, 1); }
64.8% { transform: translate(61.889px, 55.143px) scale(1.1) rotate(-5.4deg); animation-timing-function: cubic-bezier(0.28, 0, 0.68, 1); }
87%, 100% { transform: translate(61.889px, 63.143px) scale(1.1) rotate(-15deg); }
}
@keyframes zenBoostsBrushRotate {
0%, 2.22% { transform: translate(18.247px, 109.504px) scale(1.1) rotate(0deg); animation-timing-function: cubic-bezier(0.6, 0, 0.7, 1); }
32% { transform: translate(18.247px, 112px) scale(1.1) rotate(12deg); animation-timing-function: cubic-bezier(0.3, 0, 0.6, 1); }
52% { transform: translate(18.247px, 97.957px) scale(1.1) rotate(-16.5deg); animation-timing-function: cubic-bezier(0.4, 0, 0.5, 1); }
80%, 100% { transform: translate(18.247px, 109.504px) scale(1.1) rotate(0deg); }
}
@keyframes zenBoostsStarLargeBounce {
0%, 20% { transform: translate(85.002px, 50.174px) rotate(2.014deg) scale(1); animation-timing-function: cubic-bezier(0.29, 0, 0.83, 1); }
33.3% { transform: translate(85.002px, 59px) rotate(2.014deg) scale(1); animation-timing-function: linear; }
42.2% { transform: translate(85.002px, 60.5px) rotate(2.014deg) scale(0); animation-timing-function: linear; }
53.3% { transform: translate(85.002px, 45px) rotate(2.014deg) scale(0); animation-timing-function: cubic-bezier(0.45, 0, 0.63, 1); }
62.2% { transform: translate(85.002px, 33.344px) rotate(2.014deg) scale(1); animation-timing-function: cubic-bezier(0.45, 0, 0.63, 1); }
88.8%, 100% { transform: translate(85.002px, 50.174px) rotate(2.014deg) scale(1); }
}
@keyframes zenBoostsStarSmallBounce {
0%, 26.6% { transform: translate(68.002px, 37.075px) rotate(2.014deg) scale(1); animation-timing-function: cubic-bezier(0.29, 0, 0.83, 1); }
44.8% { transform: translate(68.002px, 44.632px) rotate(2.014deg) scale(1); animation-timing-function: linear; }
55.5% { transform: translate(68.002px, 34px) rotate(2.014deg) scale(1); animation-timing-function: linear; }
64.4% { transform: translate(68.002px, 25px) rotate(2.014deg) scale(0); animation-timing-function: linear; }
75.5% { transform: translate(68.002px, 15px) rotate(2.014deg) scale(0); animation-timing-function: cubic-bezier(0.45, 0, 0.63, 1); }
84.4% { transform: translate(68.002px, 28px) rotate(2.014deg) scale(1); animation-timing-function: cubic-bezier(0.45, 0, 0.63, 1); }
93.3%, 100% { transform: translate(68.002px, 37.075px) rotate(2.014deg) scale(1); }
}
.zen-boosts-card { animation: zenBoostsCardTilt 0.583s forwards; }
.zen-boosts-brush { animation: zenBoostsBrushRotate 0.583s forwards; }
.zen-boosts-star-large { animation: zenBoostsStarLargeBounce 0.583s forwards; }
.zen-boosts-star-small { animation: zenBoostsStarSmallBounce 0.583s forwards; }
</style>
<defs>
<linearGradient id="zen-boosts-grad" gradientUnits="userSpaceOnUse" x1="64" y1="16" x2="64" y2="144">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
<mask id="zen-boosts-mask" maskContentUnits="userSpaceOnUse">
<rect x="-100" y="-100" width="300" height="300" fill="#fff"/>
<path fill="#000" stroke="#000" stroke-width="8" stroke-linejoin="round" d="M-3.79 54.121C-5.31 51.635-6.984 48.884-7.082 42.132L-7.073 42.091-7.063 42.051C-6.2 38.573-3.904 36.054-1.1 34.382L4.474 31.059C6.968 29.572 9.896 28.739 13.003 29.233 13.558 28.514 14.137 27.648 14.736 26.617L14.752 26.589 14.769 26.561C15.438 25.438 16.22 24.032 17.105 22.317 18.032 20.521 19.176 18.3 20.531 15.65 21.63 13.5 23.221 11.59 25.405 10.221 27.368 8.99 29.589 8.271 31.984 8.218L32.013 8.218C34.325 8.178 36.528 8.749 38.557 9.821 40.713 10.96 42.401 12.627 43.642 14.615L43.662 14.646 63.922 47.997C66.006 51.466 67.049 55.422 66.104 59.547 65.148 63.723 62.394 66.785 58.898 68.869L44.274 77.586C44.222 77.953 44.148 78.323 44.06 78.696 43.24 82.158 40.963 84.66 38.246 86.34L38.205 86.365 32.548 89.738C29.738 91.413 26.42 92.18 22.922 91.299 18.565 90.201 14.485 87.919 10.34 86.196 10.34 86.196 8.779 87.737 7.305 89.123 5.699 90.632 4.021 91.927 2.291 92.956-2.345 95.712-7.442 96.912-12.65 95.581-17.868 94.248-21.776 90.71-24.597 86.068-27.411 81.439-28.702 76.368-27.456 71.195-26.206 66.005-22.739 62.101-18.096 59.333-16.417 58.332-14.507 57.488-12.438 56.763L-12.4 56.75C-10.473 56.089-8.488 55.472-6.445 54.899-5.537 54.635-4.652 54.375-3.79 54.121Z"/>
</mask>
</defs>
<g class="zen-boosts-card" style="transform-origin: 0 0;" transform="translate(61.889 63.143) scale(1.1) rotate(-15)">
<g class="zen-boosts-card-anchor" transform="translate(-44 -44)">
<rect class="zen-boosts-bg" x="3.55" y="3.55" width="80.9" height="80.9" rx="12.45" fill="context-fill" fill-opacity="1"/>
<rect class="zen-boosts-gradient" x="3.55" y="3.55" width="80.9" height="80.9" rx="12.45" fill="url(#zen-boosts-grad)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<rect class="zen-boosts-border" width="88" height="88" rx="16" mask="url(#zen-boosts-mask)" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g class="zen-boosts-brush" style="transform-origin: 0 0;" transform="translate(18.247 109.504) scale(1.1)">
<g class="zen-boosts-brush-anchor" transform="translate(-15 -70)">
<g class="zen-boosts-brush-tip-translate" transform="translate(27.307 3.73)">
<path class="zen-boosts-bg" d="M0 28 6 14 12 0 44 34 26 54Z" fill="context-fill" fill-opacity="1"/>
<path class="zen-boosts-gradient" d="M0 28 6 14 12 0 44 34 26 54Z" fill="url(#zen-boosts-grad)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
</g>
<g class="zen-boosts-brush-silhouette-fills">
<path class="zen-boosts-bg" fill="context-fill" fill-opacity="1" d="M0 69.826C-0.023 66.459 1.467 63.29 4.469 60.318 5.501 59.296 6.814 58.264 8.409 57.219 10.027 56.174 11.717 55.176 13.476 54.154 15.235 53.132 16.877 52.158 18.472 51.229L22.377 48.617 15.024 41.373C13.546 39.91 12.795 38.354 12.772 36.706 12.772 35.034 13.5 33.479 14.954 32.04L19.563 27.478C21.017 26.039 22.577 25.33 24.242 25.353 26.219 25.381 26.531 25.828 28.992 27.547L56.964 55.303C58.465 56.766 59.215 58.322 59.215 59.97 59.238 61.595 58.535 63.139 57.104 64.602L52.46 69.199C51.029 70.615 49.47 71.311 47.781 71.288 46.092 71.288 44.52 70.557 43.066 69.094L35.748 61.816C34.904 62.814 34.012 64.091 33.074 65.647 32.136 67.203 31.15 68.862 30.118 70.627 29.109 72.392 28.078 74.063 27.022 75.642 25.99 77.244 24.946 78.555 23.89 79.577 20.888 82.549 17.686 84.023 14.285 84 10.884 83.977 7.658 82.444 4.609 79.403 1.56 76.385 0.023 73.193 0 69.826ZM56.257 54.603L54.309 52.669 68.012 38.552C68.669 37.902 68.997 37.182 68.997 36.393 68.997 35.604 68.633 34.849 67.906 34.129L41.764 8.289C41.412 7.941 41.048 7.754 40.673 7.731 40.321 7.708 39.993 7.825 39.688 8.08 39.407 8.312 39.196 8.695 39.055 9.229 38.211 12.294 37.495 14.917 36.909 17.099 36.323 19.281 35.712 21.244 35.079 22.985 34.446 24.726 33.636 26.444 32.651 28.139 31.689 29.811 31.369 29.906 31.369 29.906L28.992 27.547 26.531 25.828C27.258 24.69 28.147 22.962 28.64 21.871 29.133 20.757 29.578 19.514 29.977 18.144 30.399 16.751 30.845 15.079 31.314 13.129 31.783 11.156 32.358 8.718 33.038 5.816 33.39 4.307 34.023 3.088 34.938 2.159 35.853 1.207 36.909 0.569 38.105 0.244 39.325-0.081 40.556-0.081 41.799 0.244 43.042 0.546 44.168 1.184 45.177 2.159L72.832 29.567C74.92 31.657 75.975 33.85 75.998 36.149 76.045 38.447 75.049 40.607 73.008 42.627L58.083 56.603 56.257 54.603Z"/>
<path class="zen-boosts-gradient" fill="url(#zen-boosts-grad)" style="mix-blend-mode: overlay; opacity: 0.1;" d="M0 69.826C-0.023 66.459 1.467 63.29 4.469 60.318 5.501 59.296 6.814 58.264 8.409 57.219 10.027 56.174 11.717 55.176 13.476 54.154 15.235 53.132 16.877 52.158 18.472 51.229L22.377 48.617 15.024 41.373C13.546 39.91 12.795 38.354 12.772 36.706 12.772 35.034 13.5 33.479 14.954 32.04L19.563 27.478C21.017 26.039 22.577 25.33 24.242 25.353 26.219 25.381 26.531 25.828 28.992 27.547L56.964 55.303C58.465 56.766 59.215 58.322 59.215 59.97 59.238 61.595 58.535 63.139 57.104 64.602L52.46 69.199C51.029 70.615 49.47 71.311 47.781 71.288 46.092 71.288 44.52 70.557 43.066 69.094L35.748 61.816C34.904 62.814 34.012 64.091 33.074 65.647 32.136 67.203 31.15 68.862 30.118 70.627 29.109 72.392 28.078 74.063 27.022 75.642 25.99 77.244 24.946 78.555 23.89 79.577 20.888 82.549 17.686 84.023 14.285 84 10.884 83.977 7.658 82.444 4.609 79.403 1.56 76.385 0.023 73.193 0 69.826ZM56.257 54.603L54.309 52.669 68.012 38.552C68.669 37.902 68.997 37.182 68.997 36.393 68.997 35.604 68.633 34.849 67.906 34.129L41.764 8.289C41.412 7.941 41.048 7.754 40.673 7.731 40.321 7.708 39.993 7.825 39.688 8.08 39.407 8.312 39.196 8.695 39.055 9.229 38.211 12.294 37.495 14.917 36.909 17.099 36.323 19.281 35.712 21.244 35.079 22.985 34.446 24.726 33.636 26.444 32.651 28.139 31.689 29.811 31.369 29.906 31.369 29.906L28.992 27.547 26.531 25.828C27.258 24.69 28.147 22.962 28.64 21.871 29.133 20.757 29.578 19.514 29.977 18.144 30.399 16.751 30.845 15.079 31.314 13.129 31.783 11.156 32.358 8.718 33.038 5.816 33.39 4.307 34.023 3.088 34.938 2.159 35.853 1.207 36.909 0.569 38.105 0.244 39.325-0.081 40.556-0.081 41.799 0.244 43.042 0.546 44.168 1.184 45.177 2.159L72.832 29.567C74.92 31.657 75.975 33.85 75.998 36.149 76.045 38.447 75.049 40.607 73.008 42.627L58.083 56.603 56.257 54.603Z"/>
</g>
<path class="zen-boosts-border" fill-rule="evenodd" fill="context-stroke" d="M0 69.826C-0.023 66.459 1.467 63.29 4.469 60.318 5.501 59.296 6.814 58.264 8.409 57.219 10.027 56.174 11.717 55.176 13.476 54.154 15.235 53.132 16.877 52.158 18.472 51.229L22.377 48.617 15.024 41.373C13.546 39.91 12.795 38.354 12.772 36.706 12.772 35.034 13.5 33.479 14.954 32.04L19.563 27.478C21.017 26.039 22.577 25.33 24.242 25.353 26.219 25.381 26.531 25.828 28.992 27.547L56.964 55.303C58.465 56.766 59.215 58.322 59.215 59.97 59.238 61.595 58.535 63.139 57.104 64.602L52.46 69.199C51.029 70.615 49.47 71.311 47.781 71.288 46.092 71.288 44.52 70.557 43.066 69.094L35.748 61.816C34.904 62.814 34.012 64.091 33.074 65.647 32.136 67.203 31.15 68.862 30.118 70.627 29.109 72.392 28.078 74.063 27.022 75.642 25.99 77.244 24.946 78.555 23.89 79.577 20.888 82.549 17.686 84.023 14.285 84 10.884 83.977 7.658 82.444 4.609 79.403 1.56 76.385 0.023 73.193 0 69.826ZM20.618 38.308L29.133 46.701C29.86 47.398 30.188 48.187 30.118 49.069 30.048 49.951 29.625 50.799 28.851 51.612 28.171 52.309 27.034 53.11 25.439 54.015 23.844 54.92 22.037 55.931 20.02 57.045 18.026 58.159 16.044 59.355 14.074 60.632 12.104 61.909 10.427 63.232 9.043 64.602 7.425 66.181 6.615 67.887 6.615 69.721 6.638 71.555 7.471 73.297 9.113 74.945 10.778 76.57 12.525 77.383 14.355 77.383 16.208 77.406 17.945 76.617 19.563 75.015 20.97 73.645 22.307 71.985 23.574 70.035 24.864 68.085 26.072 66.122 27.198 64.149 28.324 62.152 29.344 60.377 30.259 58.821 31.197 57.242 32.007 56.116 32.687 55.443 33.508 54.654 34.364 54.235 35.255 54.189 36.146 54.119 36.956 54.444 37.683 55.164L46.127 63.557C46.971 64.416 47.804 64.404 48.625 63.522L51.299 60.875C52.12 60.039 52.132 59.216 51.334 58.403L25.79 33.154C25.415 32.759 25.016 32.574 24.594 32.597 24.172 32.597 23.762 32.794 23.363 33.189L20.618 35.836C19.774 36.649 19.774 37.472 20.618 38.308ZM11.294 72.855C10.473 72.042 10.063 71.068 10.063 69.93 10.063 68.792 10.473 67.818 11.294 67.005 12.115 66.192 13.101 65.786 14.25 65.786 15.399 65.786 16.384 66.192 17.205 67.005 18.026 67.818 18.437 68.792 18.437 69.93 18.437 71.068 18.026 72.042 17.205 72.855 16.384 73.668 15.399 74.074 14.25 74.074 13.101 74.074 12.115 73.668 11.294 72.855ZM56.257 54.603L54.309 52.669 68.012 38.552C68.669 37.902 68.997 37.182 68.997 36.393 68.997 35.604 68.633 34.849 67.906 34.129L41.764 8.289C41.412 7.941 41.048 7.754 40.673 7.731 40.321 7.708 39.993 7.825 39.688 8.08 39.407 8.312 39.196 8.695 39.055 9.229 38.211 12.294 37.495 14.917 36.909 17.099 36.323 19.281 35.712 21.244 35.079 22.985 34.446 24.726 33.636 26.444 32.651 28.139 31.689 29.811 31.369 29.906 31.369 29.906L28.992 27.547 26.531 25.828C27.258 24.69 28.147 22.962 28.64 21.871 29.133 20.757 29.578 19.514 29.977 18.144 30.399 16.751 30.845 15.079 31.314 13.129 31.783 11.156 32.358 8.718 33.038 5.816 33.39 4.307 34.023 3.088 34.938 2.159 35.853 1.207 36.909 0.569 38.105 0.244 39.325-0.081 40.556-0.081 41.799 0.244 43.042 0.546 44.168 1.184 45.177 2.159L72.832 29.567C74.92 31.657 75.975 33.85 75.998 36.149 76.045 38.447 75.049 40.607 73.008 42.627L58.083 56.603 56.257 54.603ZM50.455 37.786C52.425 35.836 54.138 33.7 55.592 31.378 57.07 29.056 57.973 26.352 58.301 23.264L66.041 30.89C65.15 32.004 63.93 33.142 62.382 34.303 60.834 35.441 59.216 36.463 57.527 37.368 55.862 38.25 54.36 38.878 53.023 39.249 51.709 39.62 50.818 39.573 50.349 39.109 49.927 38.714 49.962 38.274 50.455 37.786Z"/>
</g>
</g>
<g class="zen-boosts-star-small" style="transform-origin: 0 0;" transform="translate(68.002 37.075) rotate(2.014)">
<g class="zen-boosts-star-small-anchor" transform="translate(-8 -8)">
<path class="zen-boosts-border" d="M8 0C8 4.418 4.418 8 0 8 4.418 8 8 11.582 8 16 8 11.582 11.582 8 16 8 11.582 8 8 4.418 8 0Z" fill="context-stroke" stroke="context-stroke" stroke-width="3" stroke-linejoin="round"/>
</g>
</g>
<g class="zen-boosts-star-large" style="transform-origin: 0 0;" transform="translate(85.002 50.174) rotate(2.014)">
<g class="zen-boosts-star-large-anchor" transform="translate(-12 -12)">
<path class="zen-boosts-border" d="M12 0C12 6.627 6.627 12 0 12 6.627 12 12 17.373 12 24 12 17.373 17.373 12 24 12 17.373 12 12 6.627 12 0Z" fill="context-stroke" stroke="context-stroke" stroke-width="3" stroke-linejoin="round"/>
</g>
</g>
</svg>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 400 KiB

View File

@@ -0,0 +1,32 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-boosts-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="zen-boosts-mask" maskContentUnits="userSpaceOnUse">
<rect x="-100" y="-100" width="300" height="300" fill="#fff"/>
<path fill="#000" stroke="#000" stroke-width="8" stroke-linejoin="round" d="M-3.79 54.121C-5.31 51.635-6.984 48.884-7.082 42.132L-7.073 42.091-7.063 42.051C-6.2 38.573-3.904 36.054-1.1 34.382L4.474 31.059C6.968 29.572 9.896 28.739 13.003 29.233 13.558 28.514 14.137 27.648 14.736 26.617L14.752 26.589 14.769 26.561C15.438 25.438 16.22 24.032 17.105 22.317 18.032 20.521 19.176 18.3 20.531 15.65 21.63 13.5 23.221 11.59 25.405 10.221 27.368 8.99 29.589 8.271 31.984 8.218L32.013 8.218C34.325 8.178 36.528 8.749 38.557 9.821 40.713 10.96 42.401 12.627 43.642 14.615L43.662 14.646 63.922 47.997C66.006 51.466 67.049 55.422 66.104 59.547 65.148 63.723 62.394 66.785 58.898 68.869L44.274 77.586C44.222 77.953 44.148 78.323 44.06 78.696 43.24 82.158 40.963 84.66 38.246 86.34L38.205 86.365 32.548 89.738C29.738 91.413 26.42 92.18 22.922 91.299 18.565 90.201 14.485 87.919 10.34 86.196 10.34 86.196 8.779 87.737 7.305 89.123 5.699 90.632 4.021 91.927 2.291 92.956-2.345 95.712-7.442 96.912-12.65 95.581-17.868 94.248-21.776 90.71-24.597 86.068-27.411 81.439-28.702 76.368-27.456 71.195-26.206 66.005-22.739 62.101-18.096 59.333-16.417 58.332-14.507 57.488-12.438 56.763L-12.4 56.75C-10.473 56.089-8.488 55.472-6.445 54.899-5.537 54.635-4.652 54.375-3.79 54.121Z"/>
</mask>
</defs>
<g transform="translate(61.889 63.143) scale(1.1) rotate(-15)">
<g transform="translate(-44 -44)">
<rect width="88" height="88" rx="16" mask="url(#zen-boosts-mask)" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g transform="translate(18.247 109.504) scale(1.1)">
<g transform="translate(-15 -70)">
<path fill-rule="evenodd" fill="context-stroke" d="M0 69.826C-0.023 66.459 1.467 63.29 4.469 60.318 5.501 59.296 6.814 58.264 8.409 57.219 10.027 56.174 11.717 55.176 13.476 54.154 15.235 53.132 16.877 52.158 18.472 51.229L22.377 48.617 15.024 41.373C13.546 39.91 12.795 38.354 12.772 36.706 12.772 35.034 13.5 33.479 14.954 32.04L19.563 27.478C21.017 26.039 22.577 25.33 24.242 25.353 26.219 25.381 26.531 25.828 28.992 27.547L56.964 55.303C58.465 56.766 59.215 58.322 59.215 59.97 59.238 61.595 58.535 63.139 57.104 64.602L52.46 69.199C51.029 70.615 49.47 71.311 47.781 71.288 46.092 71.288 44.52 70.557 43.066 69.094L35.748 61.816C34.904 62.814 34.012 64.091 33.074 65.647 32.136 67.203 31.15 68.862 30.118 70.627 29.109 72.392 28.078 74.063 27.022 75.642 25.99 77.244 24.946 78.555 23.89 79.577 20.888 82.549 17.686 84.023 14.285 84 10.884 83.977 7.658 82.444 4.609 79.403 1.56 76.385 0.023 73.193 0 69.826ZM20.618 38.308L29.133 46.701C29.86 47.398 30.188 48.187 30.118 49.069 30.048 49.951 29.625 50.799 28.851 51.612 28.171 52.309 27.034 53.11 25.439 54.015 23.844 54.92 22.037 55.931 20.02 57.045 18.026 58.159 16.044 59.355 14.074 60.632 12.104 61.909 10.427 63.232 9.043 64.602 7.425 66.181 6.615 67.887 6.615 69.721 6.638 71.555 7.471 73.297 9.113 74.945 10.778 76.57 12.525 77.383 14.355 77.383 16.208 77.406 17.945 76.617 19.563 75.015 20.97 73.645 22.307 71.985 23.574 70.035 24.864 68.085 26.072 66.122 27.198 64.149 28.324 62.152 29.344 60.377 30.259 58.821 31.197 57.242 32.007 56.116 32.687 55.443 33.508 54.654 34.364 54.235 35.255 54.189 36.146 54.119 36.956 54.444 37.683 55.164L46.127 63.557C46.971 64.416 47.804 64.404 48.625 63.522L51.299 60.875C52.12 60.039 52.132 59.216 51.334 58.403L25.79 33.154C25.415 32.759 25.016 32.574 24.594 32.597 24.172 32.597 23.762 32.794 23.363 33.189L20.618 35.836C19.774 36.649 19.774 37.472 20.618 38.308ZM11.294 72.855C10.473 72.042 10.063 71.068 10.063 69.93 10.063 68.792 10.473 67.818 11.294 67.005 12.115 66.192 13.101 65.786 14.25 65.786 15.399 65.786 16.384 66.192 17.205 67.005 18.026 67.818 18.437 68.792 18.437 69.93 18.437 71.068 18.026 72.042 17.205 72.855 16.384 73.668 15.399 74.074 14.25 74.074 13.101 74.074 12.115 73.668 11.294 72.855ZM56.257 54.603L54.309 52.669 68.012 38.552C68.669 37.902 68.997 37.182 68.997 36.393 68.997 35.604 68.633 34.849 67.906 34.129L41.764 8.289C41.412 7.941 41.048 7.754 40.673 7.731 40.321 7.708 39.993 7.825 39.688 8.08 39.407 8.312 39.196 8.695 39.055 9.229 38.211 12.294 37.495 14.917 36.909 17.099 36.323 19.281 35.712 21.244 35.079 22.985 34.446 24.726 33.636 26.444 32.651 28.139 31.689 29.811 31.369 29.906 31.369 29.906L28.992 27.547 26.531 25.828C27.258 24.69 28.147 22.962 28.64 21.871 29.133 20.757 29.578 19.514 29.977 18.144 30.399 16.751 30.845 15.079 31.314 13.129 31.783 11.156 32.358 8.718 33.038 5.816 33.39 4.307 34.023 3.088 34.938 2.159 35.853 1.207 36.909 0.569 38.105 0.244 39.325-0.081 40.556-0.081 41.799 0.244 43.042 0.546 44.168 1.184 45.177 2.159L72.832 29.567C74.92 31.657 75.975 33.85 75.998 36.149 76.045 38.447 75.049 40.607 73.008 42.627L58.083 56.603 56.257 54.603ZM50.455 37.786C52.425 35.836 54.138 33.7 55.592 31.378 57.07 29.056 57.973 26.352 58.301 23.264L66.041 30.89C65.15 32.004 63.93 33.142 62.382 34.303 60.834 35.441 59.216 36.463 57.527 37.368 55.862 38.25 54.36 38.878 53.023 39.249 51.709 39.62 50.818 39.573 50.349 39.109 49.927 38.714 49.962 38.274 50.455 37.786Z"/>
</g>
</g>
<g transform="translate(68.002 37.075) rotate(2.014)">
<g transform="translate(-8 -8)">
<path d="M8 0C8 4.418 4.418 8 0 8 4.418 8 8 11.582 8 16 8 11.582 11.582 8 16 8 11.582 8 8 4.418 8 0Z" fill="context-stroke" stroke="context-stroke" stroke-width="3" stroke-linejoin="round"/>
</g>
</g>
<g transform="translate(85.002 50.174) rotate(2.014)">
<g transform="translate(-12 -12)">
<path d="M12 0C12 6.627 6.627 12 0 12 6.627 12 12 17.373 12 24 12 17.373 17.373 12 24 12 17.373 12 12 6.627 12 0Z" fill="context-stroke" stroke="context-stroke" stroke-width="3" stroke-linejoin="round"/>
</g>
</g>
</svg>

View File

@@ -0,0 +1,36 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-downloads-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes zenDownloadsArrowBounce {
0% { transform: translateY(0px); d: path('M 64 45 L 64 83 M 50 69 L 64 83 L 78 69'); animation-timing-function: cubic-bezier(0.33, 0, 0.7, 1.28); }
31.43% { transform: translateY(10px); d: path('M 64 57 L 64 83 M 50 69 L 64 83 L 78 69'); animation-timing-function: cubic-bezier(0.44, 0.2, 0.44, 0.98); }
54.29% { transform: translateY(-10.83px); d: path('M 64 45 L 64 83 M 50 69 L 64 83 L 78 69'); animation-timing-function: cubic-bezier(0.45, -0.03, 0.83, 0.9); }
85.71% { transform: translateY(0.66px); d: path('M 64 45 L 64 83 M 50 69 L 64 83 L 78 69'); animation-timing-function: cubic-bezier(0.17, -0.98, 0.83, 0.83); }
100% { transform: translateY(0px); d: path('M 64 45 L 64 83 M 50 69 L 64 83 L 78 69'); }
}
@keyframes zenDownloadsCircleBounce {
0% { transform: translateY(0px); animation-timing-function: cubic-bezier(0.33, 0, 0.71, 2.31); }
31.43% { transform: translateY(2.5px); animation-timing-function: cubic-bezier(0.23, 0.15, 0.36, 0.97); }
54.29% { transform: translateY(-11.46px); animation-timing-function: cubic-bezier(0.48, -0.04, 0.83, 0.89); }
85.71% { transform: translateY(0.85px); animation-timing-function: cubic-bezier(0.17, -0.70, 0.83, 0.83); }
100% { transform: translateY(0px); }
}
.zen-downloads-circle-translate { animation: zenDownloadsCircleBounce 0.583s forwards; }
.zen-downloads-arrow { animation: zenDownloadsArrowBounce 0.583s forwards; }
</style>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" style="transform-origin: 64px 64px;">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

View File

@@ -0,0 +1,511 @@
<!-- 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/. -->
<!-- Generated by scripts/generate-library-sprites.mjs. Do not edit;
- regenerate from the library-*-active.svg sources instead. -->
<svg xmlns="http://www.w3.org/2000/svg" width="1008" height="28" fill="none">
<svg x="0" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f0">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f0)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0)"/>
</svg>
<svg x="28" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f1">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.131) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f1)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45.355 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.296)"/>
</svg>
<svg x="56" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f2">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.473) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f2)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 46.299 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 1.082)"/>
</svg>
<svg x="84" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f3">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.955) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f3)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 47.669 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 2.224)"/>
</svg>
<svg x="112" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f4">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 1.517) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f4)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 49.32 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 3.6)"/>
</svg>
<svg x="140" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f5">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 2.103) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f5)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 51.12 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 5.1)"/>
</svg>
<svg x="168" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f6">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 2.656) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f6)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 52.937 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 6.614)"/>
</svg>
<svg x="196" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f7">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.119) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f7)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 54.637 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 8.031)"/>
</svg>
<svg x="224" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f8">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.425) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f8)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 56.076 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 9.23)"/>
</svg>
<svg x="252" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f9">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.496) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f9)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 57.089 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 10.075)"/>
</svg>
<svg x="280" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f10">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 3.233) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f10)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 57.481 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 10.4)"/>
</svg>
<svg x="308" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f11">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 2.501) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f11)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 57.001 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 10)"/>
</svg>
<svg x="336" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f12">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.423) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f12)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 56.035 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 8.325)"/>
</svg>
<svg x="364" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f13">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -2.757) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f13)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 54.37 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 5.434)"/>
</svg>
<svg x="392" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f14">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -5.733) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f14)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 51.937 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 1.211)"/>
</svg>
<svg x="420" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f15">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -8.006) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f15)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 49.263 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -3.429)"/>
</svg>
<svg x="448" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f16">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -9.597) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f16)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 47.182 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -7.043)"/>
</svg>
<svg x="476" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f17">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -10.631) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f17)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45.895 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -9.277)"/>
</svg>
<svg x="504" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f18">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.223) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f18)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45.228 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.434)"/>
</svg>
<svg x="532" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f19">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.46) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f19)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.83)"/>
</svg>
<svg x="560" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f20">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.413) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f20)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.754)"/>
</svg>
<svg x="588" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f21">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -11.089) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f21)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -10.398)"/>
</svg>
<svg x="616" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f22">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -10.494) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f22)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -9.781)"/>
</svg>
<svg x="644" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f23">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -9.636) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f23)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -8.922)"/>
</svg>
<svg x="672" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f24">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -8.527) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f24)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -7.844)"/>
</svg>
<svg x="700" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f25">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -7.186) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f25)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -6.57)"/>
</svg>
<svg x="728" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f26">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -5.639) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f26)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -5.133)"/>
</svg>
<svg x="756" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f27">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -3.928) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f27)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -3.575)"/>
</svg>
<svg x="784" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f28">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -2.132) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f28)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -1.967)"/>
</svg>
<svg x="812" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f29">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 -0.407) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f29)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 -0.441)"/>
</svg>
<svg x="840" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f30">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.851) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f30)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.661)"/>
</svg>
<svg x="868" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f31">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.99) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f31)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.846)"/>
</svg>
<svg x="896" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f32">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.823) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f32)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.718)"/>
</svg>
<svg x="924" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f33">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.566) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f33)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.496)"/>
</svg>
<svg x="952" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f34">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0.271) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f34)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0.236)"/>
</svg>
<svg x="980" width="28" height="28" viewBox="0 0 128 128">
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-downloads-grad-front-f35">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-downloads-circle-translate" transform="translate(64 64) translate(0 0) translate(-64 -64)">
<circle class="zen-downloads-bg" cx="64" cy="64" r="47.5" fill="context-fill" fill-opacity="1"/>
<circle class="zen-downloads-gradient" cx="64" cy="64" r="47.5" fill="url(#zen-downloads-grad-front-f35)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<circle class="zen-downloads-border" cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path class="zen-downloads-arrow" d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 0)"/>
</svg>
</svg>

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -0,0 +1,8 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-downloads-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="64" cy="64" r="47.5" fill="none" stroke="context-stroke" stroke-width="7.1"/>
<path d="M 64 45 L 64 83 M 50 69 L 64 83 L 78 69" stroke="context-stroke" stroke-width="7.1" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

View File

@@ -0,0 +1,62 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-history-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes zenHistoryBodyTranslate {
0%, 31.43% { transform: translate(63.977px, 79.047px); animation-timing-function: cubic-bezier(0.16, 0.41, 0.34, 1); }
60% { transform: translate(63.977px, 63.253px); animation-timing-function: cubic-bezier(0.33, 0, 0.62, 0.55); }
77.14%, 100% { transform: translate(63.977px, 79.047px); }
}
@keyframes zenHistoryLid {
0% { transform: translate(63.977px, 37.148px) rotate(0deg) translate(-46.852px, -12.82px); animation-timing-function: cubic-bezier(0.21, 0, 0.56, 1); }
31.43% { transform: translate(63.977px, 51.609px) rotate(0deg) translate(-46.852px, -12.82px); animation-timing-function: cubic-bezier(0.38, 0, 0.55, 1); }
62.86% { transform: translate(63.977px, 17.08px) rotate(-6.93deg) translate(-46.852px, -12.82px); animation-timing-function: cubic-bezier(0.43, 0.14, 0.38, 1); }
82.86% { transform: translate(63.977px, 23.397px) rotate(4.2deg) translate(-46.852px, -12.82px); animation-timing-function: cubic-bezier(0.41, 0, 0.49, 1); }
100% { transform: translate(63.977px, 37.148px) rotate(0deg) translate(-46.852px, -12.82px); }
}
@keyframes zenHistoryDashTranslate {
0% { transform: translate(64px, 65px) scale(0.9, 1); animation-timing-function: cubic-bezier(0.33, 0, 0.7, 1.28); }
31.43% { transform: translate(64px, 79.461px) scale(0.8, 1); animation-timing-function: cubic-bezier(0.44, 0.2, 0.44, 0.98); }
54.29% { transform: translate(64px, 61.033px) scale(0.86, 1); animation-timing-function: cubic-bezier(0.45, -0.03, 0.83, 0.9); }
85.71% { transform: translate(64px, 65.66px) scale(0.9, 1); animation-timing-function: cubic-bezier(0.22, -0.9, 0.25, 1.25); }
100% { transform: translate(64px, 65px) scale(0.9, 1); }
}
.zen-history-body-translate { animation: zenHistoryBodyTranslate 0.583s forwards; }
.zen-history-lid { animation: zenHistoryLid 0.583s forwards; }
.zen-history-dash-translate { animation: zenHistoryDashTranslate 0.583s forwards; }
</style>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="0" x2="64" y2="128" id="zen-history-grad-front">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-history-body-translate" style="transform-origin: 0 0; transform: translate(63.977px, 79.047px);">
<g transform="translate(-39.867, -30.328)">
<path class="zen-history-bg"
d="M 3.55 0 L 76.184 0 L 76.184 46.856 A 10.25 10.25 0 0 1 65.934 57.106 L 13.8 57.106 A 10.25 10.25 0 0 1 3.55 46.856 Z"
fill="context-fill" fill-opacity="1"/>
<path class="zen-history-gradient"
d="M 3.55 0 L 76.184 0 L 76.184 46.856 A 10.25 10.25 0 0 1 65.934 57.106 L 13.8 57.106 A 10.25 10.25 0 0 1 3.55 46.856 Z"
fill="url(#zen-history-grad-front)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<path class="zen-history-border"
d="M 3.55 0 L 76.184 0 L 76.184 46.856 A 10.25 10.25 0 0 1 65.934 57.106 L 13.8 57.106 A 10.25 10.25 0 0 1 3.55 46.856 Z"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g class="zen-history-lid" style="transform-origin: 0 0; transform: translate(63.977px, 37.148px) rotate(0deg) translate(-46.852px, -12.82px);">
<rect class="zen-history-bg" x="3.55" y="3.55" width="86.603" height="18.541" rx="6.05"
fill="context-fill" fill-opacity="1"/>
<rect class="zen-history-gradient" x="3.55" y="3.55" width="86.603" height="18.541" rx="6.05"
fill="url(#zen-history-grad-front)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<rect class="zen-history-border" x="3.55" y="3.55" width="86.603" height="18.541" rx="6.05"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<g class="zen-history-dash-translate" style="transform-origin: 0 0; transform: translate(64px, 65px) scale(0.9, 1);">
<path class="zen-history-dash-path" fill="none"
d="M -16 0 L 16 0"
stroke="context-stroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -0,0 +1,14 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-history-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(63.977, 79.047)">
<path d="M -36.317 -30.328 L 36.317 -30.328 L 36.317 16.528 A 10.25 10.25 0 0 1 26.067 26.778 L -26.067 26.778 A 10.25 10.25 0 0 1 -36.317 16.528 Z"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<g transform="translate(17.125, 24.328)">
<rect x="3.55" y="3.55" width="86.603" height="18.541" rx="6.05" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<path d="M 49.6 65 L 78.4 65" fill="none" stroke="context-stroke" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

View File

@@ -0,0 +1,74 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-media-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes zenMediaBackBounce {
0% { transform: translate(54.799px, 57.743px) rotate(-7deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1); }
24.41% { transform: translate(57.299px, 65.243px) rotate(3deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.3, 0, 0.8, 1); }
43.67% { transform: translate(56.16px, 47.803px) rotate(-18.58deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.16, 0, 0.8, 1); }
78.21% { transform: translate(55.25px, 54.3px) rotate(-3.2deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.65, 0, 0.35, 1); }
100% { transform: translate(54.799px, 57.743px) rotate(-7deg) translate(-46.27px, -36.445px); }
}
@keyframes zenMediaFrontBounce {
0% { transform: translate(78.827px, 77.737px) rotate(0deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.33, 0, 0.67, 1); }
24.41% { transform: translate(78.827px, 80.737px) rotate(0deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.4, 0, 0.64, 1); }
55.88% { transform: translate(75.827px, 62.61px) rotate(9.32deg) translate(-46.27px, -36.445px); animation-timing-function: cubic-bezier(0.25, 0, 0.75, 1); }
88.24% { transform: translate(78.827px, 77.737px) rotate(0deg) translate(-46.27px, -36.445px); animation-timing-function: linear; }
100% { transform: translate(78.827px, 77.737px) rotate(0deg) translate(-46.27px, -36.445px); }
}
@keyframes zenMediaSunBounce {
0% { transform: translate(64.76px, 67.886px) translate(-9.914px, -9.984px); animation-timing-function: cubic-bezier(0.32, 0, 0.67, 1); }
24.41% { transform: translate(64.76px, 73.606px) translate(-9.914px, -9.984px); animation-timing-function: linear; }
35.29% { transform: translate(64.76px, 73.606px) translate(-9.914px, -9.984px); animation-timing-function: cubic-bezier(0.35, 0, 0.76, 0.72); }
55.88% { transform: translate(61.737px, 51.974px) translate(-9.914px, -9.984px); animation-timing-function: cubic-bezier(0.21, -0.71, 0.45, 1); }
100% { transform: translate(64.76px, 67.886px) translate(-9.914px, -9.984px); }
}
.zen-media-back-card { animation: zenMediaBackBounce 0.583s forwards; }
.zen-media-front-card { animation: zenMediaFrontBounce 0.583s forwards; }
.zen-media-sun { animation: zenMediaSunBounce 0.583s forwards; }
</style>
<defs>
<mask id="zen-media-mask">
<rect x="-10" y="-10" width="148" height="148" fill="white"/>
<g class="zen-media-front-card-mask" transform="translate(78.827, 77.737) translate(-46.27, -36.445)">
<rect x="0" y="0" width="92.539" height="72.891" rx="12.812" fill="black"/>
</g>
</mask>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="0" x2="64" y2="128" id="zen-media-grad-back">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="0" x2="64" y2="128" id="zen-media-grad-front">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-media-back-wrapper" mask="url(#zen-media-mask)">
<g class="zen-media-back-card" transform="translate(54.799, 57.743) rotate(-7) translate(-46.27, -36.445)">
<rect class="zen-media-bg" x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262"
fill="context-fill" fill-opacity="1"/>
<rect class="zen-media-gradient" x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262"
fill="url(#zen-media-grad-back)" fill-opacity="1" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<rect class="zen-media-border" x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g class="zen-media-front-card" transform="translate(78.827, 77.737) translate(-46.27, -36.445)">
<rect class="zen-media-bg" x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262"
fill="context-fill" fill-opacity="1"/>
<rect class="zen-media-gradient" x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262"
fill="url(#zen-media-grad-front)" fill-opacity="1" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<g class="zen-media-mountain" transform="translate(0.289, 32.609)">
<path class="zen-media-mountain-path" d="M7.432 21.147 L17.865 12.11 C19.665 10.596 21.373 9.862 23.173 9.862 C25.158 9.862 27.005 10.596 28.805 12.202 L36.191 18.853 L54.84 2.431 C56.779 0.734 58.81 0 61.072 0 C63.334 0 65.55 0.826 67.35 2.477 L84.568 18.67 L92 25.78 C92 35.23 87.153 40 77.551 40 L14.495 40 C4.801 40 0 35.275 0 25.78 Z"
fill="context-stroke"/>
</g>
<rect class="zen-media-border" x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
<g class="zen-media-sun" transform="translate(64.76, 67.886) translate(-9.914, -9.984)">
<circle class="zen-media-sun-path" cx="9.914" cy="9.984" r="9.914"
fill="context-stroke"/>
</g>
</svg>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -0,0 +1,22 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-media-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="zen-media-mask-inactive">
<rect x="-10" y="-10" width="148" height="148" fill="white"/>
<g transform="translate(78.827, 77.737) translate(-46.27, -36.445)">
<rect x="0" y="0" width="92.539" height="72.891" rx="12.812" fill="black"/>
</g>
</mask>
</defs>
<g mask="url(#zen-media-mask-inactive)">
<g transform="translate(54.799, 57.743) rotate(-7) translate(-46.27, -36.445)">
<rect x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g transform="translate(78.827, 77.737) translate(-46.27, -36.445)">
<rect x="3.55" y="3.55" width="85.439" height="65.791" rx="9.262" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</svg>

View File

@@ -0,0 +1,56 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-spaces-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<style>
@keyframes zenSpacesBackBounce {
0% { transform: translate(51.28px, 61.69px) rotate(-17.5deg) translate(-35.022px, -44.68px); animation-timing-function: cubic-bezier(0.21, 0, 0.56, 1); }
34.28% { transform: translate(56.28px, 72.19px) rotate(6deg) translate(-35.022px, -44.68px); animation-timing-function: cubic-bezier(0.38, 0, 0.55, 1); }
62.86% { transform: translate(51.28px, 55.52px) rotate(-27.83deg) translate(-35.022px, -44.68px); animation-timing-function: cubic-bezier(0.73, 0, 0.63, 1); }
100% { transform: translate(51.28px, 61.69px) rotate(-17.5deg) translate(-35.022px, -44.68px); }
}
@keyframes zenSpacesFrontBounce {
0% { transform: translate(77.02px, 75.93px) rotate(0deg) translate(-35.022px, -44.68px); animation-timing-function: cubic-bezier(0.21, 0, 0.56, 1); }
31.43% { transform: translate(77.02px, 78.18px) rotate(-5deg) translate(-35.022px, -44.68px); animation-timing-function: cubic-bezier(0.38, 0, 0.55, 1); }
54.28% { transform: translate(77.02px, 55.71px) rotate(28.96deg) translate(-35.022px, -44.68px); animation-timing-function: cubic-bezier(0.73, 0, 0.63, 1); }
100% { transform: translate(77.02px, 75.93px) rotate(0deg) translate(-35.022px, -44.68px); }
}
.zen-spaces-back-card { animation: zenSpacesBackBounce 0.583s forwards; }
.zen-spaces-front-card { animation: zenSpacesFrontBounce 0.583s forwards; }
</style>
<defs>
<mask id="zen-spaces-mask">
<rect x="-10" y="-10" width="148" height="148" fill="white"/>
<g class="zen-spaces-front-card" style="transform-origin: 0 0; transform: translate(77.02px, 75.93px) rotate(0deg) translate(-35.022px, -44.68px);">
<rect x="0" y="0" width="70.04" height="89.36" rx="14" fill="black"/>
</g>
</mask>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-spaces-grad-back">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
<linearGradient gradientUnits="userSpaceOnUse" x1="64" y1="20" x2="64" y2="148" id="zen-spaces-grad-front">
<stop offset="0" stop-color="context-fill"/>
<stop offset="1" stop-color="context-stroke"/>
</linearGradient>
</defs>
<g class="zen-spaces-back-wrapper" mask="url(#zen-spaces-mask)">
<g class="zen-spaces-back-card" style="transform-origin: 0 0; transform: translate(51.28px, 61.69px) rotate(-17.5deg) translate(-35.022px, -44.68px);">
<rect class="zen-spaces-bg" x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45"
fill="context-fill" fill-opacity="1"/>
<rect class="zen-spaces-gradient" x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45"
fill="url(#zen-spaces-grad-back)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<rect class="zen-spaces-border" x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g class="zen-spaces-front-card" style="transform-origin: 0 0; transform: translate(77.02px, 75.93px) rotate(0deg) translate(-35.022px, -44.68px);">
<rect class="zen-spaces-bg" x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45"
fill="context-fill" fill-opacity="1"/>
<rect class="zen-spaces-gradient" x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45"
fill="url(#zen-spaces-grad-front)" style="mix-blend-mode: overlay; opacity: 0.1;"/>
<rect class="zen-spaces-border" x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45"
fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</svg>

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -0,0 +1,22 @@
#filter dumbComments emptyLines substitution
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<svg class="zen-spaces-icon" width="28" height="28" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="zen-spaces-mask-inactive">
<rect x="-10" y="-10" width="148" height="148" fill="white"/>
<g style="transform-origin: 0 0; transform: translate(77.02px, 75.93px) translate(-35.022px, -44.68px);">
<rect x="0" y="0" width="70.04" height="89.36" rx="14" fill="black"/>
</g>
</mask>
</defs>
<g mask="url(#zen-spaces-mask-inactive)">
<g style="transform-origin: 0 0; transform: translate(51.28px, 61.69px) rotate(-17.5deg) translate(-35.022px, -44.68px);">
<rect x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</g>
<g style="transform-origin: 0 0; transform: translate(77.02px, 75.93px) translate(-35.022px, -44.68px);">
<rect x="3.55" y="3.55" width="62.94" height="82.26" rx="10.45" fill="none" stroke="context-stroke" stroke-width="7.1"/>
</g>
</svg>

View File

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

View File

@@ -309,7 +309,8 @@
display: none;
}
#library-button {
#library-button,
#zen-library-button {
list-style-image: url("library.svg") !important;
}

View File

@@ -14,8 +14,8 @@
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
@@ -104,26 +104,26 @@
* skin/classic/browser/zen-icons/popup-fill.svg (../shared/zen-icons/nucleo/popup-fill.svg)
* skin/classic/browser/zen-icons/popup.svg (../shared/zen-icons/nucleo/popup.svg)
* skin/classic/browser/zen-icons/print.svg (../shared/zen-icons/nucleo/print.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
* skin/classic/browser/zen-icons/reader-mode.svg (../shared/zen-icons/nucleo/reader-mode.svg)
* skin/classic/browser/zen-icons/reload.svg (../shared/zen-icons/nucleo/reload.svg)
* skin/classic/browser/zen-icons/save.svg (../shared/zen-icons/nucleo/save.svg)
* skin/classic/browser/zen-icons/screen-blocked.svg (../shared/zen-icons/nucleo/screen-blocked.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
* skin/classic/browser/zen-icons/search-glass.svg (../shared/zen-icons/nucleo/search-glass.svg)
* skin/classic/browser/zen-icons/search-page.svg (../shared/zen-icons/nucleo/search-page.svg)
* skin/classic/browser/zen-icons/security-broken.svg (../shared/zen-icons/nucleo/security-broken.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
* skin/classic/browser/zen-icons/send-to-device.svg (../shared/zen-icons/nucleo/send-to-device.svg)
* skin/classic/browser/zen-icons/settings-fill.svg (../shared/zen-icons/nucleo/settings-fill.svg)
* skin/classic/browser/zen-icons/settings.svg (../shared/zen-icons/nucleo/settings.svg)
* skin/classic/browser/zen-icons/share.svg (../shared/zen-icons/nucleo/share.svg)
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
@@ -164,8 +164,8 @@
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
@@ -254,26 +254,26 @@
* skin/classic/browser/zen-icons/popup-fill.svg (../shared/zen-icons/nucleo/popup-fill.svg)
* skin/classic/browser/zen-icons/popup.svg (../shared/zen-icons/nucleo/popup.svg)
* skin/classic/browser/zen-icons/print.svg (../shared/zen-icons/nucleo/print.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
* skin/classic/browser/zen-icons/reader-mode.svg (../shared/zen-icons/nucleo/reader-mode.svg)
* skin/classic/browser/zen-icons/reload.svg (../shared/zen-icons/nucleo/reload.svg)
* skin/classic/browser/zen-icons/save.svg (../shared/zen-icons/nucleo/save.svg)
* skin/classic/browser/zen-icons/screen-blocked.svg (../shared/zen-icons/nucleo/screen-blocked.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
* skin/classic/browser/zen-icons/search-glass.svg (../shared/zen-icons/nucleo/search-glass.svg)
* skin/classic/browser/zen-icons/search-page.svg (../shared/zen-icons/nucleo/search-page.svg)
* skin/classic/browser/zen-icons/security-broken.svg (../shared/zen-icons/nucleo/security-broken.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
* skin/classic/browser/zen-icons/send-to-device.svg (../shared/zen-icons/nucleo/send-to-device.svg)
* skin/classic/browser/zen-icons/settings-fill.svg (../shared/zen-icons/nucleo/settings-fill.svg)
* skin/classic/browser/zen-icons/settings.svg (../shared/zen-icons/nucleo/settings.svg)
* skin/classic/browser/zen-icons/share.svg (../shared/zen-icons/nucleo/share.svg)
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
@@ -314,8 +314,8 @@
* skin/classic/browser/zen-icons/autoplay-media-fill.svg (../shared/zen-icons/nucleo/autoplay-media-fill.svg)
* skin/classic/browser/zen-icons/autoplay-media.svg (../shared/zen-icons/nucleo/autoplay-media.svg)
* skin/classic/browser/zen-icons/back.svg (../shared/zen-icons/nucleo/back.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/block.svg (../shared/zen-icons/nucleo/block.svg)
* skin/classic/browser/zen-icons/blocked-element.svg (../shared/zen-icons/nucleo/blocked-element.svg)
* skin/classic/browser/zen-icons/bolt.svg (../shared/zen-icons/nucleo/bolt.svg)
* skin/classic/browser/zen-icons/bookmark-hollow.svg (../shared/zen-icons/nucleo/bookmark-hollow.svg)
* skin/classic/browser/zen-icons/bookmark-star-on-tray.svg (../shared/zen-icons/nucleo/bookmark-star-on-tray.svg)
@@ -404,26 +404,26 @@
* skin/classic/browser/zen-icons/popup-fill.svg (../shared/zen-icons/nucleo/popup-fill.svg)
* skin/classic/browser/zen-icons/popup.svg (../shared/zen-icons/nucleo/popup.svg)
* skin/classic/browser/zen-icons/print.svg (../shared/zen-icons/nucleo/print.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
* skin/classic/browser/zen-icons/private-window.svg (../shared/zen-icons/nucleo/private-window.svg)
* skin/classic/browser/zen-icons/privateBrowsing.svg (../shared/zen-icons/nucleo/privateBrowsing.svg)
* skin/classic/browser/zen-icons/reader-mode.svg (../shared/zen-icons/nucleo/reader-mode.svg)
* skin/classic/browser/zen-icons/reload.svg (../shared/zen-icons/nucleo/reload.svg)
* skin/classic/browser/zen-icons/save.svg (../shared/zen-icons/nucleo/save.svg)
* skin/classic/browser/zen-icons/screen-blocked.svg (../shared/zen-icons/nucleo/screen-blocked.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
* skin/classic/browser/zen-icons/screen.svg (../shared/zen-icons/nucleo/screen.svg)
* skin/classic/browser/zen-icons/screenshot.svg (../shared/zen-icons/nucleo/screenshot.svg)
* skin/classic/browser/zen-icons/search-glass.svg (../shared/zen-icons/nucleo/search-glass.svg)
* skin/classic/browser/zen-icons/search-page.svg (../shared/zen-icons/nucleo/search-page.svg)
* skin/classic/browser/zen-icons/security-broken.svg (../shared/zen-icons/nucleo/security-broken.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
* skin/classic/browser/zen-icons/security-warning.svg (../shared/zen-icons/nucleo/security-warning.svg)
* skin/classic/browser/zen-icons/security.svg (../shared/zen-icons/nucleo/security.svg)
* skin/classic/browser/zen-icons/send-to-device.svg (../shared/zen-icons/nucleo/send-to-device.svg)
* skin/classic/browser/zen-icons/settings-fill.svg (../shared/zen-icons/nucleo/settings-fill.svg)
* skin/classic/browser/zen-icons/settings.svg (../shared/zen-icons/nucleo/settings.svg)
* skin/classic/browser/zen-icons/share.svg (../shared/zen-icons/nucleo/share.svg)
* skin/classic/browser/zen-icons/sidebar-right.svg (../shared/zen-icons/nucleo/sidebar-right.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sidebar.svg (../shared/zen-icons/nucleo/sidebar.svg)
* skin/classic/browser/zen-icons/sidebars-right.svg (../shared/zen-icons/nucleo/sidebars-right.svg)
* skin/classic/browser/zen-icons/sliders.svg (../shared/zen-icons/nucleo/sliders.svg)
* skin/classic/browser/zen-icons/sparkles.svg (../shared/zen-icons/nucleo/sparkles.svg)
* skin/classic/browser/zen-icons/spell-check.svg (../shared/zen-icons/nucleo/spell-check.svg)
@@ -453,14 +453,19 @@
* skin/classic/browser/zen-icons/zoom-out.svg (../shared/zen-icons/nucleo/zoom-out.svg)
#endif
* skin/classic/browser/zen-icons/urlbar-arrow.svg (../shared/zen-icons/common/urlbar-arrow.svg)
skin/classic/browser/zen-icons/library/library-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/selectable/airplane.svg (../shared/zen-icons/common/selectable/airplane.svg)
* skin/classic/browser/zen-icons/selectable/american-football.svg (../shared/zen-icons/common/selectable/american-football.svg)
* skin/classic/browser/zen-icons/selectable/baseball.svg (../shared/zen-icons/common/selectable/baseball.svg)
* skin/classic/browser/zen-icons/selectable/basket.svg (../shared/zen-icons/common/selectable/basket.svg)
* skin/classic/browser/zen-icons/selectable/bed.svg (../shared/zen-icons/common/selectable/bed.svg)
* skin/classic/browser/zen-icons/selectable/bell.svg (../shared/zen-icons/common/selectable/bell.svg)
* skin/classic/browser/zen-icons/selectable/bookmark.svg (../shared/zen-icons/common/selectable/bookmark.svg)
* skin/classic/browser/zen-icons/selectable/book.svg (../shared/zen-icons/common/selectable/book.svg)
* skin/classic/browser/zen-icons/selectable/bookmark.svg (../shared/zen-icons/common/selectable/bookmark.svg)
* skin/classic/browser/zen-icons/selectable/briefcase.svg (../shared/zen-icons/common/selectable/briefcase.svg)
* skin/classic/browser/zen-icons/selectable/brush.svg (../shared/zen-icons/common/selectable/brush.svg)
* skin/classic/browser/zen-icons/selectable/bug.svg (../shared/zen-icons/common/selectable/bug.svg)
@@ -522,8 +527,8 @@
* skin/classic/browser/zen-icons/selectable/shapes.svg (../shared/zen-icons/common/selectable/shapes.svg)
* skin/classic/browser/zen-icons/selectable/shirt.svg (../shared/zen-icons/common/selectable/shirt.svg)
* skin/classic/browser/zen-icons/selectable/skull.svg (../shared/zen-icons/common/selectable/skull.svg)
* skin/classic/browser/zen-icons/selectable/squares.svg (../shared/zen-icons/common/selectable/squares.svg)
* skin/classic/browser/zen-icons/selectable/square.svg (../shared/zen-icons/common/selectable/square.svg)
* skin/classic/browser/zen-icons/selectable/squares.svg (../shared/zen-icons/common/selectable/squares.svg)
* skin/classic/browser/zen-icons/selectable/star-1.svg (../shared/zen-icons/common/selectable/star-1.svg)
* skin/classic/browser/zen-icons/selectable/star.svg (../shared/zen-icons/common/selectable/star.svg)
* skin/classic/browser/zen-icons/selectable/stats-chart.svg (../shared/zen-icons/common/selectable/stats-chart.svg)

View File

@@ -53,12 +53,15 @@ do_common_icons() {
echo "Working on $filename"
echo "* skin/classic/browser/zen-icons/$filename (../shared/zen-icons/common/$filename) " >> jar.inc.mn
done
for filename in common/selectable/*.svg; do
# remove the os/ prefix
add_header_to_file $filename
filename=$(basename $filename)
echo "Working on $filename"
echo "* skin/classic/browser/zen-icons/selectable/$filename (../shared/zen-icons/common/selectable/$filename) " >> jar.inc.mn
# go through all subdirectories of common and do the same
for dir in common/*/; do
display_dir=$(basename $dir)
for filename in $dir/*.svg; do
add_header_to_file $filename
filename=$(basename $filename)
echo "Working on $filename"
echo "* skin/classic/browser/zen-icons/$display_dir/$filename (../shared/zen-icons/common/$display_dir/$filename) " >> jar.inc.mn
done
done
}

View File

@@ -1,76 +0,0 @@
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -411,10 +411,13 @@
pref("browser.urlbar.maxRichResults", 10);
// The maximum number of historical search results to show.
pref("browser.urlbar.maxHistoricalSearchSuggestions", 2);
+// Whether to close the urlbar when blurring the window while the urlbar is focused.
+pref("browser.urlbar.closeOnWindowBlur", true);
+
// The default behavior for the urlbar can be configured to use any combination
// of the match filters with each additional filter adding more results (union).
pref("browser.urlbar.suggest.bookmark", true);
pref("browser.urlbar.suggest.clipboard", true);
pref("browser.urlbar.suggest.history", true);
diff --git a/browser/components/urlbar/UrlbarPrefs.sys.mjs b/browser/components/urlbar/UrlbarPrefs.sys.mjs
--- a/browser/components/urlbar/UrlbarPrefs.sys.mjs
+++ b/browser/components/urlbar/UrlbarPrefs.sys.mjs
@@ -152,10 +152,13 @@
["filter.javascript", true],
// Feature gate pref for flight status suggestions in the urlbar.
["flightStatus.featureGate", false],
+ // Whether to close the urlbar when blurring the window while the urlbar is focused.
+ ["closeOnWindowBlur", true],
+
// The minimum prefix length of a flight status keyword the user must type to
// trigger the suggestion. 0 means the min length should be taken from Nimbus
// or remote settings.
["flightStatus.minKeywordLength", 0],
diff --git a/browser/components/urlbar/content/UrlbarView.mjs b/browser/components/urlbar/content/UrlbarView.mjs
--- a/browser/components/urlbar/content/UrlbarView.mjs
+++ b/browser/components/urlbar/content/UrlbarView.mjs
@@ -3909,10 +3909,16 @@
}
}
}
on_blur() {
+ if (
+ this.document.commandDispatcher.focusedElement == this.input.inputField &&
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
+ ) {
+ return;
+ }
// If the view is open without the input being focused, it will not close
// automatically when the window loses focus. We might be in this state
// after a Search Tip is shown on an engine homepage.
if (!lazy.UrlbarPrefs.get("ui.popup.disable_autohide")) {
this.close();
diff --git a/browser/components/urlbar/content/UrlbarInput.mjs b/browser/components/urlbar/content/UrlbarInput.mjs
--- a/browser/components/urlbar/content/UrlbarInput.mjs
+++ b/browser/components/urlbar/content/UrlbarInput.mjs
@@ -5460,11 +5460,17 @@
_on_blur(event) {
if (this.view.resultMenu.hasAttribute("open")) {
return;
}
lazy.logger.debug("Blur Event");
+ if (
+ this.document.commandDispatcher.focusedElement == this.inputField &&
+ !lazy.UrlbarPrefs.get("closeOnWindowBlur")
+ ) {
+ return;
+ }
// We cannot count every blur events after a missed engagement as abandoment
// because the user may have clicked on some view element that executes
// a command causing a focus change. For example opening preferences from
// the oneoff settings button.
// For now we detect that case by discarding the event on command, but we
// may want to figure out a more robust way to detect abandonment.

View File

@@ -0,0 +1,166 @@
diff --git a/browser/components/sessionstore/test/browser.toml b/browser/components/sessionstore/test/browser.toml
--- a/browser/components/sessionstore/test/browser.toml
+++ b/browser/components/sessionstore/test/browser.toml
@@ -14,10 +14,12 @@
"browser_frame_history_c.html",
"browser_frame_history_c1.html",
"browser_frame_history_c2.html",
"browser_formdata_format_sample.html",
"browser_sessionHistory_slow.sjs",
+ "browser_policy_container_sample.html",
+ "browser_policy_container_sample_frame.html",
"browser_scrollPositions_sample.html",
"browser_scrollPositions_sample2.html",
"browser_scrollPositions_sample_frameset.html",
"browser_scrollPositions_readerModeArticle.html",
"browser_sessionStorage.html",
@@ -226,10 +228,12 @@
["browser_pinned_tabs.js"]
skip-if = [
"ccov", # Bug 1625525
]
+["browser_policy_container_not_stored.js"]
+
["browser_privatetabs.js"]
["browser_purge_domaindata.js"]
["browser_purge_shistory.js"]
diff --git a/browser/components/sessionstore/test/browser_policy_container_not_stored.js b/browser/components/sessionstore/test/browser_policy_container_not_stored.js
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_policy_container_not_stored.js
@@ -0,0 +1,51 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * A session history entry only needs to retain the policy container for loads
+ * that inherit their policies (about:blank, about:srcdoc, blob:, data:, ...).
+ * Anything fetched over the network gets its policies from the response again,
+ * so storing them would only bloat the session store (bug 2011236).
+ */
+
+const TEST_ROOT = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "https://example.com"
+);
+const PARENT_URL = TEST_ROOT + "browser_policy_container_sample.html";
+const FRAME_URL = TEST_ROOT + "browser_policy_container_sample_frame.html";
+
+add_task(async function test_policy_container_only_for_inheriting_loads() {
+ const tab = BrowserTestUtils.addTab(gBrowser, PARENT_URL);
+ gBrowser.selectedTab = tab;
+ await promiseBrowserLoaded(tab.linkedBrowser, true, PARENT_URL);
+ await TabStateFlusher.flush(tab.linkedBrowser);
+
+ const state = JSON.parse(ss.getTabState(tab));
+ const topEntry = state.entries.at(-1);
+ is(topEntry.url, PARENT_URL, "collected the parent entry");
+
+ const children = topEntry.children ?? [];
+ const networkFrame = children.find(child => child.url === FRAME_URL);
+ const blankFrame = children.find(child => child.url === "about:blank");
+
+ ok(networkFrame, "collected the network-scheme subframe entry");
+ ok(blankFrame, "collected the about:blank subframe entry");
+
+ // The parent has a CSP, so both subframes inherit a policy container onto
+ // their load state. Only the one that cannot recover it from a response
+ // should keep it in session history.
+ ok(
+ !("policyContainer" in networkFrame),
+ "https subframe entry does not store a policyContainer"
+ );
+ ok(
+ "policyContainer" in blankFrame,
+ "about:blank subframe entry still stores its inherited policyContainer"
+ );
+
+ await BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/sessionstore/test/browser_policy_container_sample.html b/browser/components/sessionstore/test/browser_policy_container_sample.html
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_policy_container_sample.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="default-src * data: blob: 'unsafe-inline'">
+ <title>policyContainer session store sample</title>
+</head>
+<body>
+ <iframe id="network" src="browser_policy_container_sample_frame.html"></iframe>
+ <iframe id="blank" src="about:blank"></iframe>
+</body>
+</html>
diff --git a/browser/components/sessionstore/test/browser_policy_container_sample_frame.html b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_policy_container_sample_frame.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>policyContainer session store sample frame</title>
+</head>
+<body>
+ frame
+</body>
+</html>
diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp
--- a/docshell/shistory/SessionHistoryEntry.cpp
+++ b/docshell/shistory/SessionHistoryEntry.cpp
@@ -41,10 +41,18 @@
extern mozilla::LazyLogModule gSHLog;
namespace mozilla {
namespace dom {
+// Only store policy container for loads that can't carry it themselves
+// (about:blank, about:srcdoc, blob:, data:, ...)
+// bug 1867137, bug 2011236
+static nsIPolicyContainer* PolicyContainerToStore(
+ nsIURI* aURI, nsIPolicyContainer* aPolicyContainer) {
+ return CSP_ShouldURIInheritCSP(aURI) ? aPolicyContainer : nullptr;
+}
+
SessionHistoryInfo::SessionHistoryInfo(nsDocShellLoadState* aLoadState,
nsIChannel* aChannel)
: mURI(aLoadState->URI()),
mOriginalURI(aLoadState->OriginalURI()),
mResultPrincipalURI(aLoadState->ResultPrincipalURI()),
@@ -59,9 +67,10 @@
mLoadReplace(aLoadState->LoadReplace()),
mHasUserActivation(aLoadState->HasValidUserGestureActivation()),
mSharedState(SharedState::Create(
aLoadState->TriggeringPrincipal(), aLoadState->PrincipalToInherit(),
aLoadState->PartitionedPrincipalToInherit(),
- aLoadState->PolicyContainer(),
+ PolicyContainerToStore(aLoadState->URI(),
+ aLoadState->PolicyContainer()),
/* FIXME Is this correct? */
aLoadState->TypeHint())) {
// Pull the upload stream off of the channel instead of the load state, as
@@ -124,11 +133,12 @@
loadInfo->GetPrincipalToInherit(
getter_AddRefs(mSharedState.Get()->mPrincipalToInherit));
mSharedState.Get()->mPartitionedPrincipalToInherit =
aPartitionedPrincipalToInherit;
- mSharedState.Get()->mPolicyContainer = aPolicyContainer;
+ mSharedState.Get()->mPolicyContainer =
+ PolicyContainerToStore(mURI, aPolicyContainer);
aChannel->GetContentType(mSharedState.Get()->mContentType);
aChannel->GetOriginalURI(getter_AddRefs(mOriginalURI));
uint32_t loadFlags;
aChannel->GetLoadFlags(&loadFlags);

View File

@@ -0,0 +1,55 @@
diff --git a/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs b/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
--- a/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
+++ b/toolkit/components/pictureinpicture/PictureInPicture.sys.mjs
@@ -119,10 +119,19 @@
}
case "PictureInPicture:VideoTabHidden": {
if (!lazy.PIP_ENABLED || !lazy.PIP_WHEN_SWITCHING_TABS) {
break;
}
+
+ // Suppress auto-toggle if the user deliberately closed this PiP window
+ if (
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.has(browser)
+ ) {
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.delete(browser);
+ break;
+ }
+
// If the tab is still selected, then we can ignore this event
if (browser.documentGlobal.gBrowser.selectedBrowser == browser) {
break;
}
let actor = browsingContext.currentWindowGlobal.getActor(
@@ -261,10 +270,12 @@
maxConcurrentPlayerCount: 0,
// Maps auto pip browser to PictureInPictureParent actor
weakAutoPipBrowserToParent: new WeakMap(),
+ weakAutoPipBrowserClosedDeliberately: new WeakSet(),
+
/**
* Returns the player window if one exists and if it hasn't yet been closed.
*
* @param {PictureInPictureParent} pipActorRef
* Reference to the calling PictureInPictureParent actor
@@ -899,10 +910,17 @@
const { reason, actorRef } = closeData;
const win = this.getWeakPipPlayer(actorRef);
if (!win) {
return;
}
+
+ const browser = this.weakWinToBrowser.get(win);
+
+ if (reason === "CloseButton") {
+ PictureInPicture.weakAutoPipBrowserClosedDeliberately.add(browser);
+ }
+
this.removePiPBrowserFromWeakMap(this.weakWinToBrowser.get(win));
this.weakAutoPipBrowserToParent.delete(this.weakWinToBrowser.get(win));
Glean.pictureinpicture["closedMethod" + reason].record();
await this.closePipWindow(win);

View File

@@ -7,14 +7,6 @@
"id": "D299584",
"name": "Native MacOS popovers fix"
},
{
"type": "phabricator",
"id": "D284404",
"name": "Add urlbar closeOnWindowBlur preference",
"replaces": {
"\n\n": "\n // may want to figure out a more robust way to detect abandonment."
}
},
{
"type": "local",
// TODO: Convert into https://phabricator.services.mozilla.com/D298079
@@ -48,5 +40,21 @@
"this._lastJsonLength = uncompressedBytes;": "this._lastJsonLength = uncompressedBytes.jsonLength;",
" jsonLengthHint,": " lengthHint: jsonLengthHint,"
}
},
{
"type": "phabricator",
"id": "D320897",
"name": "Bug 2011236",
"replaces": {
// The upstream patch context includes a MOZ_DIAGNOSTIC_ASSERT that
// doesn't exist yet in our Firefox version.
"@@ -59,11 +67,12 @@": "@@ -59,9 +67,10 @@",
"aLoadState->TypeHint())) {\n MOZ_DIAGNOSTIC_ASSERT(!mURI->SchemeIs(\"javascript\"));\n \n // Pull": "aLoadState->TypeHint())) {\n // Pull"
}
},
{
"type": "phabricator",
"id": "D321446",
"name": "Issue 14710"
}
]

View File

@@ -1,5 +1,5 @@
diff --git a/services/sync/modules/service.sys.mjs b/services/sync/modules/service.sys.mjs
index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f06a4f8dbf 100644
index 4f4efe572909441abdd81909282936702c225621..2b47a2c9e97cf87641a0be9814f9a6be4d43f6ea 100644
--- a/services/sync/modules/service.sys.mjs
+++ b/services/sync/modules/service.sys.mjs
@@ -99,6 +99,10 @@ function getEngineModules() {
@@ -7,8 +7,8 @@ index 4f4efe572909441abdd81909282936702c225621..81ab06a9414ed656b644a21b40b683f0
whenFalse: "ExtensionStorageEngineBridge",
};
+ result.Spaces = {
+ module: "resource:///modules/zen/ZenWorkspacesSync.sys.mjs",
+ symbol: "ZenWorkspacesEngine",
+ module: "resource:///modules/zen/ZenSpacesSync.sys.mjs",
+ symbol: "ZenSpacesSyncEngine",
+ };
return result;
}

View File

@@ -0,0 +1,14 @@
diff --git a/testing/xpcshell/runxpcshelltests.py b/testing/xpcshell/runxpcshelltests.py
index 403401613189ffb758037850dc6bbb1d55f194ab..cad28e245155eb54775e551885532026e56c5fa9 100755
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -2275,6 +2275,9 @@ class XPCShellTests:
appDirKey = None
if "appname" in self.mozInfo:
appDirKey = self.mozInfo["appname"] + "-appdir"
+ if appDirKey == "zen-appdir":
+ # Inherited manifests use firefox-appdir.
+ appDirKey = "firefox-appdir"
# We have to do this before we run tests that depend on having the node
# http/2 server.

View File

@@ -1,37 +0,0 @@
diff --git a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
index 5702ff28cc22206f5ce16584dac8a78d816562ce..3d08ecc97ce5995b30d8a4af0c33df329b428008 100644
--- a/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
+++ b/toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs
@@ -270,11 +270,29 @@ _ContextualIdentityService.prototype = {
});
},
- create(name, icon, color) {
+ create(name, icon, color, id = null) {
this.ensureDataReady();
- // Retrieve the next userContextId available.
- let userContextId = ++this._lastUserContextId;
+ if (id !== null) {
+ id = typeof id == "string" ? Number(id) : id;
+ if (!Number.isSafeInteger(id) || id <= 0) {
+ throw new Error(`Invalid userContextId '${id}'`);
+ }
+ }
+
+ let userContextId;
+ if (
+ id !== null &&
+ !this._identities.some(identity => identity.userContextId === id)
+ ) {
+ userContextId = id;
+ this._lastUserContextId = Math.max(
+ this._lastUserContextId,
+ userContextId
+ );
+ } else {
+ userContextId = ++this._lastUserContextId;
+ }
// Throw an error if the next userContextId available is invalid (the one associated to
// MAX_USER_CONTEXT_ID is already reserved to "userContextIdInternal.webextStorageLocal", which

View File

@@ -0,0 +1,29 @@
diff --git a/toolkit/content/widgets/panel-list/panel-list.mjs b/toolkit/content/widgets/panel-list/panel-list.mjs
index 68ad2fafc8b31dfff541cf8381d9b9533e376d1c..fa62fec6bfb33338091fc06d3736fb76fd5c47c4 100644
--- a/toolkit/content/widgets/panel-list/panel-list.mjs
+++ b/toolkit/content/widgets/panel-list/panel-list.mjs
@@ -223,7 +223,12 @@ export class PanelList extends HTMLElement {
this.setAttribute("showing", "true");
// Tell the host element to hide any overflow in case the panel extends off
// the page before the alignment is set.
- hostElement.style.overflow = "hidden";
+ const hostHidesOverflow = !getComputedStyle(hostElement)
+ .overflow.split(" ")
+ .includes("visible");
+ if (!hostHidesOverflow) {
+ hostElement.style.overflow = "hidden";
+ }
// Wait for a layout flush, then find the bounds.
let {
@@ -344,7 +349,9 @@ export class PanelList extends HTMLElement {
// Set the alignments and show the panel.
this.setAttribute("align", align);
this.setAttribute("valign", valign);
- hostElement.style.overflow = "";
+ if (!hostHidesOverflow) {
+ hostElement.style.overflow = "";
+ }
// Decide positioning based on where this panel will be rendered
const offsetParentIsBody =
this.supportsPopover() ||

View File

@@ -0,0 +1,29 @@
diff --git a/widget/gtk/nsDragService.cpp b/widget/gtk/nsDragService.cpp
index f760a6bb887b321376761431886c771f14fd03af..d52110ba52a95ef7c7bfb915e120b8dd636c9448 100644
--- a/widget/gtk/nsDragService.cpp
+++ b/widget/gtk/nsDragService.cpp
@@ -23,6 +23,7 @@
#include "mozilla/StaticPrefs_widget.h"
#include "mozilla/WidgetUtils.h"
#include "mozilla/WidgetUtilsGtk.h"
+#include "mozilla/nsZenDragAndDrop.h"
#include "nsAppShell.h"
#include "nsArrayUtils.h"
#include "nsCRT.h"
@@ -2304,11 +2305,15 @@ bool nsDragSession::SetAlphaPixmap(SourceSurface* aSurface,
cairo_image_surface_get_stride(surf), SurfaceFormat::B8G8R8A8);
if (!dt) return false;
+ auto drag_translucency = DRAG_IMAGE_ALPHA_LEVEL;
+ if (auto zenDragAndDrop = zen::nsZenDragAndDrop::GetZenDragAndDropInstance()) {
+ drag_translucency = zenDragAndDrop->GetDragImageOpacity();
+ }
dt->ClearRect(Rect(0, 0, dragRect.width, dragRect.height));
dt->DrawSurface(
aSurface, Rect(0, 0, dragRect.width, dragRect.height),
Rect(0, 0, dragRect.width, dragRect.height), DrawSurfaceOptions(),
- DrawOptions(DRAG_IMAGE_ALPHA_LEVEL, CompositionOp::OP_SOURCE));
+ DrawOptions(drag_translucency, CompositionOp::OP_SOURCE));
cairo_surface_mark_dirty(surf);
cairo_surface_set_device_offset(surf, -aXOffset, -aYOffset);

View File

@@ -28,6 +28,7 @@
"chrome://browser/content/zen-components/ZenEmojiPicker.mjs",
"chrome://browser/content/zen-components/ZenLiveFoldersUI.mjs",
"chrome://browser/content/zen-components/ZenDownloadAnimation.mjs",
"moz-src:///zen/library/ZenLibraryButton.mjs",
];
for (let script of scripts) {

View File

@@ -4,6 +4,7 @@
import checkForZenUpdates, {
createWindowUpdateAnimation,
playWindowSweepAnimation,
} from "chrome://browser/content/ZenUpdates.mjs";
class ZenStartup {
@@ -195,6 +196,14 @@ class ZenStartup {
checkForZenUpdates();
return await createWindowUpdateAnimation();
}
/**
* Entry point for privileged modules (e.g. the sync applier) to play the
* update sweep animation in this window.
*/
playWindowSweepAnimation() {
return playWindowSweepAnimation();
}
}
window.gZenStartup = new ZenStartup();

View File

@@ -493,6 +493,13 @@ window.gZenUIManager = {
_clearTimeout: null,
_lastTab: null,
_urlbarSessionCounter: 0,
_urlbarOwner: null,
_isOwner(closeSeq) {
return this._urlbarOwner === closeSeq;
},
// Check if browser elements are in a valid state for tab operations
_validateBrowserState() {
// Check if browser window is still open
@@ -517,8 +524,15 @@ window.gZenUIManager = {
werePassedURL,
searchClipboard,
where,
overridePreferance = false
overridePreferance = false,
closeToken = {}
) {
// Keep this here. It's better to make sure every
// call has a different token rather then forgetting
// to increment it in one of the early returns.
const closeSeq = ++this._urlbarSessionCounter;
closeToken.id = closeSeq;
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn("Browser state invalid for new tab operation");
@@ -542,7 +556,13 @@ window.gZenUIManager = {
// Close the new tab popup on cmd/ctrl + t
if (!overridePreferance && gURLBar.hasAttribute("zen-newtab")) {
this.handleUrlbarClose();
// The _zenHandleUrlbarClose holds a token in it's closure.
// Call that instead.
if (gURLBar._zenHandleUrlbarClose) {
gURLBar._zenHandleUrlbarClose();
} else {
this.handleUrlbarClose(closeSeq);
}
return true;
}
@@ -568,7 +588,9 @@ window.gZenUIManager = {
this._prevUrlbarLabel = gURLBar._untrimmedValue || "";
// Set up URL bar for new tab
gURLBar._zenHandleUrlbarClose = this.handleUrlbarClose.bind(this);
gURLBar._zenHandleUrlbarClose = (onSwitch, onElementPicked) =>
this.handleUrlbarClose(closeSeq, onSwitch, onElementPicked);
this._urlbarOwner = closeSeq;
gURLBar.setAttribute("zen-newtab", true);
// Update newtab buttons
@@ -582,18 +604,33 @@ window.gZenUIManager = {
document.getElementById("Browser:OpenLocation").doCommand();
} catch (e) {
console.error("Error opening location in new tab:", e);
this.handleUrlbarClose(false);
this.handleUrlbarClose(closeSeq, false, false);
return false;
}
return true;
},
/**
* Check whether the closeToken given by handleNewTab
* corresponds to a close event.
*
* @param {object} closeToken - Token previously passed to handleNewTab.
* @param {CustomEvent} event - A ZenURLBarClosed event.
* @returns {boolean}
*/
matchesCloseToken(closeToken, event) {
return (
typeof closeToken?.id === "number" &&
event?.detail?.closeSeq === closeToken.id
);
},
clearUrlbarData() {
this._prevUrlbarLabel = null;
this._lastSearch = "";
},
handleUrlbarClose(onSwitch = false, onElementPicked = false) {
handleUrlbarClose(closeSeq, onSwitch = false, onElementPicked = false) {
// Validate browser state first
if (!this._validateBrowserState()) {
console.warn("Browser state invalid for URL bar close operation");
@@ -607,59 +644,69 @@ window.gZenUIManager = {
const isFocusedBefore = gURLBar.focused;
setTimeout(() => {
// We use this attribute on Tabbrowser::addTab
gURLBar.removeAttribute("zen-newtab");
/* If someone else opened the url bar in the meantime don't be
* stingy and leave it open for them.*/
if (this._isOwner(closeSeq)) {
// We use this attribute on Tabbrowser::addTab
gURLBar.removeAttribute("zen-newtab");
// Safely restore tab visual state with proper validation
if (
this._lastTab &&
!this._lastTab.closing &&
this._lastTab.documentGlobal &&
!this._lastTab.documentGlobal.closed &&
gBrowser.selectedTab === this._lastTab
) {
this._lastTab._visuallySelected = true;
this._lastTab = null;
}
// Reset newtab buttons
for (const button of this.newtabButtons) {
button.removeAttribute("in-urlbar");
}
// Handle search data
if (onSwitch) {
this.clearUrlbarData();
} else {
this._lastSearch = gURLBar._untrimmedValue || "";
if (this._clearTimeout) {
clearTimeout(this._clearTimeout);
// Safely restore tab visual state with proper validation
if (
this._lastTab &&
!this._lastTab.closing &&
this._lastTab.documentGlobal &&
!this._lastTab.documentGlobal.closed &&
gBrowser.selectedTab === this._lastTab
) {
this._lastTab._visuallySelected = true;
this._lastTab = null;
}
this._clearTimeout = setTimeout(() => {
// Reset newtab buttons
for (const button of this.newtabButtons) {
button.removeAttribute("in-urlbar");
}
// Handle search data
if (onSwitch) {
this.clearUrlbarData();
}, this.urlbarWaitToClear);
}
} else {
this._lastSearch = gURLBar._untrimmedValue || "";
// Safely restore URL bar state with proper validation
if (this._prevUrlbarLabel) {
gURLBar.setURI({
uri: this._prevUrlbarLabel,
dueToTabSwitch: onSwitch,
isSameDocument: !onSwitch,
});
}
if (this._clearTimeout) {
clearTimeout(this._clearTimeout);
}
gURLBar.handleRevert();
this._clearTimeout = setTimeout(() => {
this.clearUrlbarData();
}, this.urlbarWaitToClear);
}
// Safely restore URL bar state with proper validation
if (this._prevUrlbarLabel) {
gURLBar.setURI({
uri: this._prevUrlbarLabel,
dueToTabSwitch: onSwitch,
isSameDocument: !onSwitch,
});
}
gURLBar.handleRevert();
}
if (isFocusedBefore) {
setTimeout(() => {
window.dispatchEvent(
new CustomEvent("ZenURLBarClosed", {
detail: { onSwitch, onElementPicked },
detail: { onSwitch, onElementPicked, closeSeq },
})
);
if (!this._isOwner(closeSeq)) {
return;
}
this._urlbarOwner = null;
gURLBar.view.close({ elementPicked: onElementPicked });
gURLBar.updateTextOverflow();
@@ -683,6 +730,8 @@ window.gZenUIManager = {
}
}
}, 0);
} else if (this._isOwner(closeSeq)) {
this._urlbarOwner = null;
}
}, 0);
},

View File

@@ -61,6 +61,14 @@ export async function createWindowUpdateAnimation() {
return;
}
Services.prefs.setStringPref(ZEN_BUILD_ID_PREF, appID);
await playWindowSweepAnimation();
}
/**
* Plays the full-window sweep shown after updates. Also used the first
* time incoming sync data is applied on this profile.
*/
export async function playWindowSweepAnimation() {
await gZenWorkspaces.promiseInitialized;
const appWrapper = document.getElementById("zen-main-app-wrapper");
const element = document.createElement("div");

View File

@@ -78,13 +78,13 @@
}
.zen-sidebar-notification-progress {
height: 2px;
height: 1px;
overflow: hidden;
transform: translateY(-1px);
& .zen-sidebar-notification-progress-bar {
height: 100%;
background: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
background: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, transparent 10%, #6b6b6b));
transform-origin: left center;
}
}

View File

@@ -795,3 +795,162 @@
display: none;
}
}
#zen-library-button {
position: relative;
}
#zen-library-button::after {
content: "";
position: absolute;
top: 1px;
right: 1px;
width: 12px;
height: 12px;
border-radius: 50%;
background:
conic-gradient(
var(--zen-colors-primary, currentColor)
calc(var(--zen-library-button-progress, 0) * 360deg),
color-mix(in srgb, currentColor 25%, transparent) 0
);
mask: radial-gradient(circle, transparent 3.5px, black 4px);
pointer-events: none;
opacity: 0;
transform: scale(0.6);
transition:
opacity 0.18s ease,
transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
}
#zen-library-button[downloading]::after {
opacity: 1;
transform: scale(1);
}
@keyframes zen-library-button-spin {
to {
transform: rotate(360deg);
}
}
#zen-library-button[downloading-indeterminate]::after {
background: conic-gradient(
var(--zen-colors-primary, currentColor) 0deg 90deg,
color-mix(in srgb, currentColor 20%, transparent) 90deg 360deg
);
animation: zen-library-button-spin 1.05s linear infinite;
}
#zen-library-button[downloading]::before {
content: "";
position: absolute;
top: 5px;
right: 5px;
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--zen-colors-primary, currentColor);
pointer-events: none;
}
/*
* Stack of recent download tiles inserted just before
* #zen-sidebar-foot-buttons. Visible only while hovering the library button
* (or the stack itself). Anchored to the bottom of #navigator-toolbox so it
* floats above the tab strip without pushing layout.
*/
#navigator-toolbox {
position: relative;
}
.zen-library-button-panel {
position: absolute;
left: 0;
right: 0;
bottom: var(--zen-library-stack-bottom-offset, 42px);
z-index: 2;
padding: 6px 8px;
opacity: 0;
transform: translateY(8px);
pointer-events: none;
}
.zen-library-button-panel[data-state="open"] {
pointer-events: auto;
}
/* Fade the bottom of the tab strip so it doesn't visually collide with
* the floating download stack. The fade region scales with the actual
* stack height (published from JS as --zen-library-stack-height). */
#tabbrowser-tabs[zen-library-stack-open="true"] {
mask-image: linear-gradient(to bottom, black var(--zen-library-stack-height), black 50%, transparent calc(100% - var(--zen-library-stack-height) + 20px));
transition: mask-image 0.18s ease;
}
.zen-library-button-panel-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.zen-library-button-panel-empty {
padding: 12px 10px;
text-align: center;
font-size: 12px;
opacity: 0.6;
}
.zen-library-button-panel-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 8px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.12s ease;
}
.zen-library-button-panel-row:hover {
background: color-mix(in srgb, currentColor 10%, transparent);
}
.zen-library-button-panel-icon {
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 6px;
}
.zen-library-button-panel-labels {
display: flex;
flex: 1;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.zen-library-button-panel-name {
margin: 0;
font-size: 12.5px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
}
.zen-library-button-panel-row[data-file-deleted] .zen-library-button-panel-name {
text-decoration: line-through;
opacity: 0.6;
}
.zen-library-button-panel-status {
margin: 0;
font-size: 10.5px;
opacity: 0.65;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
pointer-events: none;
}

View File

@@ -92,7 +92,11 @@
--in-content-accent-color-active: currentColor !important;
/* This is like the secondary button */
--in-content-button-background: transparent !important;
--in-content-button-background: color-mix(
in srgb,
currentColor,
transparent 90%
) !important;
--in-content-button-text-color: var(--zen-secondary-btn-color) !important;
--in-content-button-background-hover: color-mix(
in srgb,

View File

@@ -9,7 +9,7 @@ export const ZenCustomizableUI = new (class {
TYPE_TOOLBAR = "toolbar";
defaultSidebarIcons = [
"downloads-button",
Services.prefs.getBoolPref("zen.library.enabled") ? "zen-library-button" : "downloads-button",
"zen-workspaces-button",
"zen-create-new-button",
];

View File

@@ -7,11 +7,14 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
CustomizableUI: "moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
});
class nsZenUIMigration {
PREF_NAME = "zen.ui.migration.version";
PREF_LIBRARY_REPLACED_DOWNLOADS =
"zen.ui.migration.library-button-replaced-downloads";
MIGRATION_VERSION = 7;
init(isNewProfile) {
@@ -22,6 +25,14 @@ class nsZenUIMigration {
console.error("ZenUIMigration: Error during migration", e);
}
}
try {
this.#maybeReplaceDownloadsWithLibrary(isNewProfile);
} catch (e) {
console.error(
"ZenUIMigration: Error replacing downloads with library button",
e
);
}
this.clearVariables();
if (this.shouldRestart) {
Services.startup.quit(
@@ -50,6 +61,28 @@ class nsZenUIMigration {
this._migrationVersion = this.MIGRATION_VERSION;
}
#maybeReplaceDownloadsWithLibrary(isNewProfile) {
if (
Services.prefs.getBoolPref(this.PREF_LIBRARY_REPLACED_DOWNLOADS, false)
) {
return;
}
if (!isNewProfile) {
const placement =
lazy.CustomizableUI.getPlacementOfWidget("downloads-button");
if (placement?.area === "zen-sidebar-foot-buttons") {
const { position } = placement;
lazy.CustomizableUI.removeWidgetFromArea("downloads-button");
lazy.CustomizableUI.addWidgetToArea(
"zen-library-button",
"zen-sidebar-foot-buttons",
position
);
}
}
Services.prefs.setBoolPref(this.PREF_LIBRARY_REPLACED_DOWNLOADS, true);
}
_migrateV1() {
// If there's an userChrome.css or userContent.css existing, we set
// 'toolkit.legacyUserProfileCustomizations.stylesheets' back to true

View File

@@ -138,6 +138,14 @@ document.addEventListener(
gZenWorkspaces.unloadAllOtherWorkspaces();
break;
}
case "cmd_zenToggleLibrary": {
const { ZenLibrary } = ChromeUtils.importESModule(
"moz-src:///zen/library/ZenLibrary.mjs",
{ global: "current" }
);
ZenLibrary.toggle();
break;
}
case "cmd_zenOpenSpaceRoutingSettings": {
gZenSpaceRoutingManager.openSpaceRoutingDialog(window);
break;

View File

@@ -832,8 +832,9 @@ window.gZenCompactModeManager = {
const onLeave = event => {
if (AppConstants.platform == "macosx") {
const buttonRect =
gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect();
const buttonRect = window.windowUtils.getBoundsWithoutFlushing(
gZenVerticalTabsManager.actualWindowButtons
);
const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y;
if (

View File

@@ -147,10 +147,20 @@ class nsZenDownloadAnimationElement extends HTMLElement {
return Services.prefs.getBoolPref("zen.tabs.vertical.right-side");
}
get #downloadButton() {
const ids = ["zen-library-button", "downloads-button"];
for (const id of ids) {
const button = document.getElementById(id);
if (button && this.#isElementVisible(button)) {
return button;
}
}
return null;
}
#determineEndPosition() {
const downloadsButton = document.getElementById("downloads-button");
const isDownloadButtonVisible =
downloadsButton && this.#isElementVisible(downloadsButton);
const downloadsButton = this.#downloadButton;
const isDownloadButtonVisible = downloadsButton !== null;
let endPosition = { clientX: 0, clientY: 0 };

View File

@@ -183,10 +183,10 @@
if (tabClone.hasAttribute("visuallyselected")) {
tabClone.style.transform = "translate(-50%, -50%)";
}
} else if (AppConstants.platform !== "macosx" && !tab.isZenFolder) {
// On windows and linux, we still don't add some extra opaqueness
// for the tab to be more visible. This is a hacky workaround.
// TODO: Make windows and linux DnD use nsZenDragAndDrop::mDragImageOpacity
} else if (AppConstants.platform == "win" && !tab.isZenFolder) {
// On windows, the shell applies its own translucency to the drag
// image which we can't control, so force some extra contrast for
// the tab to be more visible. This is a hacky workaround.
tabClone.style.colorScheme = "light";
tabClone.style.setProperty("--tab-selected-textcolor", "black");
}

View File

@@ -0,0 +1,580 @@
// 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";
let lazy = {};
let gZenLibraryInstance = null;
const PREVIOUS_TAB_PREF = "zen.library.previous-tab";
const DONATE_URL = "https://zen-browser.app/donate/";
ChromeUtils.defineESModuleGetters(
lazy,
{
ZenLibrarySections: "moz-src:///zen/library/ZenLibrarySections.mjs",
},
{ global: "current" }
);
ChromeUtils.defineLazyGetter(lazy, "l10n", function () {
return new Localization(["browser/zen-library.ftl"], true);
});
ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
return document.getElementById("zen-appcontent-wrapper");
});
/**
* The ZenLibrary class is responsible for managing the UI for the library feature.
* This feature allows users to view and manage their browsing history, downloads,
* spaces, and other related data in a unified interface.
*/
export class ZenLibrary extends MozLitElement {
static #ANIMATION_DURATION = 280;
static #ANIMATION_EASING = "cubic-bezier(0.32, 0.72, 0, 1)";
static #TOOLBOX_OPEN_TRANSFORM = "scale(0.96)";
static #TOOLBOX_OPEN_OPACITY = "-0.5";
#initialized = false;
#resizeObserver = null;
#sections = [];
#activeAnimations = new Set();
#animating = false;
_deletionIdleCallbackId = null;
static properties = {
_activeTab: { type: String },
};
static queries = {
_content: "#zen-library-content",
_tabs: { all: "#zen-library-sidebar-tabs > .library-tab" },
_header: "#zen-library-sidebar-header",
_footer: "#zen-library-sidebar-footer",
};
createRenderRoot() {
return this;
}
constructor() {
super();
this.activeTab = Services.prefs.getStringPref(PREVIOUS_TAB_PREF, "") || "history";
}
set activeTab(value) {
if (this.activeTab === value) {
return;
}
this._activeTab = value;
Services.prefs.setStringPref(PREVIOUS_TAB_PREF, value);
}
get activeTab() {
return this._activeTab;
}
connectedCallback() {
super.connectedCallback();
if (this.#initialized) {
return;
}
window.addEventListener("keydown", this);
this.addEventListener("animationend", this);
// Add connected call back and make `appContentWrapper` transform translate the oposite of this element
this.#resizeObserver = new ResizeObserver(() => {
if (gZenWorkspaces._swipeManager._swipeState.librarySwiping) {
return;
}
let translateX = this.#computeWrapperTargetPx();
lazy.appContentWrapper.style.transform = `translateX(${translateX}px)`;
});
this.#resizeObserver.observe(this);
for (const Section of Object.values(lazy.ZenLibrarySections)) {
let section = new Section();
this.#sections.push(section);
}
this.#initialized = true;
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("keydown", this);
for (const section of this.#sections) {
section.remove();
}
this.#sections = [];
if (this.#resizeObserver) {
this.#resizeObserver.disconnect();
this.#resizeObserver = null;
}
this.#initialized = false;
}
firstUpdated() {
super.firstUpdated?.();
this._header.appendChild(
gZenVerticalTabsManager.actualWindowButtons.cloneNode(true)
);
this.#buildFooterButtons();
const activeTabEl = this.querySelector(".zen-library-tab[active]");
if (activeTabEl) {
this.#animateTabIcon(activeTabEl);
}
}
#buildFooterButtons() {
const buttons = [
{
l10nId: "library-close-button",
image: "chrome://browser/skin/zen-icons/back.svg",
command: () => ZenLibrary.toggle(),
},
{
l10nId: "library-donate-button",
image: "chrome://browser/skin/zen-icons/heart-circle-fill.svg",
command: () => {
window.openTrustedLinkIn(DONATE_URL, "tab");
ZenLibrary.toggle();
},
},
];
for (const { l10nId, image, command } of buttons) {
const button = document.createXULElement("toolbarbutton");
button.className = "toolbarbutton-1";
button.setAttribute("image", image);
document.l10n.setAttributes(button, l10nId);
button.addEventListener("command", command);
this._footer.appendChild(button);
}
}
/**
* Plays a tab's icon sprite animation, reload-to-stop style: the [animate]
* attribute starts the strip's steps() animation (see zen-library.css) and
* the animationend handler removes it again. Restarts if mid-animation.
*
* @param {Element} tab
*/
#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"
/>
<vbox id="zen-library-sidebar">
<vbox id="zen-library-sidebar-header"></vbox>
<vbox id="zen-library-sidebar-tabs">
${Object.values(lazy.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>${lazy.l10n.formatValueSync(Section.label)}</label>
</vbox>
`
)}
</vbox>
<toolbar mode="icons"
fullscreentoolbar="true"
class="browser-toolbar chromeclass-location"
id="zen-library-sidebar-footer">
</toolbar>
</vbox>
<vbox
id="zen-library-content"
flex="1"
?large-content=${lazy.ZenLibrarySections[this.activeTab].largeContent}
>
${this.#sections.find(section => section.constructor.id === this.activeTab)}
</vbox>
`;
}
handleEvent(event) {
switch (event.type) {
case "keydown": {
if (event.key === "Escape" && this.isOpen) {
ZenLibrary.toggle();
}
break;
}
case "animationend": {
if (event.animationName === "zen-library-tab-icon-play") {
event.target.closest(".zen-library-tab")?.removeAttribute("animate");
}
break;
}
}
}
get isOpen() {
return this.hasAttribute("open");
}
/** True when a live library instance exists and is in the open state. */
static get isOpen() {
return gZenLibraryInstance?.isOpen ?? false;
}
static getInstance() {
if (!gZenLibraryInstance) {
gZenLibraryInstance = new ZenLibrary();
gNavToolbox.before(gZenLibraryInstance);
}
return gZenLibraryInstance;
}
static clearInstance() {
if (gZenLibraryInstance) {
gZenLibraryInstance._deletionIdleCallbackId = null;
gZenLibraryInstance.remove();
gZenLibraryInstance = null;
}
}
static toggle() {
if (gZenLibraryInstance?.#animating) {
return;
}
window.docShell.treeOwner
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIAppWindow)
.rollupAllPopups();
let instance = this.getInstance();
instance.toggleAttribute("open");
if (instance.isOpen) {
if (instance._deletionIdleCallbackId) {
cancelIdleCallback(instance._deletionIdleCallbackId);
instance._deletionIdleCallbackId = null;
}
gNavToolbox.setAttribute("zen-library-open", "true");
instance.#animateOpen();
} else {
gNavToolbox.removeAttribute("zen-library-open");
// #animateClose schedules the instance disposal itself once the slide-
// out finishes, so we don't queue an idle callback eagerly here — that
// would otherwise race the animation and yank the element out of the
// DOM mid-slide.
instance.#animateClose();
}
}
#cancelActiveAnimations() {
for (const anim of this.#activeAnimations) {
try {
anim.cancel();
} catch {
/* already settled */
}
}
this.#activeAnimations.clear();
}
#animate(element, keyframes) {
const anim = element.animate(keyframes, {
duration: ZenLibrary.#ANIMATION_DURATION,
easing: ZenLibrary.#ANIMATION_EASING,
fill: "forwards",
});
this.#activeAnimations.add(anim);
anim.finished.then(
() => this.#activeAnimations.delete(anim),
() => this.#activeAnimations.delete(anim)
);
return anim;
}
/**
* Mirrors the ResizeObserver's math but returns the translateX (px) instead
* of writing it inline. Used when the observer hasn't fired yet (re-opens
* on the same instance) or when we need a target without side effects.
*/
#computeWrapperTargetPx() {
const isRightSide = gZenVerticalTabsManager._prefsRightSide;
let translateX = this.getBoundingClientRect()[
isRightSide ? "left" : "right"
];
const contentPosition = window.windowUtils.getBoundsWithoutFlushing(
lazy.appContentWrapper
)[isRightSide ? "right" : "left"];
const existingTransform = new DOMMatrix(
lazy.appContentWrapper.style.transform
).m41;
translateX = translateX - contentPosition + existingTransform;
return isRightSide ? -translateX : translateX;
}
async #animateOpen() {
this.#animating = true;
try {
this.#cancelActiveAnimations();
await new Promise(r => requestAnimationFrame(r));
if (!this.isOpen || !this.isConnected) {
return;
}
// Re-opens on the same instance won't trigger the observer (library
// size is unchanged), so fall back to computing the target ourselves.
let wrapperTarget = lazy.appContentWrapper.style.transform;
if (!wrapperTarget) {
wrapperTarget = `translateX(${this.#computeWrapperTargetPx()}px)`;
}
lazy.appContentWrapper.style.transform = "";
this.#animate(this, [
{ transform: "translateX(-100%)", opacity: 0 },
{ transform: "translateX(0)", opacity: 1 },
]);
this.#animate(gNavToolbox, [
{ transform: "scale(1)", opacity: 1 },
{
transform: ZenLibrary.#TOOLBOX_OPEN_TRANSFORM,
opacity: ZenLibrary.#TOOLBOX_OPEN_OPACITY,
},
]);
const wrapperAnim = this.#animate(lazy.appContentWrapper, [
{ transform: "translateX(0)" },
{ transform: wrapperTarget },
]);
try {
await wrapperAnim.finished;
} catch {
return;
}
if (!this.isOpen) {
return;
}
// Persist the final transform inline so the resize observer's
// diff math keeps working after the animation ends.
wrapperAnim.commitStyles();
wrapperAnim.cancel();
this.#activeAnimations.delete(wrapperAnim);
} finally {
this.#animating = false;
}
}
/**
* Prepare the library for swipe-driven state changes. Works whether the
* library is currently open (close swipe) or closed (open swipe). After
* this resolves, callers drive `updateSwipeProgress(0..1)` directly until
* `finishSwipe(targetOpen)` commits or reverts.
*/
static async beginSwipe() {
// Refuse to enter swipe mode while a non-swipe animation is still
// running; let it finish so the start/end states are well-defined.
if (gZenLibraryInstance?.#animating) {
return;
}
const instance = this.getInstance();
if (instance._deletionIdleCallbackId) {
cancelIdleCallback(instance._deletionIdleCallbackId);
instance._deletionIdleCallbackId = null;
}
instance.#cancelActiveAnimations();
const wasOpen = instance.hasAttribute("open");
if (wasOpen) {
// Library is already open; the wrapper's current inline transform IS
// the target — no remeasure needed.
instance._swipeWrapperTargetPx =
new DOMMatrix(lazy.appContentWrapper.style.transform).m41 ||
instance.#computeWrapperTargetPx();
} else {
// Measure the open-state wrapper target without flashing: temporarily
// mark [open] so layout reflects the open position, then revert.
instance.setAttribute("open", "true");
instance.style.visibility = "hidden";
await new Promise(r => requestAnimationFrame(r));
instance._swipeWrapperTargetPx = instance.#computeWrapperTargetPx();
instance.style.visibility = "";
instance.removeAttribute("open");
lazy.appContentWrapper.style.transform = "";
}
instance._swipeActive = true;
// Initialize visual state to match the current attribute.
ZenLibrary.updateSwipeProgress(wasOpen ? 1 : 0);
}
/**
* Sets the library, toolbox, and content-wrapper styles to a fraction
* (0..1) of the way to fully open. Must be preceded by `beginSwipe()`.
*
* @param {number} progress
*/
static updateSwipeProgress(progress) {
const instance = gZenLibraryInstance;
if (!instance?._swipeActive) {
return;
}
const p = Math.max(0, Math.min(1, progress));
instance.style.transform = `translateX(${(-1 + p) * 100}%)`;
instance.style.opacity = String(p);
const targetOpacity = Number(ZenLibrary.#TOOLBOX_OPEN_OPACITY);
gNavToolbox.style.setProperty(
"transform",
`scale(${1 - p * 0.04})`,
"important"
);
gNavToolbox.style.setProperty(
"opacity",
String(1 - p * (1 - targetOpacity)),
"important"
);
lazy.appContentWrapper.style.setProperty(
"transform",
`translateX(${p * (instance._swipeWrapperTargetPx ?? 0)}px)`,
"important"
);
instance._swipeProgress = p;
}
/**
* Finish a swipe gesture. If `targetOpen` is true, animate the remaining
* distance to fully open; otherwise animate to fully closed and dispose
* the instance like a normal close.
*
* @param {boolean} targetOpen
*/
static async finishSwipe(targetOpen) {
const instance = gZenLibraryInstance;
if (!instance?._swipeActive) {
return;
}
instance._swipeActive = false;
const libFromTransform = instance.style.transform;
const libFromOpacity = instance.style.opacity;
const tbFromTransform = gNavToolbox.style.transform;
const tbFromOpacity = gNavToolbox.style.opacity;
const wrapperFromX = new DOMMatrix(lazy.appContentWrapper.style.transform)
.m41;
// Hand off styling to WAAPI by clearing the inline styles we set during
// the swipe; the keyframes restore the from-state on their first frame.
instance.style.transform = "";
instance.style.opacity = "";
gNavToolbox.style.transform = "";
gNavToolbox.style.opacity = "";
lazy.appContentWrapper.style.transform = "";
if (targetOpen) {
instance.setAttribute("open", "true");
gNavToolbox.setAttribute("zen-library-open", "true");
instance.#animate(instance, [
{ transform: libFromTransform, opacity: libFromOpacity },
{ transform: "translateX(0)", opacity: 1 },
]);
instance.#animate(gNavToolbox, [
{ transform: tbFromTransform, opacity: tbFromOpacity },
{
transform: ZenLibrary.#TOOLBOX_OPEN_TRANSFORM,
opacity: ZenLibrary.#TOOLBOX_OPEN_OPACITY,
},
]);
const wrapperAnim = instance.#animate(lazy.appContentWrapper, [
{ transform: `translateX(${wrapperFromX}px)` },
{ transform: `translateX(${instance._swipeWrapperTargetPx}px)` },
]);
try {
await wrapperAnim.finished;
if (instance.isOpen) {
wrapperAnim.commitStyles();
wrapperAnim.cancel();
instance.#activeAnimations.delete(wrapperAnim);
}
} catch {
/* cancelled by a follow-up toggle */
}
} else {
instance.removeAttribute("open");
gNavToolbox.removeAttribute("zen-library-open");
instance.#animate(instance, [
{ transform: libFromTransform, opacity: libFromOpacity },
{ transform: "translateX(-100%)", opacity: 0 },
]);
instance.#animate(gNavToolbox, [
{ transform: tbFromTransform, opacity: tbFromOpacity },
{ transform: "scale(1)", opacity: 1 },
]);
instance.#animate(lazy.appContentWrapper, [
{ transform: `translateX(${wrapperFromX}px)` },
{ transform: "translateX(0)" },
]);
if (!instance._deletionIdleCallbackId) {
instance._deletionIdleCallbackId = requestIdleCallback(() => {
ZenLibrary.clearInstance();
});
}
}
}
async #animateClose() {
this.#animating = true;
try {
this.#cancelActiveAnimations();
const wrapperCurrent =
lazy.appContentWrapper.style.transform ||
getComputedStyle(lazy.appContentWrapper).transform ||
"translateX(0)";
lazy.appContentWrapper.style.transform = "";
const libAnim = this.#animate(this, [
{ transform: "translateX(0)", opacity: 1 },
{ transform: "translateX(-100%)", opacity: 0 },
]);
this.#animate(gNavToolbox, [
{
transform: ZenLibrary.#TOOLBOX_OPEN_TRANSFORM,
opacity: ZenLibrary.#TOOLBOX_OPEN_OPACITY,
},
{ transform: "scale(1)", opacity: 1 },
]);
this.#animate(lazy.appContentWrapper, [
{ transform: wrapperCurrent },
{ transform: "translateX(0)" },
]);
try {
await libAnim.finished;
} catch {
// Cancelled by a follow-up open before the slide finished.
return;
}
// If the user re-opened in the meantime, leave the live instance alone.
if (this.isOpen) {
return;
}
if (!this._deletionIdleCallbackId) {
this._deletionIdleCallbackId = requestIdleCallback(() => {
ZenLibrary.clearInstance();
});
}
} finally {
this.#animating = false;
}
}
}
customElements.define("zen-library", ZenLibrary);

View File

@@ -0,0 +1,556 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
import { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
DownloadsCommon:
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
DownloadsViewUI:
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
});
const RECENT_KEEP = 5;
// Mouse can stray this far past the panel/button before we dismiss the stack.
const HOVER_TOLERANCE_TOP_PX = 100;
const HOVER_TOLERANCE_SIDE_PX = 40;
/**
* Per-window controller for the toolbar Library button:
* - Watches downloads and overlays a progress ring on the button while
* anything is in flight (indeterminate spinner when total bytes are
* unknown).
* - Pops a small panel with the most recent finished/active downloads
* when the user hovers the button.
*/
class nsZenLibraryButton extends nsZenDOMOperatedFeature {
#button = null;
#downloads = null;
#view = null;
#active = new Set();
#recent = [];
#panel = null;
#closeAnim = null;
#trackingMouse = false;
#contextMenuOpen = false;
init() {
this.#button = document.getElementById("zen-library-button");
if (!this.#button) {
return;
}
this.#setupDownloads();
this.#button.addEventListener("mouseenter", this);
window.addEventListener("unload", this, { once: true });
}
handleEvent(event) {
switch (event.type) {
case "mouseenter":
this.#onEnter();
break;
case "mousemove":
this.#checkMousePosition(event);
break;
case "unload":
this.#button?.removeEventListener("mouseenter", this);
this.#detachMouseTracking();
if (this.#downloads && this.#view) {
this.#downloads.removeView(this.#view);
}
break;
}
}
#setupDownloads() {
this.#downloads = lazy.DownloadsCommon.getData(window, true);
this.#view = {
onDownloadAdded: dl => {
if (!dl.stopped) {
this.#active.add(dl);
} else if (dl.succeeded) {
this.#pushRecent(dl);
}
this.#updateRing();
this.#refreshPanel();
},
onDownloadChanged: dl => {
if (dl.stopped) {
this.#active.delete(dl);
if (dl.succeeded) {
this.#pushRecent(dl);
}
} else {
this.#active.add(dl);
}
this.#updateRing();
this.#refreshPanel();
},
onDownloadRemoved: dl => {
this.#active.delete(dl);
this.#recent = this.#recent.filter(d => d !== dl);
this.#updateRing();
this.#refreshPanel();
},
};
this.#downloads.addView(this.#view);
}
#pushRecent(dl) {
if (!this.#recent.includes(dl)) {
this.#recent.unshift(dl);
this.#recent = this.#recent.slice(0, RECENT_KEEP);
}
}
/** Update the conic-gradient progress + indeterminate state on the button. */
#updateRing() {
if (!this.#button) {
return;
}
if (this.#active.size === 0) {
this.#button.removeAttribute("downloading");
this.#button.removeAttribute("downloading-indeterminate");
this.#button.style.removeProperty("--zen-library-button-progress");
return;
}
let total = 0;
let current = 0;
let hasUnknown = false;
for (const dl of this.#active) {
if (dl.hasProgress && dl.totalBytes > 0) {
total += dl.totalBytes;
current += dl.currentBytes;
} else {
hasUnknown = true;
}
}
this.#button.setAttribute("downloading", "true");
if (hasUnknown || total === 0) {
this.#button.setAttribute("downloading-indeterminate", "true");
this.#button.style.removeProperty("--zen-library-button-progress");
} else {
this.#button.removeAttribute("downloading-indeterminate");
const ratio = Math.max(0, Math.min(1, current / total));
this.#button.style.setProperty(
"--zen-library-button-progress",
String(ratio)
);
}
}
#ensurePanel() {
if (this.#panel) {
return this.#panel;
}
const stack = document.createElement("div");
stack.id = "zen-library-button-panel";
stack.classList.add("zen-library-button-panel");
stack.dataset.state = "closed";
const list = document.createElement("div");
list.className = "zen-library-button-panel-list";
stack.appendChild(list);
const host = document.getElementById("zen-sidebar-foot-buttons");
host.before(stack);
this.#panel = stack;
return stack;
}
#refreshPanel() {
if (!this.#panel || this.#panel.dataset.state !== "open") {
return;
}
this.#populatePanel();
}
#populatePanel() {
const items = [
...Array.from(this.#active),
...this.#recent.filter(d => !this.#active.has(d)),
].slice(0, RECENT_KEEP);
if (!items.length) {
return;
}
const panel = this.#ensurePanel();
const list = panel.querySelector(".zen-library-button-panel-list");
list.replaceChildren();
for (const dl of items) {
list.appendChild(this.#renderRow(dl));
}
}
#renderRow(dl) {
const row = document.createElement("div");
row.className = "zen-library-button-panel-row";
if (this.#isFileMissing(dl)) {
row.dataset.fileDeleted = "true";
}
row.addEventListener("click", () => {
if (dl.succeeded) {
lazy.DownloadsCommon.openDownload(dl).catch(console.error);
} else if (dl.source?.url) {
window.openTrustedLinkIn(dl.source.url, "tab");
}
});
row.addEventListener("contextmenu", e => this.#showContextMenu(e, dl));
const icon = document.createElement("img");
icon.className = "zen-library-button-panel-icon";
icon.alt = "";
icon.src = dl.target?.path
? `moz-icon://${dl.target.path}?size=16`
: "moz-icon://.unknown?size=16";
row.appendChild(icon);
const labels = document.createElement("div");
labels.className = "zen-library-button-panel-labels";
const display = lazy.DownloadsViewUI.getDisplayName(dl);
const name = typeof display === "string" ? display : dl.source?.url || "";
const label = document.createXULElement("label");
label.className = "zen-library-button-panel-name";
label.textContent = name;
labels.appendChild(label);
const sublabel = document.createXULElement("label");
sublabel.className = "zen-library-button-panel-status";
sublabel.textContent = this.#statusFor(dl);
labels.appendChild(sublabel);
row.appendChild(labels);
return row;
}
/** True when the on-disk file is gone (deleted / moved). */
#isFileMissing(dl) {
if (!dl) {
return false;
}
if (dl.deleted) {
return true;
}
return dl.succeeded && dl.target?.exists === false;
}
#statusFor(dl) {
const C = lazy.DownloadsCommon;
const state = C.stateOfDownload(dl);
if (state === C.DOWNLOAD_DOWNLOADING) {
const total = dl.hasProgress ? dl.totalBytes : -1;
const [status] = lazy.DownloadUtils.getDownloadStatus(
dl.currentBytes,
total,
dl.speed
);
return status;
}
if (state === C.DOWNLOAD_FINISHED) {
return (
lazy.DownloadsViewUI.getSizeWithUnits(dl) ||
C.strings.sizeUnknown ||
""
);
}
if (state === C.DOWNLOAD_PAUSED) {
return C.strings.statePaused || "Paused";
}
if (state === C.DOWNLOAD_FAILED) {
return C.strings.stateFailed || "Failed";
}
if (state === C.DOWNLOAD_CANCELED) {
return C.strings.stateCanceled || "Canceled";
}
return "";
}
/**
* Right-click → small XUL menupopup mirroring the relevant subset of
* Firefox's `downloadsContextMenu` (built dynamically so we can react to
* each row's current state). Reuses `browser/downloads.ftl` strings.
*/
#showContextMenu(event, dl) {
event.preventDefault();
event.stopPropagation();
const C = lazy.DownloadsCommon;
const state = C.stateOfDownload(dl);
const isFinished = state === C.DOWNLOAD_FINISHED;
const isActive =
state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_PAUSED;
const fileExists = isFinished && dl.target?.exists !== false && !dl.deleted;
const sourceUrl = dl.source?.originalUrl || dl.source?.url;
const items = [];
if (state === C.DOWNLOAD_DOWNLOADING) {
items.push({
l10nId: "downloads-cmd-pause",
onClick: () => dl.cancel().catch(() => {}),
});
} else if (state === C.DOWNLOAD_PAUSED) {
items.push({
l10nId: "downloads-cmd-resume",
onClick: () => dl.start?.().catch(() => {}),
});
}
if (fileExists) {
items.push({
l10nId: "downloads-cmd-show-menuitem-2",
onClick: () => {
try {
const file = new lazy.FileUtils.File(dl.target.path);
C.showDownloadedFile(file);
} catch (ex) {
console.error(ex);
}
},
});
}
if (sourceUrl) {
items.push({
l10nId: "downloads-cmd-go-to-download-page",
onClick: () => window.openTrustedLinkIn(sourceUrl, "tab"),
});
items.push({
l10nId: "downloads-cmd-copy-download-link",
onClick: () => {
const helper = Cc[
"@mozilla.org/widget/clipboardhelper;1"
].getService(Ci.nsIClipboardHelper);
helper.copyString(sourceUrl);
},
});
}
items.push({ separator: true });
if (fileExists) {
items.push({
l10nId: "downloads-cmd-delete-file",
onClick: () => {
C.deleteDownloadFiles(
dl,
lazy.DownloadsViewUI.clearHistoryOnDelete
).catch(console.error);
},
});
}
if (!isActive) {
items.push({
l10nId: "downloads-cmd-remove-from-history",
onClick: () => C.deleteDownload(dl).catch(console.error),
});
}
if (!items.some(i => !i.separator)) {
return;
}
const popupSet = document.getElementById("mainPopupSet");
const popup = document.createXULElement("menupopup");
for (const item of items) {
if (item.separator) {
if (
!popup.lastChild ||
popup.lastChild.tagName === "menuseparator"
) {
continue;
}
popup.appendChild(document.createXULElement("menuseparator"));
continue;
}
const mi = document.createXULElement("menuitem");
mi.setAttribute("data-l10n-id", item.l10nId);
mi.addEventListener(
"command",
() => {
try {
item.onClick?.();
} catch (ex) {
console.error(ex);
}
},
{ once: true }
);
popup.appendChild(mi);
}
// Drop a trailing separator if any.
while (popup.lastChild?.tagName === "menuseparator") {
popup.lastChild.remove();
}
// Pin the hover panel open for the lifetime of the context menu — the
// popup briefly steals focus and would otherwise let the mouse-distance
// check dismiss the stack mid-interaction.
this.#contextMenuOpen = true;
popup.addEventListener(
"popuphidden",
() => {
this.#contextMenuOpen = false;
popup.remove();
// Re-evaluate distance now that the popup is gone.
if (this.#panel?.dataset.state === "open") {
this.#checkMousePosition();
}
},
{ once: true }
);
popupSet.appendChild(popup);
popup.openPopupAtScreen(event.screenX, event.screenY, true);
}
#onEnter() {
if (this.#active.size === 0 && this.#recent.length === 0) {
return;
}
// Customize mode can move the button out of the sidebar's foot toolbar —
// the inline stack only makes sense when it's still there.
if (!this.#button.closest("#zen-sidebar-foot-buttons")) {
return;
}
// Interrupt a pending close so we reuse the same element instead of
// racing a tear-down.
this.#closeAnim?.cancel();
this.#closeAnim = null;
const panel = this.#ensurePanel();
this.#populatePanel();
panel.dataset.state = "open";
this.#updateMaskHeight(panel);
panel.animate(
[
{ opacity: 0, transform: "translateY(8px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{
duration: 180,
easing: "cubic-bezier(0.32, 0.72, 0, 1)",
fill: "forwards",
id: "zen-library-stack-open",
}
);
this.#attachMouseTracking();
}
/**
* Publish the panel's rendered height as a CSS variable on the tab strip
* so the strip's mask gradient fades over exactly the area the stack
* occupies (instead of a hardcoded amount).
*/
#updateMaskHeight(panel) {
const tabs = window.gBrowser?.tabContainer;
if (!tabs) {
return;
}
requestAnimationFrame(() => {
if (!panel.isConnected) {
return;
}
const h = Math.ceil(panel.getBoundingClientRect().height);
tabs.style.setProperty("--zen-library-stack-height", `${h}px`);
tabs.setAttribute("zen-library-stack-open", "true");
});
}
#attachMouseTracking() {
if (this.#trackingMouse) {
return;
}
window.addEventListener("mousemove", this, true);
this.#trackingMouse = true;
}
#detachMouseTracking() {
if (!this.#trackingMouse) {
return;
}
window.removeEventListener("mousemove", this, true);
this.#trackingMouse = false;
}
/**
* Hide the stack when the cursor moves outside a tolerance zone around the
* panel + button: HOVER_TOLERANCE_TOP_PX above, HOVER_TOLERANCE_SIDE_PX on
* either side (and the same below). Pinned open while a context menu is up.
*/
#checkMousePosition(event) {
if (this.#contextMenuOpen || !this.#panel) {
return;
}
if (this.#panel.dataset.state !== "open") {
this.#detachMouseTracking();
return;
}
const x = event?.clientX ?? -Infinity;
const y = event?.clientY ?? -Infinity;
const panelRect = this.#panel.getBoundingClientRect();
const buttonRect = this.#button?.getBoundingClientRect();
const left =
Math.min(panelRect.left, buttonRect?.left ?? panelRect.left) -
HOVER_TOLERANCE_SIDE_PX;
const right =
Math.max(panelRect.right, buttonRect?.right ?? panelRect.right) +
HOVER_TOLERANCE_SIDE_PX;
const top = panelRect.top - HOVER_TOLERANCE_TOP_PX;
const bottom =
Math.max(panelRect.bottom, buttonRect?.bottom ?? panelRect.bottom) +
HOVER_TOLERANCE_SIDE_PX;
if (x < left || x > right || y < top || y > bottom) {
this.#hide();
}
}
#hide() {
if (!this.#panel || this.#closeAnim) {
return;
}
this.#detachMouseTracking();
const tabs = window.gBrowser?.tabContainer;
tabs?.removeAttribute("zen-library-stack-open");
const panel = this.#panel;
panel.dataset.state = "closed";
const anim = panel.animate(
[
{ opacity: 1, transform: "translateY(0)" },
{ opacity: 0, transform: "translateY(8px)" },
],
{
duration: 140,
easing: "cubic-bezier(0.32, 0.72, 0, 1)",
fill: "forwards",
id: "zen-library-stack-close",
}
);
this.#closeAnim = anim;
anim.finished.then(
() => {
if (this.#closeAnim !== anim) {
return;
}
panel.remove();
if (this.#panel === panel) {
this.#panel = null;
}
this.#closeAnim = null;
tabs?.style.removeProperty("--zen-library-stack-height");
},
() => {
/* cancelled — re-entered while closing */
}
);
}
}
new nsZenLibraryButton();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
content/browser/zen-styles/zen-library.css (../../zen/library/zen-library.css)

View File

@@ -0,0 +1,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
MOZ_SRC_FILES += [
"ZenLibrary.mjs",
"ZenLibraryButton.mjs",
"ZenLibrarySections.mjs",
]

View File

@@ -0,0 +1,677 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
zen-library {
@media (-moz-platform: windows) {
--border-radius-medium: 6px;
--tab-border-radius: 4px;
}
@media (-moz-platform: macos) {
--border-radius-medium: 14px;
--tab-border-radius: 8px;
font-size: 1rem; /* Slightly larger font on macOS */
}
display: flex;
position: absolute;
height: 100%;
transform: translateX(-100%);
opacity: 0;
will-change: transform, opacity;
z-index: 2;
}
zen-library[open] {
transform: translateX(0);
opacity: 1;
z-index: 9;
}
/* Layout Components */
#zen-library-sidebar {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 84px;
height: 100%;
padding: 8px;
background: rgba(255, 255, 255, 0.05);
box-shadow: rgba(0, 0, 0, 0.1) 0 10px 50px;
border-right: 1px solid rgba(0,0,0,.08);
-moz-window-dragging: drag;
}
#zen-library-content {
display: flex;
width: 27rem;
max-width: 80vh;
overflow: hidden;
transition: width 0.15s ease-in-out;
&[large-content] {
width: 75rem;
}
& > * {
/*
* Fixed width so inner content does not stretch or squish while the
* outer container animates. flex-shrink: 0 prevents flexbox from
* compressing it below this value during the width transition.
*/
display: flex;
flex-direction: column;
width: 27rem;
min-width: 27rem;
flex-shrink: 0;
overflow: hidden;
}
}
/* Sidebar Tabs */
#zen-library-sidebar-tabs {
display: flex;
flex-direction: column;
gap: 8px;
-moz-window-dragging: no-drag;
}
.zen-library-tab {
display: flex;
position: relative;
justify-content: center;
align-items: center;
padding: 12px 0;
background: transparent;
font-weight: 600;
flex-direction: column;
gap: 4px;
&::before {
content: "";
position: absolute;
inset: 0;
border-radius: 8px;
transition: transform 0.1s ease-in-out;
}
&:hover::before {
background: color-mix(in srgb, currentColor 5%, transparent);
}
&[active]::before {
background: color-mix(in srgb, currentColor 10%, transparent);
}
&:hover:active::before {
transform: scale(0.96);
}
& .zen-library-tab-icon {
width: 28px;
height: 28px;
overflow: clip;
}
& .zen-library-tab-icon-image {
width: calc(36 * 28px);
height: 28px;
fill: rgba(255, 255, 255, 0.8);
stroke: var(--zen-colors-primary);
-moz-context-properties: fill, stroke;
transform: translateX(0);
}
&[data-section="history"] .zen-library-tab-icon-image {
background-image: url("chrome://browser/skin/zen-icons/library/library-history-sprite.svg");
}
&[data-section="downloads"] .zen-library-tab-icon-image {
background-image: url("chrome://browser/skin/zen-icons/library/library-downloads-sprite.svg");
}
&[data-section="boosts"] .zen-library-tab-icon-image {
background-image: url("chrome://browser/skin/zen-icons/library/library-boosts-sprite.svg");
}
&[data-section="spaces"] .zen-library-tab-icon-image {
background-image: url("chrome://browser/skin/zen-icons/library/library-spaces-sprite.svg");
}
&[data-section="media"] .zen-library-tab-icon-image {
background-image: url("chrome://browser/skin/zen-icons/library/library-media-sprite.svg");
}
@media (prefers-reduced-motion: no-preference) {
&[animate] .zen-library-tab-icon-image {
animation: zen-library-tab-icon-play 0.583s steps(36, jump-none);
}
}
}
@keyframes zen-library-tab-icon-play {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(35 * -28px));
}
}
/* Search Bar */
.search-bar {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 4px;
margin: 8px 6px;
padding: 0;
@media (-moz-platform: macos) {
font-size: 1rem; /* Slightly larger font on macOS */
}
}
.search-urlbar {
display: flex;
position: relative;
flex: 1;
align-items: stretch;
min-width: 0;
height: 38px;
margin-inline: 2px;
background: transparent;
border-radius: var(--border-radius-medium);
}
.search-urlbar-background {
position: absolute;
inset: 0;
background: var(--zen-toolbar-element-bg);
border-radius: var(--border-radius-medium);
pointer-events: none;
}
.search-urlbar:hover .search-urlbar-background {
background: var(--zen-toolbar-element-bg-hover);
}
.search-input-container {
display: flex;
position: relative;
flex: 1;
align-items: center;
min-width: 0;
gap: 0;
padding-inline: var(--urlbar-icon-padding, 9px);
padding-block: var(--urlbar-padding-block, 4px);
overflow: hidden;
& .search-icon {
width: 18px;
height: 18px;
margin-inline-end: 6px;
padding: 4px;
flex-shrink: 0;
fill: currentColor;
opacity: 0.5;
-moz-context-properties: fill, fill-opacity;
}
& .search-input {
flex: 1;
min-width: 0;
height: calc(var(--urlbar-height, 38px) - 2 * var(--urlbar-padding-block, 4px));
margin: 0;
padding: 0;
background: transparent;
border: none;
outline: none;
color: var(--toolbar-field-color, rgba(0,0,0,0.8));
font-weight: 500;
line-height: var(--tab-label-line-height);
&::placeholder {
color: inherit;
opacity: 0.5;
}
}
&:focus-within .search-input {
color: var(--toolbar-field-focus-color, light-dark(black, rgb(251,251,254)));
}
}
.search-filter-button {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 4px;
height: var(--urlbar-height, 38px);
margin-inline: 2px;
padding: 0 8px;
background: var(--zen-toolbar-element-bg);
border: none;
border-radius: var(--border-radius-medium);
color: inherit;
font-weight: 500;
line-height: var(--tab-label-line-height);
fill: currentColor;
fill-opacity: 0.5;
-moz-context-properties: fill, fill-opacity;
& label {
opacity: .8;
}
&:hover,
&[open] {
background: var(--zen-toolbar-element-bg-hover);
}
&:active {
background: var(--toolbarbutton-active-background, color-mix(in srgb, light-dark(#101010,#e2e2e2) 7%, transparent 93%));
}
}
/* List */
.search-results {
display: flex;
flex: 1;
flex-direction: column;
align-items: stretch;
padding: 10px;
padding-top: 0 !important;
overflow-y: auto;
scrollbar-width: auto;
scrollbar-color: color-mix(in srgb, currentColor 35%, transparent 65%) transparent;
-moz-window-dragging: drag;
@media (-moz-platform: macos) {
font-size: 1.25rem; /* Slightly larger font on macOS */
}
&:not([scrolled-to-top]) {
box-shadow: inset 0 10px 10px -10px var(--zen-toolbar-element-bg);
}
}
/* unfinished */
.library-date-separator {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
padding: 4px;
padding-top: 12px;
font-size: .9em;
font-weight: 500;
letter-spacing: 0.05em;
opacity: 0.65;
/* I am not sure if we should keep the separator line or not, I'll ask cheffy later */
/*&::after {
content: "";
flex: 1;
height: 1px;
opacity: 0.5;
background: var(--toolbarseparator-color);
}*/
}
.library-item {
--tab-label-mask-size: 1em;
display: flex;
align-items: stretch;
appearance: none;
background-color: transparent;
border-width: 0;
border-radius: 0;
color: inherit;
color-scheme: unset;
overflow: clip;
}
.library-item-stack {
display: grid;
position: relative;
width: 100%;
-moz-window-dragging: no-drag;
}
.library-item-background {
position: absolute;
inset: 0;
min-height: var(--tab-min-height);
background-color: transparent;
border-radius: var(--border-radius-medium) !important;
outline: var(--tab-outline);
outline-offset: var(--tab-outline-offset);
overflow: hidden;
}
.library-item:hover .library-item-background {
background-color: var(--tab-hover-background-color);
outline-color: var(--tab-hover-outline-color);
}
.library-item-content {
display: flex;
position: relative;
align-items: center;
min-width: 0;
padding: 0 10px;
overflow: clip;
height: 56px;
}
.library-item-icon-stack {
display: flex;
position: relative;
align-items: center;
justify-content: center;
}
.library-item-icon-image {
flex-shrink: 0;
width: 18px;
height: 18px;
margin-inline-start: calc(var(--toolbarbutton-padding-inner) / 2);
margin-inline-end: calc(var(--toolbarbutton-padding-inner) * 1.5);
padding: 0;
border-radius: 4px;
fill: currentColor;
-moz-context-properties: fill, stroke;
}
.library-download-progress {
display: flex;
position: relative;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin-inline-start: calc(var(--toolbarbutton-padding-inner) / 2);
margin-inline-end: calc(var(--toolbarbutton-padding-inner) * 1.5);
padding: 0;
appearance: none;
background: transparent;
border: none;
border-radius: 50%;
color: currentColor;
.library-item-stack:hover & {
color: var(--zen-colors-primary);
}
& > svg {
position: absolute;
inset: 0;
pointer-events: none;
transition: opacity 0.12s ease;
}
}
.library-download-progress-ring .track {
fill: none;
stroke: color-mix(in srgb, currentColor 22%, transparent);
stroke-width: 1.8;
}
.library-download-progress-ring .arc {
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: 50% 50%;
transition: stroke-dashoffset 0.25s linear;
}
.library-download-progress[data-progress="indeterminate"] .arc {
stroke-dasharray: 14 50 !important;
animation: zen-library-download-spin 1.2s linear infinite;
}
@keyframes zen-library-download-spin {
from {
transform: rotate(-90deg);
}
to {
transform: rotate(270deg);
}
}
.library-download-progress-cancel {
opacity: 0;
-moz-context-properties: fill;
fill: var(--toolbox-textcolor);
}
.library-item-stack:hover .library-download-progress-ring .arc,
.library-item-stack:hover .library-download-progress-ring .track {
opacity: 0;
}
.library-item-stack:hover .library-download-progress-cancel {
opacity: 1;
}
.library-item-label-container {
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
opacity: 1;
direction: ltr;
mask-image: linear-gradient(to left, transparent, black var(--tab-label-mask-size));
overflow: hidden;
gap: 4px;
}
.library-item-label {
margin-inline: 0;
white-space: nowrap;
line-height: 1;
}
.library-item[file-missing] .library-item-label {
text-decoration: line-through;
opacity: 0.6;
}
/* finished (i hope so) */
.library-item-sublabel {
margin: 0;
font-size: 11px;
white-space: nowrap;
opacity: 0.6; /* what opacity should i keep? */
pointer-events: none;
overflow: hidden;
}
.library-item-side {
display: flex;
flex-direction: column;
flex-shrink: 0;
align-items: flex-end;
justify-content: center;
gap: 2px;
margin-inline-start: 8px;
padding-inline-end: 4px;
text-align: end;
white-space: nowrap;
opacity: 0.65;
pointer-events: none;
}
.library-item-side-top {
font-size: 0.85em;
}
.library-item-side-bottom {
font-size: x-small;
opacity: 0.85;
}
.library-item-actions {
display: none;
flex-shrink: 0;
align-items: center;
gap: 4px;
margin-inline-start: 8px;
}
.library-item:hover .library-item-actions,
.library-item:focus-within .library-item-actions {
display: flex;
}
.library-item-action-button {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
padding: 6px;
appearance: none;
background: transparent;
border: none;
border-radius: var(--border-radius-small);
color: inherit;
fill: currentColor;
-moz-context-properties: fill, fill-opacity;
& img {
width: 18px;
height: 18px;
opacity: 0.75;
pointer-events: none;
}
&:hover {
background: color-mix(in srgb, currentColor 12%, transparent);
}
&:hover img {
opacity: 1;
}
}
.empty-state {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
font-size: 2em;
font-weight: 600;
opacity: 0.5;
}
/* Boosts section */
.library-boost-item .library-item-content {
gap: 6px;
}
.library-boost-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
margin-inline-end: 8px;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.15);
border-radius: 12px;
position: relative;
overflow: hidden;
}
.library-boost-icon-image {
width: 24px;
height: 24px;
border-radius: 10px;
background: rgba(0, 0, 0, 0.05);
padding: 7.5px;
}
/* Slashed-out look when inactive — mirrors the screenshot's first row. */
.library-boost-icon[inactive] {
opacity: 0.5;
}
.library-boost-icon[inactive]::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
to bottom right,
transparent calc(50% - 1px),
currentColor calc(50% - 1px),
currentColor calc(50% + 1px),
transparent calc(50% + 1px)
);
pointer-events: none;
}
.library-boost-toggle {
display: inline-flex;
position: relative;
flex-shrink: 0;
align-items: center;
width: 36px;
height: 22px;
padding: 2px;
appearance: none;
background: color-mix(in srgb, currentColor 18%, transparent);
border: none;
border-radius: 999px;
opacity: 0;
transition:
background-color 0.18s ease,
opacity 0.12s ease;
}
/* Stays visible while it represents an "on" state so the user still sees
* which boosts are enabled at a glance; otherwise reveal on row hover. */
.library-boost-item:hover .library-boost-toggle,
.library-boost-toggle:focus-visible {
opacity: 1;
}
.library-boost-item .library-item-content {
padding-left: 6px;
}
.library-boost-toggle[checked] {
background: var(--zen-colors-primary);
}
.library-boost-toggle-thumb {
display: block;
width: 18px;
height: 18px;
background: light-dark(white, rgba(255, 255, 255, 0.95));
border-radius: 50%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transition: transform 0.18s cubic-bezier(0.32, 0.72, 0, 1);
pointer-events: none;
}
.library-boost-toggle[checked] .library-boost-toggle-thumb {
transform: translateX(14px);
}

View File

@@ -1,6 +1,7 @@
// 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, {
NetUtil: "resource://gre/modules/NetUtil.sys.mjs",

View File

@@ -200,6 +200,50 @@ class nsZenLiveFoldersManager {
return this.liveFolders.get(id);
}
/**
* Returns the provider config needed to recreate this live folder on
* another device, or null for non-live folders. Fetch bookkeeping fields
* are stripped since they are device-local.
*
* @param {string} id - The folder ID.
*/
getSyncableFolderData(id) {
const liveFolder = this.liveFolders.get(id);
if (!liveFolder) {
return null;
}
const state = { ...liveFolder.serialize().state };
delete state.lastFetched;
delete state.lastErrorId;
return { type: liveFolder.constructor.type, state };
}
/**
* Registers a live folder provider for a folder that arrived from
* Firefox Sync. The folder element must already exist.
*
* @param {string} id - The folder ID.
* @param {{ type: string, state: object }} config - Synced provider config.
*/
async adoptSyncedFolder(id, { type, state } = {}) {
await this.stateRestored.promise;
if (this.liveFolders.has(id)) {
return;
}
const ProviderClass = this.registry.get(type);
if (!ProviderClass || !state) {
return;
}
const liveFolder = new ProviderClass({
id,
state: this.#applyDefaultStateValues({ ...state }),
manager: this,
});
this.liveFolders.set(id, liveFolder);
liveFolder.start();
this.saveState();
}
async createFolder(type) {
const [provider, providerType] = type.split(":");
let ProviderClass = this.registry.get(provider);
@@ -324,7 +368,7 @@ class nsZenLiveFoldersManager {
},
],
content: {
width: "310px",
width: "312px",
position: "callout",
title_logo: {
imageURL: icon,
@@ -619,6 +663,7 @@ class nsZenLiveFoldersManager {
async #restoreState() {
let data = await this.#readStateFromDisk();
if (!Array.isArray(data)) {
this.stateRestored.resolve();
return;
}

View File

@@ -346,7 +346,7 @@ class ZenMediaCard {
onToggle() {
if (this.controller?.isPlaying) {
this.controller.pause();
this.controller.pause("user");
} else {
this.controller?.play();
}
@@ -365,7 +365,7 @@ class ZenMediaCard {
}
onSeekDrag(event) {
this.controller?.pause();
this.controller?.pause("user");
const newTime = (event.target.value / 100) * this.duration;
this.currentTimeEl.textContent = this.formatSecondsToTime(newTime);
}
@@ -408,7 +408,7 @@ class ZenMediaCard {
onClose() {
if (this.controller) {
this.controller.pause();
this.controller.pause("user");
} else {
const webRTC =
this.browser.browsingContext.currentWindowGlobal.getActor("WebRTC");

View File

@@ -20,6 +20,7 @@ DIRS += [
"sessionstore",
"share",
"spaces",
"library",
"space-routing",
"sync",
"window-drag",

View File

@@ -10,7 +10,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenLiveFoldersManager:
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
SessionStartup: "resource:///modules/sessionstore/SessionStartup.sys.mjs",
@@ -611,7 +610,9 @@ export class nsZenSessionManager {
}
);
this.#collectWindowData(windows);
lazy.ZenSyncStore.notifyAboutChanges();
// Let interested consumers (e.g. Firefox Sync) know fresh sidebar data
// is available, without this module knowing anything about them.
Services.obs.notifyObservers(null, "zen-sidebar-data-collected");
// This would save the data to disk asynchronously or when quitting the app.
let sidebar = this.#sidebarWithoutCloning;
this.#file.data = sidebar;

View File

@@ -606,7 +606,9 @@ class nsZenWindowSync {
if (isTab) {
if (originalIsEssential !== targetIsEssential) {
if (originalIsEssential) {
gZenPinnedTabManager.addToEssentials(aTargetItem);
gZenPinnedTabManager.addToEssentials(aTargetItem, {
replicating: true,
});
} else {
gZenPinnedTabManager.removeEssentials(
aTargetItem,
@@ -1232,7 +1234,7 @@ class nsZenWindowSync {
activeIndex = Math.min(activeIndex, entries.length - 1);
activeIndex = Math.max(activeIndex, 0);
let entryToUse = (entries[activeIndex] || entries[0]) ?? null;
this.#setPinnedInitialState(
this.setPinnedInitialState(
aTab,
{ url: entryToUse?.url, title: entryToUse?.title },
image
@@ -1250,14 +1252,22 @@ class nsZenWindowSync {
*/
setPinnedUrl(aTab, aUrl, aImage) {
this.log(`Setting pinned url for tab ${aTab.id}`);
this.#setPinnedInitialState(
this.setPinnedInitialState(
aTab,
{ url: aUrl, title: aTab.zenStaticLabel },
aImage
);
}
#setPinnedInitialState(aTab, aEntry, aImage) {
/**
* Sets the pinned initial state (canonical entry and icon) for a tab's
* instances across all windows.
*
* @param {object} aTab - Any window's instance of the tab.
* @param {object} aEntry - The canonical { url, title } entry.
* @param {string} [aImage] - Optional icon to store.
*/
setPinnedInitialState(aTab, aEntry, aImage) {
const initialState = { entry: aEntry, image: aImage };
this.#runOnAllWindows(null, win => {
const targetTab = this.getItemFromWindow(win, aTab.id);
@@ -1362,7 +1372,9 @@ class nsZenWindowSync {
return;
}
tab._zenContentsVisible = true;
tab.id = this.#newTabSyncId;
if (!tab.id) {
tab.id = this.#newTabSyncId;
}
if (lazy.gSyncOnlyPinnedTabs && !tab.pinned) {
return;
}

View File

@@ -27,7 +27,7 @@ class nsZenSpaceRoutingManager {
const element = window.MozXULElement.parseXULToFragment(`
<menuseparator/>
<menuitem id="context_zen-add-domain-to-routing"
data-lazy-l10n-id="tab-context-zen-add-domain-to-sr"
data-l10n-id="tab-context-zen-add-domain-to-sr"
data-l10n-args='{"tabCount": 1}'/>
`);
window.document.getElementById("context_undoCloseTab").after(element);

View File

@@ -11,7 +11,6 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
@@ -173,63 +172,6 @@ class nsZenWorkspaces {
}
}
/**
* Applies live sync changes: updates workspace cache, removes deleted items,
* then creates/updates pulled items.
*
* @param {{ spaces: Array}} pulled Reconcile-pulled items.
* @param {{ spaces: Array}} removals Items to remove.
*/
async _applySyncChanges(pulled, removals = {}) {
if (!this.shouldHaveWorkspaces || this.privateWindowOrDisabled) {
return;
}
await this.promiseInitialized;
// 1. Update workspace cache (remove deleted, merge pulled)
const removedSpaceIds = new Set((removals.spaces || []).map(s => s.uuid));
if (removedSpaceIds.size || pulled.spaces?.length) {
const localMap = new Map(
this.getWorkspaces()
.filter(w => !removedSpaceIds.has(w.uuid))
.map(w => [w.uuid, w])
);
for (const space of pulled.spaces || []) {
if (!space?.uuid) {
continue;
}
const existing = localMap.get(space.uuid);
localMap.set(space.uuid, existing ? { ...existing, ...space } : space);
}
await this.propagateWorkspaces(
this.#getOrderedWorkspacesByPosition(Array.from(localMap.values()))
);
this.#propagateWorkspaceData();
}
}
#getOrderedWorkspacesByPosition(workspaces) {
return [...workspaces]
.map((workspace, index) => ({ workspace, index }))
.sort((a, b) => {
const aPosition =
typeof a.workspace.position === "number"
? a.workspace.position
: a.index;
const bPosition =
typeof b.workspace.position === "number"
? b.workspace.position
: b.index;
return aPosition - bPosition || a.index - b.index;
})
.map(({ workspace }) => {
// strip the position property that comes from pulled workspaces
const rest = { ...workspace };
delete rest.position;
return rest;
});
}
#afterLoadInit() {
const onResize = (...args) => {
requestAnimationFrame(() => {
@@ -862,13 +804,6 @@ class nsZenWorkspaces {
})();
}
#markWorkspaceChanged(workspaceId) {
lazy.ZenSyncStore.markItemChanged({
type: "space",
id: workspaceId,
});
}
async selectStartPage() {
if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
return;
@@ -1297,8 +1232,6 @@ class nsZenWorkspaces {
} else {
workspacesData.push(workspaceData);
}
// mark item as changed for sync
this.#markWorkspaceChanged(workspaceData.uuid);
this.#propagateWorkspaceData();
}
@@ -1307,9 +1240,6 @@ class nsZenWorkspaces {
let { promise, resolve } = Promise.withResolvers();
this.#deleteWorkspaceOwnedTabs(windowID);
// mark item as changed for sync
this.#markWorkspaceChanged(windowID);
let workspacesData = this.getWorkspaces();
// Remove the workspace from the cache
workspacesData = workspacesData.filter(
@@ -1442,11 +1372,6 @@ class nsZenWorkspaces {
return;
}
const workspaces = this.getWorkspaces();
// Track previous positions so we only notify observers for workspaces whose
// position changed during the reorder.
const previousPositions = new Map(
workspaces.map((workspace, index) => [workspace.uuid, index])
);
const workspace = workspaces.find(w => w.uuid === id);
if (!workspace) {
@@ -1475,15 +1400,6 @@ class nsZenWorkspaces {
// Propagate the changes if the order has changed
if (currentIndex !== newPosition) {
this._workspaceCache = workspaces;
for (const [i, ws] of workspaces.entries()) {
if (previousPositions.get(ws.uuid) === i) {
continue;
}
// mark item as changed for sync
this.#markWorkspaceChanged(ws.uuid);
}
this.#propagateWorkspaceData(workspaces);
}
}
@@ -1790,6 +1706,15 @@ class nsZenWorkspaces {
}
makeSureEmptyTabIsFirst() {
if (
gZenUIManager.testingEnabled &&
this._emptyTab &&
(this._emptyTab.closing || !this._emptyTab.isConnected)
) {
this._emptyTab = gBrowser.tabs.find(
tab => tab.hasAttribute("zen-empty-tab") && !tab.closing
);
}
const emptyTab = this._emptyTab;
if (emptyTab) {
emptyTab.setAttribute("zen-workspace-id", this.activeWorkspace);
@@ -3320,8 +3245,10 @@ class nsZenWorkspaces {
return width;
}, 0);
// Check if the total width exceeds the parent's width
if (totalWidth > parent.clientWidth) {
// Check if the total width exceeds the parent's width.
const parentWidth =
window.windowUtils.getBoundsWithoutFlushing(parent).width;
if (totalWidth > parentWidth) {
parent.setAttribute("icons-overflow", "true");
} else {
parent.removeAttribute("icons-overflow");
@@ -3329,7 +3256,7 @@ class nsZenWorkspaces {
// Set the width of each icon to the maximum size they can fit on
const widthPerButton = Math.max(
(parent.clientWidth - separation * (parent.children.length - 1)) /
(parentWidth - separation * (parent.children.length - 1)) /
parent.children.length,
minButtonSize
);

View File

@@ -4,6 +4,14 @@
const lazy = {};
ChromeUtils.defineESModuleGetters(
lazy,
{
ZenLibrary: "moz-src:///zen/library/ZenLibrary.mjs",
},
{ global: "current" }
);
ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
return document.getElementById("zen-browser-background");
});
@@ -12,6 +20,11 @@ ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
return document.getElementById("zen-toolbar-background");
});
// Distance (in swipe-translate units, after the configured multiplier) that
// corresponds to a fully open library. Crossing this on swipe-end commits.
const LIBRARY_SWIPE_FULL_DISTANCE_FACTOR = 0.6;
const LIBRARY_SWIPE_COMMIT_THRESHOLD = 0.3;
export class ZenSpacesSwipe {
_swipeState = {
isGestureActive: false,
@@ -115,14 +128,25 @@ export class ZenSpacesSwipe {
isGestureActive: true,
lastDelta: 0,
direction: null,
librarySwiping: false,
libraryStartProgress: 0,
libraryProgress: 0,
};
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", true);
}
_handleSwipeUpdate(event) {
const ws = gZenWorkspaces;
/** True when the active workspace is the first one in the cached list. */
#isAtFirstWorkspace() {
const workspaces = gZenWorkspaces.getWorkspaces();
const active = gZenWorkspaces.getActiveWorkspaceFromCache();
return workspaces.indexOf(active) === 0;
}
if (!ws.workspaceEnabled || !this._swipeState?.isGestureActive) {
_handleSwipeUpdate(event) {
if (
!gZenWorkspaces.workspaceEnabled ||
!this._swipeState?.isGestureActive
) {
return;
}
@@ -150,13 +174,45 @@ export class ZenSpacesSwipe {
}
if (Math.abs(delta) > 0.9) {
delete ws._hasAnimatedBackgrounds;
delete gZenWorkspaces._hasAnimatedBackgrounds;
this._swipeState.direction = delta > 0 ? "left" : "right";
}
// The library can hijack the swipe in two cases:
// - Already open → swipe leftwards (translateX < 0) closes it.
// - Closed and on the first workspace → swipe rightwards (translateX > 0)
// opens it from the left edge.
const libraryOpen = lazy.ZenLibrary.isOpen;
const wantsClose = libraryOpen && translateX < 0;
const wantsOpen =
!libraryOpen && translateX > 0 && this.#isAtFirstWorkspace();
if (wantsOpen || wantsClose || this._swipeState.librarySwiping) {
if (!this._swipeState.librarySwiping) {
this._swipeState.librarySwiping = true;
this._swipeState.libraryStartProgress = libraryOpen ? 1 : 0;
// Fire-and-forget; the first update before measurement completes
// re-applies the current start progress, which is already visible.
lazy.ZenLibrary.beginSwipe();
}
const deltaProgress =
translateX / (stripWidth * LIBRARY_SWIPE_FULL_DISTANCE_FACTOR);
const progress = Math.max(
0,
Math.min(1, this._swipeState.libraryStartProgress + deltaProgress)
);
this._swipeState.libraryProgress = progress;
lazy.ZenLibrary.updateSwipeProgress(progress);
// Skip the workspace-strip translate while the library owns the swipe.
return;
}
// Apply a translateX to the tab strip to give the user feedback on the swipe
const currentWorkspace = ws.getActiveWorkspaceFromCache();
ws._organizeWorkspaceStripLocations(currentWorkspace, true, translateX);
const currentWorkspace = gZenWorkspaces.getActiveWorkspaceFromCache();
gZenWorkspaces._organizeWorkspaceStripLocations(
currentWorkspace,
true,
translateX
);
}
async _handleSwipeEnd(event) {
@@ -167,6 +223,17 @@ export class ZenSpacesSwipe {
}
event.preventDefault();
event.stopPropagation();
// If the swipe was driving the library, commit to whichever side of the
// threshold the progress landed on and don't change workspace.
if (this._swipeState.librarySwiping) {
const targetOpen =
this._swipeState.libraryProgress >= LIBRARY_SWIPE_COMMIT_THRESHOLD;
delete this._swipeState.librarySwiping;
await lazy.ZenLibrary.finishSwipe(targetOpen);
return;
}
const isRTL = document.documentElement.matches(":-moz-locale-dir(rtl)");
const moveForward =
(event.direction === SimpleGestureEvent.DIRECTION_RIGHT) !== isRTL;
@@ -179,6 +246,10 @@ export class ZenSpacesSwipe {
onSwipeGestureAnimationEnd() {
const ws = gZenWorkspaces;
if (this._swipeState.librarySwiping) {
lazy.ZenLibrary.finishSwipe(false);
}
// Reset swipe state
this._swipeState = {
isGestureActive: false,

View File

@@ -230,7 +230,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
}
this.removeGroup(groupIndex);
if (changeTab) {
gBrowser.selectedTab = remainingTabs[0];
const tabToSelect = remainingTabs.find(remaining => remaining !== tab);
if (tabToSelect) {
gBrowser.selectedTab = tabToSelect;
}
document
.getElementById("cmd_zenNewEmptySplit")
.removeAttribute("disabled");
@@ -2530,22 +2533,33 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
this.activateSplitView(data);
gBrowser.selectedTab = emptyTab;
setTimeout(() => {
const closeToken = {};
const controller = new AbortController();
const cleanup = (onSwitch = false, groupIndex = null) => {
if (groupIndex === null) {
groupIndex = this._data.findIndex(group =>
group.tabs.includes(emptyTab)
);
}
this.removeTabFromGroup(emptyTab, groupIndex, {
changeTab: !onSwitch,
forUnsplit: true,
});
const command = document.getElementById("cmd_zenNewEmptySplit");
command.removeAttribute("disabled");
};
window.addEventListener(
"ZenURLBarClosed",
event => {
if (!gZenUIManager.matchesCloseToken(closeToken, event)) {
return;
}
controller.abort();
const { onElementPicked, onSwitch } = event.detail;
const groupIndex = this._data.findIndex(group =>
group.tabs.includes(emptyTab)
);
const newSelectedTab = gBrowser.selectedTab;
const cleanup = () => {
this.removeTabFromGroup(emptyTab, groupIndex, {
changeTab: !onSwitch,
forUnsplit: true,
});
const command = document.getElementById("cmd_zenNewEmptySplit");
command.removeAttribute("disabled");
};
if (onElementPicked) {
if (
newSelectedTab === emptyTab ||
@@ -2553,7 +2567,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
selectedTab.getAttribute("zen-workspace-id") !==
newSelectedTab.getAttribute("zen-workspace-id")
) {
cleanup();
cleanup(onSwitch, groupIndex);
return;
}
this.removeTabFromGroup(emptyTab, groupIndex, {
@@ -2569,12 +2583,17 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
topOrLeft ? 0 : 1
);
} else {
cleanup();
cleanup(onSwitch, groupIndex);
}
},
{ once: true }
{ signal: controller.signal }
);
gZenUIManager.handleNewTab(false, false, "tab", true);
if (
!gZenUIManager.handleNewTab(false, false, "tab", true, closeToken)
) {
controller.abort();
cleanup();
}
});
});
}

View File

@@ -0,0 +1,182 @@
/* 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 {
Store,
SyncEngine,
Tracker,
} from "resource://services-sync/engines.sys.mjs";
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
import {
SIDEBAR_COLLECTED_TOPIC,
syncLog,
ZenSpacesSyncModel,
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSpacesSyncApplier: "resource:///modules/zen/ZenSpacesSyncApplier.sys.mjs",
});
const TRACKED_TOPICS = [
SIDEBAR_COLLECTED_TOPIC,
"contextual-identity-created",
"contextual-identity-updated",
"contextual-identity-deleted",
];
export class ZenSpacesSyncRecord extends CryptoWrapper {
_logName = "Sync.Record.ZenSpaces";
}
ZenSpacesSyncRecord.prototype.type = "spaces";
/**
* Store for the Spaces engine. Outgoing records are pure projections of
* the session-store sidebar data; incoming batches are handed to the
* applier, which reconciles them into the live browser.
*/
class ZenSpacesSyncStore extends Store {
async getAllIDs() {
return ZenSpacesSyncModel.getAllRecordIds();
}
async itemExists(id) {
return ZenSpacesSyncModel.itemExists(id);
}
async createRecord(id, collection) {
const record = new ZenSpacesSyncRecord(collection, id);
const projected = ZenSpacesSyncModel.projectRecord(id);
if (!projected) {
record.deleted = true;
return record;
}
record.cleartext = { id, kind: projected.kind, data: projected.data };
return record;
}
async applyIncomingBatch(records, countTelemetry) {
const failed = await lazy.ZenSpacesSyncApplier.applyBatch(records);
for (const id of failed) {
countTelemetry?.addIncomingFailedReason(`failed to apply ${id}`);
}
return failed;
}
async wipe() {
// Never delete user data on an engine wipe.
}
async changeItemID() {}
}
/**
* Tracker for the Spaces engine. There is no per-event bookkeeping: every
* time fresh sidebar data is collected (or a container changes), the model
* diffs the current projections against the last-uploaded snapshot; the
* score is only bumped when something actually differs.
*/
class ZenSpacesSyncTracker extends Tracker {
_ignoreAll = false;
get ignoreAll() {
return this._ignoreAll;
}
set ignoreAll(value) {
this._ignoreAll = value;
}
onStart() {
for (const topic of TRACKED_TOPICS) {
Services.obs.addObserver(this, topic);
}
}
onStop() {
for (const topic of TRACKED_TOPICS) {
Services.obs.removeObserver(this, topic);
}
}
observe(subject, topic) {
if (this.ignoreAll) {
return;
}
if (topic !== SIDEBAR_COLLECTED_TOPIC) {
ZenSpacesSyncModel.invalidate();
}
try {
if (ZenSpacesSyncModel.hasPendingChanges()) {
syncLog(`tracker: pending changes after ${topic}, requesting sync`);
this.score += SCORE_INCREMENT_XLARGE;
}
} catch (e) {
console.error("ZenSpacesSync: change detection failed:", e);
}
}
}
export class ZenSpacesSyncEngine extends SyncEngine {
constructor(service) {
super("Spaces", service);
}
get _storeObj() {
return ZenSpacesSyncStore;
}
get _trackerObj() {
return ZenSpacesSyncTracker;
}
get _recordObj() {
return ZenSpacesSyncRecord;
}
get version() {
return 1;
}
get syncPriority() {
return 8;
}
get allowSkippedRecord() {
// A record that persistently fails to upload is skipped; the snapshot
// diff retries it on every following sync anyway.
return true;
}
async getChangedIDs() {
return ZenSpacesSyncModel.computeChangedIDs();
}
async _reconcile(item) {
// Incoming records always apply, and any local divergence re-uploads afterwards
// (see ZenSpacesSyncModel.noteApplied). The base reconciliation would
// instead drop incoming changes whenever the id is also in the local
// changed set, because the snapshot diff stamps its changes with "now"
// and the local side would always win the age comparison.
//
// An id we accept must also leave the outgoing set. Projections only
// reflect the applied state after the delayed session collection, so
// uploading the id later in this same sync would overwrite the record
// we just accepted with the stale pre-apply state (and the other
// device would then apply that, endlessly trading states back and
// forth). Divergence that survives the collection re-uploads on a
// later sync through the snapshot diff.
this._modified.delete(item.id);
return true;
}
async trackRemainingChanges() {}
async _onRecordsWritten(succeeded) {
ZenSpacesSyncModel.markUploaded(succeeded);
}
}

View File

@@ -0,0 +1,915 @@
/* 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 {
canonicalJSON,
LAYOUT_RECORD_ID,
RECORD_KINDS,
syncableIconUrl,
syncLog,
ZenSpacesSyncModel,
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
ZenLiveFoldersManager:
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
ContextualIdentityService:
"moz-src:///toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs",
});
const FIRST_SYNC_ANIMATION_PREF = "zen.spaces-sync.first-sync-animation-shown";
/**
* Applies batches of incoming Spaces records to the local browser.
*/
class nsZenSpacesSyncApplier {
#itemIn(win, id) {
return id ? win.document.getElementById(id) : null;
}
/**
* Maps a record's container guid to the local userContextId. A guid with
* no local mapping (its container record hasn't applied here yet) throws,
* failing the record so it redelivers once the container exists .
*
* @param {?string} containerGuid
* @returns {number}
*/
#resolveContainerId(containerGuid) {
if (!containerGuid) {
return 0;
}
const mapped = ZenSpacesSyncModel.contextIdForGuid(containerGuid);
if (mapped === null) {
throw new Error(`unknown container guid ${containerGuid}`);
}
return mapped;
}
/**
* @param {Array} records - decrypted incoming records.
* @returns {Array<string>} ids of records that failed to apply.
*/
async applyBatch(records) {
const incoming = {
containers: [],
spaces: [],
tabs: [],
folders: [],
splits: [],
layout: null,
deleted: [],
};
const handled = new Set();
for (const record of records) {
if (typeof record.id !== "string" || !record.id) {
continue;
}
if (record.deleted) {
if (record.id !== LAYOUT_RECORD_ID) {
incoming.deleted.push({ key: record.id, record });
}
handled.add(record);
continue;
}
const data = record.cleartext?.data;
if (!data) {
continue;
}
const entry = { key: record.id, data, record };
switch (record.cleartext.kind) {
case RECORD_KINDS.CONTAINER:
incoming.containers.push(entry);
break;
case RECORD_KINDS.SPACE:
incoming.spaces.push(entry);
break;
case RECORD_KINDS.TAB:
incoming.tabs.push(entry);
break;
case RECORD_KINDS.FOLDER:
incoming.folders.push(entry);
break;
case RECORD_KINDS.SPLIT:
incoming.splits.push(entry);
break;
case RECORD_KINDS.LAYOUT:
incoming.layout = entry;
break;
default:
// Unknown kind (newer client): leave untouched and unacknowledged.
continue;
}
handled.add(record);
}
syncLog(
`incoming batch: ${incoming.containers.length} containers, ` +
`${incoming.spaces.length} spaces, ${incoming.tabs.length} tabs, ` +
`${incoming.folders.length} folders, ${incoming.splits.length} splits, ` +
`layout=${!!incoming.layout}, ${incoming.deleted.length} tombstones`
);
const failed = new Set();
const fail = (record, e) => {
failed.add(record.id);
console.error(`ZenSpacesSync: failed to apply ${record.id}:`, e);
};
const deletions = this.#applyContainers(incoming, fail);
const win = lazy.ZenWindowSync.firstSyncedWindow;
if (!win) {
// Report the records as failed so the engine redelivers the batch on a later sync.
const noWindow = new Error("no synced window to apply into");
for (const entry of [
...incoming.spaces,
...incoming.folders,
...incoming.tabs,
...incoming.splits,
...(incoming.layout ? [incoming.layout] : []),
...deletions,
]) {
fail(entry.record, noWindow);
}
} else {
await win.gZenWorkspaces.promiseInitialized;
this.#maybePlayFirstSyncAnimation(win);
const removals = this.#routeTombstones(win, deletions);
this.#deleteTabs(win, removals.tabs, fail);
this.#deleteSplits(win, removals.splits, fail);
await this.#applySpaces(win, incoming.spaces, fail);
await this.#applyFolders(win, incoming.folders, fail);
this.#applyTabs(win, incoming.tabs, fail);
this.#applySplits(win, incoming.splits, fail);
await this.#deleteFolders(win, removals.folders, fail);
await this.#deleteSpaces(win, removals.spaces, fail);
this.#applyOrdering(win, incoming, fail);
// Collect the session soon so the stored sidebar (and with it the
// sync projections) reflects the applied state instead of re-uploading
// the pre-apply one.
lazy.SessionSaver.runDelayed();
}
ZenSpacesSyncModel.invalidate();
for (const record of records) {
if (!handled.has(record) || failed.has(record.id)) {
continue;
}
ZenSpacesSyncModel.noteApplied(
record.id,
record.deleted ? null : record.cleartext
);
}
return [...failed];
}
/**
* Plays the update sweep animation over the window the first time
* incoming sync data lands on this profile.
*
* @param {Window} win
*/
#maybePlayFirstSyncAnimation(win) {
if (Services.prefs.getBoolPref(FIRST_SYNC_ANIMATION_PREF, false)) {
return;
}
Services.prefs.setBoolPref(FIRST_SYNC_ANIMATION_PREF, true);
win.gZenStartup.playWindowSweepAnimation();
}
/**
* Tombstones have no payload. Whatever local thing owns the id decides
* what kind of deletion this is. Ids with no local counterpart need no
* work at all.
*
* @param {Window} win
* @param {Array<{key: string, record: object}>} deletions
*/
#routeTombstones(win, deletions) {
const routed = { tabs: [], folders: [], splits: [], spaces: [] };
const spaces = win.gZenWorkspaces.getWorkspaces();
for (const entry of deletions) {
const el = this.#itemIn(win, entry.key);
if (win.gBrowser.isTab(el)) {
routed.tabs.push(entry);
} else if (el?.isZenFolder) {
routed.folders.push(entry);
} else if (el?.hasAttribute?.("split-view-group")) {
routed.splits.push(entry);
} else if (spaces.some(s => s.uuid === entry.key)) {
routed.spaces.push(entry);
}
}
if (deletions.length) {
const matched = new Set([
...routed.tabs,
...routed.folders,
...routed.splits,
...routed.spaces,
]);
syncLog("tombstones routed:", {
tabs: routed.tabs.map(e => e.key),
folders: routed.folders.map(e => e.key),
splits: routed.splits.map(e => e.key),
spaces: routed.spaces.map(e => e.key),
unmatched: deletions.filter(e => !matched.has(e)).map(e => e.key),
});
}
return routed;
}
/* Mark: containers */
#applyContainers(incoming, fail) {
for (const { key: guid, data, record } of incoming.containers) {
try {
if (!data.name) {
continue;
}
const mapped = ZenSpacesSyncModel.contextIdForGuid(guid);
const existing =
mapped !== null
? lazy.ContextualIdentityService.getPublicIdentityFromId(mapped)
: null;
if (existing) {
lazy.ContextualIdentityService.update(
mapped,
data.name,
data.icon,
data.color
);
} else {
const identity = lazy.ContextualIdentityService.create(
data.name,
data.icon,
data.color
);
ZenSpacesSyncModel.registerContainerGuid(
guid,
identity.userContextId
);
}
} catch (e) {
fail(record, e);
}
}
const rest = [];
for (const entry of incoming.deleted) {
const mapped = ZenSpacesSyncModel.contextIdForGuid(entry.key);
if (mapped === null) {
rest.push(entry);
continue;
}
try {
if (lazy.ContextualIdentityService.getPublicIdentityFromId(mapped)) {
lazy.ContextualIdentityService.remove(mapped);
}
ZenSpacesSyncModel.forgetContainerGuid(entry.key);
} catch (e) {
fail(entry.record, e);
}
}
return rest;
}
/* Mark: spaces */
async #applySpaces(win, spaces, fail) {
if (!spaces.length) {
return;
}
const list = win.gZenWorkspaces.getWorkspaces();
let changed = false;
const repaint = [];
for (const { data, record } of spaces) {
try {
if (!data.uuid) {
continue;
}
const fields = {
uuid: data.uuid,
name: data.name ?? "",
icon: data.icon ?? undefined,
theme: data.theme ?? null,
containerTabId: this.#resolveContainerId(data.containerGuid),
};
// A space record re-syncs whenever its child ordering changes, so
// only propagate (and especially repaint) when its own fields did.
const index = list.findIndex(s => s.uuid === data.uuid);
const current = index === -1 ? null : list[index];
const visualChanged =
!current ||
(current.name ?? "") !== fields.name ||
(current.icon ?? null) !== (fields.icon ?? null) ||
canonicalJSON(current.theme ?? null) !==
canonicalJSON(fields.theme ?? null);
const containerChanged =
!current || (current.containerTabId ?? 0) !== fields.containerTabId;
if (containerChanged) {
syncLog(
`space ${data.uuid} containerTabId ` +
`${current?.containerTabId ?? 0} -> ${fields.containerTabId} ` +
`(guid ${data.containerGuid ?? "none"})`
);
}
if (!current) {
list.push(fields);
changed = true;
} else if (visualChanged || containerChanged) {
list[index] = { ...current, ...fields };
changed = true;
}
if (visualChanged) {
repaint.push(data.uuid);
}
} catch (e) {
fail(record, e);
}
}
if (changed) {
await win.gZenWorkspaces.propagateWorkspaces(list);
win.gZenWindowSync.propagateWorkspacesToAllWindows(list);
for (const uuid of repaint) {
const updated = list.find(s => s.uuid === uuid);
if (updated) {
win.gZenThemePicker.onWorkspaceChange(updated);
}
}
}
}
async #deleteSpaces(win, removals, fail) {
for (const { key: uuid, record } of removals) {
try {
const spaces = win.gZenWorkspaces.getWorkspaces();
if (!spaces.some(s => s.uuid === uuid)) {
continue;
}
if (spaces.length <= 1) {
// Never delete the last space; the local copy revives it remotely.
continue;
}
if (!(await this.#confirmRemoteSpaceDelete(win, uuid))) {
continue;
}
await win.gZenWorkspaces.removeWorkspace(uuid);
} catch (e) {
fail(record, e);
}
}
}
/**
* @param {Window} win
* @param {string} uuid
* @returns {Promise<boolean>} Whether the user confirmed the deletion.
*/
async #confirmRemoteSpaceDelete(win, uuid) {
const name = win.gZenWorkspaces.getWorkspaceFromId(uuid)?.name || uuid;
const [title, body] = await win.document.l10n.formatValues([
{ id: "zen-workspaces-remote-delete-title" },
{ id: "zen-workspaces-remote-delete-body", args: { name } },
]);
const result = await Services.prompt.asyncConfirmEx(
win.browsingContext,
Services.prompt.MODAL_TYPE_WINDOW,
title,
body,
Services.prompt.STD_YES_NO_BUTTONS,
null,
null,
null,
null,
false
);
return result.get("buttonNumClicked") === 0;
}
/* Mark: folders */
async #applyFolders(win, folders, fail) {
// Parents before children so nesting targets exist.
const depths = new Map(folders.map(f => [f.key, f.data.parentFolderId]));
const depthOf = key => {
let depth = 0;
let parent = depths.get(key);
while (parent && depths.has(parent) && depth < 10) {
depth++;
parent = depths.get(parent);
}
return depth;
};
const ordered = [...folders].sort(
(a, b) => depthOf(a.key) - depthOf(b.key)
);
for (const { key: folderId, data, record } of ordered) {
try {
let folder = this.#itemIn(win, folderId);
if (!folder?.isZenFolder) {
folder = win.gZenFolders.createFolder([], {
id: folderId,
label: data.name || "Folder",
workspaceId:
data.workspaceUuid || win.gZenWorkspaces.activeWorkspace,
isLiveFolder: !!data.live,
});
} else if (data.name && folder.label !== data.name) {
folder.label = data.name;
}
if (data.live && !folder.isLiveFolder) {
// The folder synced before its provider config was available and
// materialized as a plain folder; upgrade it so this profile
// projects the config too instead of clobbering it with live: null.
folder.isLiveFolder = true;
}
if ((folder.iconURL || null) !== (data.icon || null)) {
win.gZenFolders.setFolderUserIcon(folder, data.icon || null);
// Window sync picks this up and mirrors the icon everywhere else.
folder.dispatchEvent(
new win.CustomEvent("TabGroupUpdate", { bubbles: true })
);
}
const desiredParent = data.parentFolderId
? this.#itemIn(win, data.parentFolderId)
: null;
const currentParent = folder.group;
if ((currentParent?.id || null) !== (data.parentFolderId || null)) {
if (desiredParent?.isZenFolder) {
win.gBrowser.zenHandleTabMove(folder, () => {
if (desiredParent.tabs.length) {
desiredParent.tabs[0].after(folder);
} else {
desiredParent.appendChild(folder);
}
});
} else if (!data.parentFolderId && currentParent?.isZenFolder) {
const ws = data.workspaceUuid || win.gZenWorkspaces.activeWorkspace;
const container =
win.gZenWorkspaces.workspaceElement(ws)?.pinnedTabsContainer;
if (container) {
win.gBrowser.zenHandleTabMove(folder, () => {
container.insertBefore(
folder,
container.querySelector(".pinned-tabs-container-separator")
);
});
}
}
}
if (
data.workspaceUuid &&
folder.getAttribute("zen-workspace-id") !== data.workspaceUuid
) {
if (!folder.group) {
win.gZenFolders.changeFolderToSpace(folder, data.workspaceUuid);
} else {
folder.setAttribute("zen-workspace-id", data.workspaceUuid);
}
}
if (data.live) {
await lazy.ZenLiveFoldersManager.adoptSyncedFolder(
folderId,
data.live
);
}
} catch (e) {
fail(record, e);
}
}
}
async #deleteFolders(win, removals, fail) {
for (const { key: folderId, record } of removals) {
try {
const folder = this.#itemIn(win, folderId);
if (!folder?.isZenFolder) {
continue;
}
// Members without their own tombstone survive: unpack, then delete
// the (now empty) folder.
await folder.unpackTabs();
await folder.delete();
} catch (e) {
fail(record, e);
}
}
}
/* Mark: tabs */
#applyTabs(win, tabs, fail) {
for (const { key: tabId, data, record } of tabs) {
try {
if (!data.url || data.url === "about:blank") {
continue;
}
const existing = this.#itemIn(win, tabId);
if (win.gBrowser.isTab(existing)) {
this.#updateTab(win, existing, data);
} else {
this.#createTab(win, tabId, data);
}
} catch (e) {
fail(record, e);
}
}
}
#createTab(win, tabId, data) {
const userContextId = this.#resolveContainerId(data.containerGuid);
syncLog(
`creating tab ${tabId} ` +
`(essential=${!!data.essential}, container=${userContextId})`,
data.url
);
const tab = win.gBrowser.addTrustedTab(data.url, {
createLazyBrowser: true,
inBackground: true,
skipAnimation: true,
skipBackgroundNotify: true,
lazyTabTitle: data.title || undefined,
userContextId,
});
// Setting the sync id before the queued TabOpen handler runs makes
// window sync treat this tab as already replicated.
tab.id = tabId;
tab._zenContentsVisible = true;
this.#updateTabIdentity(win, tab, data);
if (data.essential) {
win.gZenPinnedTabManager.addToEssentials(tab, { replicating: true });
} else {
if (data.workspaceUuid) {
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
}
win.gBrowser.pinTab(tab);
if (data.workspaceUuid) {
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
}
const folder = data.folderId && this.#itemIn(win, data.folderId);
if (folder?.isZenFolder) {
folder.addTabs([tab]);
}
}
lazy.ZenWindowSync.on_TabOpen({ target: tab }, { ignoreExistingId: true });
}
/**
* Reconciles the tab's synced identity: pinned initial state, static
* label/icon, the live icon and the default-container marker.
*
* @param {Window} win
* @param {MozTabbrowserTab} tab
* @param {object} data
*/
#updateTabIdentity(win, tab, data) {
const icon = syncableIconUrl(data.icon);
const initial = tab._zenPinnedInitialState;
const identityChanged =
initial?.entry?.url !== data.url ||
(initial?.entry?.title || "") !== (data.title || "");
if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) {
lazy.ZenWindowSync.setPinnedInitialState(
tab,
{ url: data.url, title: data.title || "" },
icon || undefined
);
}
if (identityChanged && !tab.linkedPanel) {
// An unloaded tab has no live state to lose: retarget its lazy session
// state so the next activation loads the new canonical url directly
// instead of whatever the tab was created with.
this.#retargetUnloadedTab(win, tab, data, initial?.entry?.url);
}
const staticLabel =
typeof data.staticLabel === "string" ? data.staticLabel : null;
const localLabel =
typeof tab.zenStaticLabel === "string" ? tab.zenStaticLabel : null;
if (staticLabel !== localLabel) {
tab.zenStaticLabel = staticLabel ?? undefined;
if (staticLabel) {
tab._zenChangeLabelFlag = true;
try {
win.gBrowser._setTabLabel(tab, staticLabel);
} finally {
delete tab._zenChangeLabelFlag;
}
}
}
tab.zenStaticIcon = data.hasStaticIcon && icon ? icon : undefined;
const iconDiffers =
icon && syncableIconUrl(tab.getAttribute("image")) !== icon;
if (iconDiffers && (data.hasStaticIcon || !tab.linkedPanel)) {
try {
syncLog(
`setting synced${data.hasStaticIcon ? " static" : ""} ` +
`icon on tab ${tab.id}`
);
win.gBrowser.setIcon(tab, icon);
lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, {
image: null,
});
} catch (e) {
console.error("ZenSpacesSync: failed to set tab icon", e);
}
}
if (data.defaultContainer) {
tab.setAttribute("zenDefaultUserContextId", "true");
} else {
tab.removeAttribute("zenDefaultUserContextId");
}
}
/**
* Points an unloaded tab's session state at the record's canonical url,
* mirroring ZenPinnedTabManager's reset-to-stored-state shape.
*
* @param {Window} win
* @param {MozTabbrowserTab} tab
* @param {object} data
* @param {?string} previousPinUrl - The pin url before this record applied.
*/
#retargetUnloadedTab(win, tab, data, previousPinUrl) {
try {
const state = JSON.parse(win.SessionStore.getTabState(tab));
const entries = state.entries || [];
let currentUrl = null;
if (entries.length) {
const index = Math.min(
Math.max((state.index || entries.length) - 1, 0),
entries.length - 1
);
currentUrl = entries[index]?.url || null;
}
if (
currentUrl === data.url ||
(currentUrl && previousPinUrl && currentUrl !== previousPinUrl)
) {
return;
}
state.entries = [
{
url: data.url,
title: data.title || "",
triggeringPrincipal_base64: lazy.E10SUtils.serializePrincipal(
Services.scriptSecurityManager.createContentPrincipal(
Services.io.newURI(data.url),
{}
)
),
},
];
state.index = 1;
state.image = syncableIconUrl(data.icon) || undefined;
delete state.scroll;
win.SessionStore.setTabState(tab, state);
} catch (e) {
console.error("ZenSpacesSync: failed to retarget unloaded tab", e);
}
}
/**
* Puts a tab or split group under the folder the record names, or back at
* top level when it names none.
*
* @param {Window} win
* @param {MozTabbrowserTab|MozTabbrowserTabGroup} item
* @param {?string} folderId
*/
#applyFolderMembership(win, item, folderId) {
const currentFolder = item.group?.isZenFolder ? item.group.id : null;
const wantFolder = folderId || null;
if (currentFolder === wantFolder) {
return;
}
const target = wantFolder && this.#itemIn(win, wantFolder);
if (target?.isZenFolder) {
target.addTabs([item]);
} else if (!wantFolder && currentFolder) {
// ungroupTab pops a single nesting level. An item inside a subfolder
// lands inside the parent folder, so keep going until it is actually
// top-level.
while (win.gBrowser.isTabGroup(item.group) && item.group.isZenFolder) {
win.gBrowser.ungroupTab(item);
}
}
}
#updateTab(win, tab, data) {
this.#updateTabIdentity(win, tab, data);
const isEssential = tab.hasAttribute("zen-essential");
if (data.essential && !isEssential) {
syncLog(`incoming record promotes tab ${tab.id} to essential`);
win.gZenPinnedTabManager.addToEssentials(tab, { replicating: true });
return;
}
if (!data.essential && isEssential) {
console.warn(
`ZenSpacesSync: incoming record demotes essential tab ${tab.id}`
);
win.gZenPinnedTabManager.removeEssentials(tab, /* unpin */ false);
}
if (data.essential) {
return;
}
const inSplit = tab.group?.hasAttribute("split-view-group");
if (inSplit) {
// The split record governs placement of its members.
return;
}
this.#applyFolderMembership(win, tab, data.folderId);
if (
!data.folderId &&
data.workspaceUuid &&
tab.getAttribute("zen-workspace-id") !== data.workspaceUuid
) {
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
}
}
#removeTab(win, tab) {
if (tab.splitView) {
win.gZenViewSplitter.removeTabFromGroup(tab, undefined, {
forUnsplit: true,
changeTab: false,
});
}
win.gBrowser.removeTab(tab, { animate: true });
}
#deleteTabs(win, removals, fail) {
for (const { key: tabId, record } of removals) {
try {
const tab = this.#itemIn(win, tabId);
if (win.gBrowser.isTab(tab)) {
if (tab.hasAttribute("zen-essential")) {
console.warn(
`ZenSpacesSync: incoming tombstone removes essential tab ${tabId}`,
tab.linkedBrowser?.currentURI?.spec ?? ""
);
} else {
syncLog(`incoming tombstone removes tab ${tabId}`);
}
this.#removeTab(win, tab);
}
} catch (e) {
fail(record, e);
}
}
}
/* Mark: splits */
#applySplits(win, splits, fail) {
for (const { key: splitId, data, record } of splits) {
try {
const members = (data.tabs || [])
.map(id => this.#itemIn(win, id))
.filter(tab => win.gBrowser.isTab(tab));
const existing = this.#itemIn(win, splitId);
const hasGroup = existing?.hasAttribute?.("split-view-group");
if (hasGroup) {
const wanted = new Set(data.tabs || []);
for (const tab of [...existing.tabs]) {
if (tab.id && !wanted.has(tab.id)) {
win.gZenViewSplitter.removeTabFromGroup(tab, undefined, {
changeTab: false,
});
}
}
}
if (members.length >= 2) {
win.gZenViewSplitter.splitTabs(members, data.gridType, -1, {
groupFetchId: splitId,
});
}
const group = this.#itemIn(win, splitId);
if (group?.hasAttribute?.("split-view-group")) {
this.#applyFolderMembership(win, group, data.folderId);
}
} catch (e) {
fail(record, e);
}
}
}
#deleteSplits(win, removals, fail) {
for (const { key: splitId, record } of removals) {
try {
const index = win.gZenViewSplitter._data.findIndex(
group => group.groupId === splitId
);
if (index >= 0) {
win.gZenViewSplitter.removeGroup(index);
}
} catch (e) {
fail(record, e);
}
}
}
/* Mark: ordering */
#applyRelativeOrder(win, elements) {
let prev = null;
for (const el of elements) {
if (!el) {
continue;
}
if (
prev &&
prev.parentNode &&
prev.parentNode === el.parentNode &&
prev.nextElementSibling !== el
) {
win.gBrowser.zenHandleTabMove(el, () => prev.after(el));
}
prev = el;
}
}
/**
* Ordering runs in the first synced window only: every move goes through
* zenHandleTabMove, whose TabMove events window sync replicates into the
* other windows.
*
* @param {Window} win
* @param {object} incoming
* @param {Function} fail
*/
#applyOrdering(win, incoming, fail) {
for (const { key, data, record } of [
...incoming.spaces,
...incoming.folders,
]) {
try {
if (Array.isArray(data.children) && data.children.length) {
// A folder's empty-tab placeholder anchors its first child: without
// it, relative ordering could never move anything to the start.
const folder = data.folderId ? this.#itemIn(win, key) : null;
const start = folder?.isZenFolder
? folder.tabs.find(t => t.hasAttribute("zen-empty-tab"))
: null;
this.#applyRelativeOrder(win, [
start ?? null,
...data.children.map(id => this.#itemIn(win, id)),
]);
}
} catch (e) {
fail(record, e);
}
}
if (!incoming.layout) {
return;
}
const { data, record } = incoming.layout;
try {
for (const tabIds of Object.values(data.essentials || {})) {
if (Array.isArray(tabIds) && tabIds.length > 1) {
this.#applyRelativeOrder(
win,
tabIds.map(id => this.#itemIn(win, id))
);
}
}
if (Array.isArray(data.spaces) && data.spaces.length > 1) {
const current = win.gZenWorkspaces.getWorkspaces();
const byUuid = new Map(current.map(s => [s.uuid, s]));
const ordered = data.spaces
.map(uuid => byUuid.get(uuid))
.filter(Boolean);
for (const space of current) {
if (!data.spaces.includes(space.uuid)) {
ordered.push(space);
}
}
const changedOrder = ordered.some(
(space, i) => current[i]?.uuid !== space.uuid
);
if (changedOrder && ordered.length === current.length) {
win.gZenWindowSync.propagateWorkspacesToAllWindows(ordered);
}
}
} catch (e) {
fail(record, e);
}
}
}
export const ZenSpacesSyncApplier = new nsZenSpacesSyncApplier();

View File

@@ -0,0 +1,722 @@
/* 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 { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ZenLiveFoldersManager:
"resource:///modules/zen/ZenLiveFoldersManager.sys.mjs",
ContextualIdentityService:
"moz-src:///toolkit/components/contextualidentity/ContextualIdentityService.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"syncDebug",
"zen.spaces-sync.debug",
false
);
/**
* Debug logging for the whole Spaces sync pipeline.
*
* @param {string} message
* @param {...any} args
*/
export function syncLog(message, ...args) {
if (lazy.syncDebug) {
// eslint-disable-next-line no-console
console.debug(`ZenSpacesSync: ${message}`, ...args);
}
}
export const SIDEBAR_COLLECTED_TOPIC = "zen-sidebar-data-collected";
export const RECORD_KINDS = Object.freeze({
CONTAINER: "container",
SPACE: "space",
TAB: "tab",
FOLDER: "folder",
SPLIT: "split",
LAYOUT: "layout",
});
export const LAYOUT_RECORD_ID = "layout";
// Firefox ships four built-in containers with fixed userContextIds. They
// exist on every device, so they map to well-known guids instead of the
// per-profile generated ones.
const BUILTIN_CONTAINER_MAX = 4;
const BUILTIN_GUID_PREFIX = "builtin-";
const STORE_FILE_NAME = "zen-spaces-sync.json";
const STORE_VERSION = 1;
// Everything setIcon accepts without a loading principal.
const LOCAL_ICON_PROTOCOLS = ["data:", "chrome:", "about:", "resource:"];
/**
* Normalizes a tab icon for syncing. The image attribute can hold a
* moz-remote-image: wrapper around the actual (svg data:) favicon, carrying
* a per-process contentParentId that means nothing on another device — sync
* the wrapped URL instead. Anything else setIcon would reject as remote is
* dropped.
*
* @param {?string} icon
* @returns {string}
*/
export function syncableIconUrl(icon) {
if (!icon || typeof icon !== "string") {
return "";
}
if (icon.startsWith("moz-remote-image:")) {
try {
const uri = Services.io.newURI(icon);
icon = new URLSearchParams(uri.query).get("url") || "";
} catch (e) {
return "";
}
}
return LOCAL_ICON_PROTOCOLS.some(protocol => icon.startsWith(protocol))
? icon
: "";
}
function sortedClone(value) {
if (Array.isArray(value)) {
return value.map(sortedClone);
}
if (value && typeof value === "object") {
const out = {};
for (const key of Object.keys(value).sort()) {
out[key] = sortedClone(value[key]);
}
return out;
}
return value === undefined ? null : value;
}
/**
* Deterministic JSON serialization (recursively sorted keys), so that two
* structurally equal payloads always stringify identically.
*
* @param {object} value
*/
export function canonicalJSON(value) {
return JSON.stringify(sortedClone(value));
}
/**
* Digest of a record's canonical form. The uploaded-state snapshot only ever
* needs equality checks, so it stores these short digests instead of the
* full canonical strings, keeping the store file small and cheap to write.
*
* @param {string} kind
* @param {object} data
* @returns {string} base64 SHA-256 of the canonical record payload.
*/
const textEncoder = new TextEncoder();
export function recordDigest(kind, data) {
const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
Ci.nsICryptoHash
);
hasher.init(Ci.nsICryptoHash.SHA256);
const bytes = textEncoder.encode(canonicalJSON({ kind, data }));
hasher.update(bytes, bytes.length);
return hasher.finish(/* base64 = */ true);
}
/**
* Owns the synced-data model for the Spaces engine.
*/
class nsZenSpacesSyncModel {
#file = null;
#cache = null;
#data() {
if (!this.#file) {
this.#file = new JSONFile({
path: PathUtils.join(PathUtils.profileDir, STORE_FILE_NAME),
dataPostProcessor: data => {
if (data.version !== STORE_VERSION) {
// Drop the uploaded snapshot so everything re-diffs against
// the (also wiped) server, but keep the container identity mappings.
data.uploaded = {};
}
data.version = STORE_VERSION;
data.uploaded ||= {};
data.containers ||= {};
// The container map used to be keyed by guid. It is keyed by
// userContextId now so that registering a guid replaces any
// previous one for the same container. Flip old stores over
// (old values are numeric ids, new values are guid strings).
for (const [key, value] of Object.entries(data.containers)) {
if (typeof value === "number") {
delete data.containers[key];
data.containers[value] = key;
}
}
return data;
},
});
this.#file.ensureDataReady();
}
return this.#file.data;
}
invalidate() {
this.#cache = null;
}
/* Mark: container identity */
guidForContextId(userContextId, { create = false } = {}) {
const id = Number(userContextId);
if (!Number.isSafeInteger(id) || id <= 0) {
return null;
}
if (id <= BUILTIN_CONTAINER_MAX) {
return `${BUILTIN_GUID_PREFIX}${id}`;
}
const data = this.#data();
const existing = data.containers[id];
if (existing) {
return existing;
}
if (!create) {
return null;
}
const guid = Services.uuid.generateUUID().toString().slice(1, -1);
data.containers[id] = guid;
this.#file.saveSoon();
return guid;
}
contextIdForGuid(guid) {
if (typeof guid !== "string" || !guid) {
return null;
}
if (guid.startsWith(BUILTIN_GUID_PREFIX)) {
const id = Number(guid.slice(BUILTIN_GUID_PREFIX.length));
return Number.isSafeInteger(id) && id > 0 && id <= BUILTIN_CONTAINER_MAX
? id
: null;
}
for (const [id, mapped] of Object.entries(this.#data().containers)) {
if (mapped === guid) {
return Number(id);
}
}
return null;
}
/**
* Adopts an incoming guid as the identity's synced name.
*
* @param {string} guid
* @param {number} userContextId
*/
registerContainerGuid(guid, userContextId) {
if (guid.startsWith(BUILTIN_GUID_PREFIX)) {
return;
}
this.#data().containers[userContextId] = guid;
this.#file.saveSoon();
}
forgetContainerGuid(guid) {
const data = this.#data();
for (const [id, mapped] of Object.entries(data.containers)) {
if (mapped === guid) {
delete data.containers[id];
this.#file.saveSoon();
return;
}
}
}
/* Mark: projections */
#isSyncableTab(tabData) {
return !!(
tabData &&
tabData.zenSyncId &&
(tabData.pinned || tabData.zenEssential) &&
!tabData.zenIsEmpty &&
!tabData.zenIsGlance &&
!tabData.zenLiveFolderItemId
);
}
#tabIdentity(tabData) {
const initial = tabData._zenPinnedInitialState;
let url = initial?.entry?.url;
let title = initial?.entry?.title;
if (!url || url === "about:blank") {
const entries = tabData.entries || [];
if (entries.length) {
let index = Math.min(
Math.max((tabData.index || entries.length) - 1, 0),
entries.length - 1
);
const entry = entries[index] || entries[0];
if (entry?.url && entry.url !== "about:blank") {
url = entry.url;
title ??= entry.title;
}
}
}
if (!url || url === "about:blank") {
return null;
}
const icon = syncableIconUrl(
(tabData.zenHasStaticIcon
? tabData.image
: initial?.image || tabData.image) || ""
);
return { url, title: title || "", icon };
}
/**
* Builds the ordered child id list for one parent (a space's pinned
* section or a folder). Tabs and splits are ordered by their position in
* the collected tab list; folders are spliced in next to their recorded
* previous sibling.
*
* @param {object} root0
* @param {Array<object>} root0.tabs
* @param {Array<object>} root0.folders
* @param {Set<string>} root0.splitIds
* @param {Map<string, ?string>} root0.splitParents
* @param {Map<string, ?string>} root0.splitWs
* @param {object} root0.scope
*/
#childSequence({ tabs, folders, splitIds, splitParents, splitWs, scope }) {
const seq = [];
const seenSplits = new Set();
for (const tab of tabs) {
const groupId = tab.groupId || null;
if (groupId && splitIds.has(groupId)) {
if (
!seenSplits.has(groupId) &&
scope.matchSplit(
splitParents.get(groupId) ?? null,
splitWs.get(groupId) ?? null
)
) {
seenSplits.add(groupId);
seq.push(groupId);
}
continue;
}
if (scope.matchTab(tab, groupId)) {
seq.push(tab.zenSyncId);
}
}
for (const folder of folders) {
if (!scope.matchFolder(folder)) {
continue;
}
let index = seq.length;
const prev = folder.prevSiblingInfo;
if (!prev || prev.type === "start") {
// No stored sibling means the folder is the first child of its
// container (the section rebuild on startup places content before
// the fake start element, so the first item has no sibling at all).
index = 0;
} else if (prev.id) {
const at = seq.indexOf(prev.id);
if (at !== -1) {
index = at + 1;
} else if (prev.type === "tab") {
// An unsynced tab sibling (empty placeholders, live folder items)
// always sits at the start of its container.
index = 0;
}
}
seq.splice(index, 0, folder.id);
}
return seq;
}
/**
* Derives the shared projection context from one sidebar collection.
* Syncable tabs, folders, the syncable splits and their parents and
* workspaces, plus the folder-membership resolver.
*
* @param {object} sidebar
*/
#projectionContext(sidebar) {
const tabs = (sidebar.tabs || []).filter(t => this.#isSyncableTab(t));
const folders = (sidebar.folders || []).filter(
f => f?.id && !f.splitViewGroup
);
const folderIds = new Set(folders.map(f => f.id));
const allSplits = (sidebar.splitViewData || []).filter(g => g?.groupId);
const splitParents = new Map();
for (const entry of sidebar.folders || []) {
if (entry?.splitViewGroup && entry.id) {
splitParents.set(entry.id, entry.parentId || null);
}
}
// Only splits whose members are all synced tabs are synced themselves.
const syncableTabIds = new Set(tabs.map(t => t.zenSyncId));
const splits = allSplits.filter(
g =>
Array.isArray(g.tabs) &&
g.tabs.length >= 2 &&
g.tabs.every(id => syncableTabIds.has(id))
);
const splitIds = new Set(splits.map(g => g.groupId));
const splitWs = new Map();
for (const tab of tabs) {
const groupId = tab.groupId || null;
if (groupId && splitIds.has(groupId) && !splitWs.has(groupId)) {
splitWs.set(groupId, tab.zenWorkspace || null);
}
}
const folderOf = groupId => {
if (!groupId) {
return null;
}
if (splitIds.has(groupId)) {
return splitParents.get(groupId) || null;
}
return folderIds.has(groupId) ? groupId : null;
};
return { tabs, folders, splits, splitIds, splitParents, splitWs, folderOf };
}
/**
* @param {Map} map
* @param {object} ctx - The projection context.
*/
#projectTabs(map, ctx) {
for (const tab of ctx.tabs) {
const identity = this.#tabIdentity(tab);
if (!identity) {
continue;
}
const essential = !!tab.zenEssential;
map.set(tab.zenSyncId, {
kind: RECORD_KINDS.TAB,
data: {
tabId: tab.zenSyncId,
url: identity.url,
title: identity.title,
icon: identity.icon,
containerGuid: this.guidForContextId(tab.userContextId, {
create: true,
}),
essential,
workspaceUuid: essential ? null : tab.zenWorkspace || null,
folderId: ctx.folderOf(tab.groupId || null),
staticLabel:
typeof tab.zenStaticLabel === "string" ? tab.zenStaticLabel : null,
hasStaticIcon: !!tab.zenHasStaticIcon,
defaultContainer: !!tab.zenDefaultUserContextId,
},
});
}
}
projections() {
const sidebar = lazy.ZenSessionStore.getSidebarData() || {};
const stamp = sidebar.lastCollected || 0;
if (this.#cache && this.#cache.stamp === stamp) {
return this.#cache.map;
}
const map = new Map();
const pending = new Set();
const ctx = this.#projectionContext(sidebar);
const { tabs, folders, splits, splitIds, splitParents, splitWs } = ctx;
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
if (!identity.name) {
continue;
}
const guid = this.guidForContextId(identity.userContextId, {
create: true,
});
if (!guid) {
continue;
}
map.set(guid, {
kind: RECORD_KINDS.CONTAINER,
data: {
guid,
name: identity.name,
icon: identity.icon || "",
color: identity.color || "",
},
});
}
const spaces = sidebar.spaces || [];
for (const space of spaces) {
if (!space?.uuid) {
continue;
}
const uuid = space.uuid;
map.set(uuid, {
kind: RECORD_KINDS.SPACE,
data: {
uuid,
name: space.name ?? "",
icon: space.icon ?? null,
theme: space.theme ?? null,
containerGuid: this.guidForContextId(space.containerTabId, {
create: true,
}),
children: this.#childSequence({
tabs,
folders,
splitIds,
splitParents,
splitWs,
scope: {
matchTab: (t, groupId) =>
!groupId &&
!t.zenEssential &&
(t.zenWorkspace || null) === uuid,
matchSplit: (parent, ws) => !parent && ws === uuid,
matchFolder: f => !f.parentId && (f.workspaceId || null) === uuid,
},
}),
},
});
}
for (const folder of folders) {
const fid = folder.id;
let live = null;
if (folder.isLiveFolder) {
try {
live = lazy.ZenLiveFoldersManager.getSyncableFolderData(fid);
} catch (e) {
console.error("ZenSpacesSync: failed to project live folder", e);
}
if (!live) {
pending.add(fid);
continue;
}
}
map.set(fid, {
kind: RECORD_KINDS.FOLDER,
data: {
folderId: fid,
name: folder.name ?? "",
icon: folder.userIcon || null,
workspaceUuid: folder.workspaceId || null,
parentFolderId: folder.parentId || null,
live,
children: this.#childSequence({
tabs,
folders,
splitIds,
splitParents,
splitWs,
scope: {
matchTab: (t, groupId) => groupId === fid,
matchSplit: parent => parent === fid,
matchFolder: f => f.parentId === fid,
},
}),
},
});
}
this.#projectTabs(map, ctx);
for (const split of splits) {
map.set(split.groupId, {
kind: RECORD_KINDS.SPLIT,
data: {
splitId: split.groupId,
gridType: split.gridType || "grid",
tabs: [...split.tabs],
workspaceUuid: splitWs.get(split.groupId) ?? null,
folderId: splitParents.get(split.groupId) || null,
},
});
}
if (spaces.length) {
const essentials = {};
for (const tab of tabs) {
if (!tab.zenEssential) {
continue;
}
const key =
this.guidForContextId(tab.userContextId, { create: true }) ||
"default";
(essentials[key] ||= []).push(tab.zenSyncId);
}
map.set(LAYOUT_RECORD_ID, {
kind: RECORD_KINDS.LAYOUT,
data: {
spaces: spaces.map(s => s.uuid).filter(Boolean),
essentials,
},
});
}
this.#cache = { stamp, map, pending };
return map;
}
/**
* Ids that are locally present but deliberately not projected this cycle
* (live folders whose provider config isn't available yet). The diff must
* not read their absence as a deletion.
*/
#pendingIds() {
this.projections();
return this.#cache.pending;
}
#digestCache = null;
/**
* Digests of all current projections. Change detection runs several times per
* sync cycle (tracker observe, changed-id computation, upload bookkeeping)
* and only needs to hash again after a fresh sidebar collection.
*/
#digestAll() {
const map = this.projections();
if (this.#digestCache?.map === map) {
return this.#digestCache.digests;
}
const digests = new Map();
for (const [id, projected] of map) {
digests.set(id, recordDigest(projected.kind, projected.data));
}
this.#digestCache = { map, digests };
return digests;
}
/* Mark: engine-facing API */
getAllRecordIds() {
const ids = {};
for (const id of this.projections().keys()) {
ids[id] = true;
}
return ids;
}
itemExists(id) {
return this.projections().has(id);
}
projectRecord(id) {
return this.projections().get(id) ?? null;
}
/**
* Changes = diff between the current projections and the last state the
* server acknowledged. Ids present locally with different content are
* modified; ids only present in the uploaded snapshot are deletions.
*/
computeChangedIDs() {
const uploaded = this.#data().uploaded;
const current = this.#digestAll();
const pending = this.#pendingIds();
const now = Date.now() / 1000;
const changes = {};
for (const [id, digest] of current) {
if (uploaded[id] !== digest) {
changes[id] = now;
}
}
for (const id of Object.keys(uploaded)) {
if (!current.has(id) && !pending.has(id)) {
changes[id] = now;
}
}
if (lazy.syncDebug && Object.keys(changes).length) {
const map = this.projections();
syncLog(
"outgoing diff:",
Object.keys(changes).map(id =>
current.has(id) ? `${map.get(id)?.kind} ${id}` : `TOMBSTONE ${id}`
)
);
}
return changes;
}
hasPendingChanges() {
const uploaded = this.#data().uploaded;
const current = this.#digestAll();
const pending = this.#pendingIds();
for (const [id, digest] of current) {
if (uploaded[id] !== digest) {
return true;
}
}
for (const id of Object.keys(uploaded)) {
if (!current.has(id) && !pending.has(id)) {
return true;
}
}
return false;
}
/**
* Called after records were successfully uploaded. Remember exactly what
* the server now holds for them.
*
* @param {Array<string>} ids
*/
markUploaded(ids) {
const data = this.#data();
const current = this.#digestAll();
for (const id of ids) {
if (current.has(id)) {
data.uploaded[id] = current.get(id);
} else {
delete data.uploaded[id];
}
}
if (lazy.syncDebug && ids.length) {
syncLog(
"server acknowledged upload:",
ids.map(id => (current.has(id) ? id : `${id} (tombstone)`))
);
}
this.#file.saveSoon();
}
/**
* Called after an incoming record was applied locally. Storing the
* incoming payload as the uploaded state means a faithful local
* materialization produces no re-upload, while a divergent one re-uploads
* the local truth (self-healing).
*
* @param {string} id
* @param {?object} cleartext - null for tombstones.
*/
noteApplied(id, cleartext) {
const data = this.#data();
if (!cleartext) {
delete data.uploaded[id];
} else {
data.uploaded[id] = recordDigest(cleartext.kind, cleartext.data);
}
syncLog(
`acknowledged incoming ${cleartext ? cleartext.kind : "tombstone"} ${id}`
);
this.#file.saveSoon();
}
}
export const ZenSpacesSyncModel = new nsZenSpacesSyncModel();

View File

@@ -1,160 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs",
});
function normalizeUserContextId(value) {
const normalized = typeof value === "string" ? Number(value) : value;
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
class ZenSyncManager {
getSidebarData() {
return lazy.ZenSessionStore.getSidebarData();
}
/**
* Whether to ignore changes to items. This is used to prevent
* infinite loops when applying incoming sync changes.
*
* @type {boolean}
*/
#ignoreChanges = false;
#changedItems = new Map();
markItemChanged(item) {
if (item.type && item.id && !this.#ignoreChanges) {
const key = `${item.type}~${item.id}`;
this.#changedItems.set(key, { type: item.type, id: item.id });
}
}
#getChangedItems() {
return Array.from(this.#changedItems.values());
}
#clearChangedItems() {
this.#changedItems.clear();
}
notifyAboutChanges() {
const changedItems = this.#getChangedItems();
for (const item of changedItems) {
Services.obs.notifyObservers(
{ wrappedJSObject: item },
"zen-workspace-item-changed"
);
}
this.#clearChangedItems();
}
async applyIncomingBatch(pulled, removals) {
try {
this.#ignoreChanges = true;
this.#applyIncomingContainers(
pulled.containers || [],
removals.containers || []
);
const win = lazy.ZenWindowSync.firstSyncedWindow;
if (win?.gZenWorkspaces) {
await win.gZenWorkspaces._applySyncChanges(pulled, removals);
}
} catch (e) {
console.error("ZenSyncManager: Failed to apply incoming sync data:", e);
throw e;
} finally {
this.#ignoreChanges = false;
}
}
#applyIncomingContainers(pulledContainers, removedContainers) {
const localContainersById = new Map(
lazy.ContextualIdentityService.getPublicIdentities().map(container => [
container.userContextId,
container,
])
);
for (const container of pulledContainers) {
if (!container.name) {
continue;
}
const userContextId = normalizeUserContextId(container.userContextId);
if (userContextId === null) {
console.warn(
"ZenSyncManager: Ignoring incoming container with invalid userContextId",
{ container }
);
continue;
}
const existsLocally = localContainersById.has(userContextId);
if (existsLocally) {
lazy.ContextualIdentityService.update(
userContextId,
container.name,
container.icon,
container.color
);
continue;
}
const createdIdentity = lazy.ContextualIdentityService.create(
container.name,
container.icon,
container.color,
userContextId
);
if (createdIdentity) {
localContainersById.set(createdIdentity.userContextId, createdIdentity);
}
if (createdIdentity && createdIdentity.userContextId !== userContextId) {
console.warn("ZenSyncManager: Container sync created unexpected ID", {
requestedId: userContextId,
createdId: createdIdentity.userContextId,
name: container.name,
});
}
}
for (const container of removedContainers) {
const userContextId = normalizeUserContextId(container.userContextId);
if (userContextId === null) {
console.warn(
"ZenSyncManager: Ignoring container removal with invalid userContextId",
{ container }
);
continue;
}
if (!localContainersById.has(userContextId)) {
continue;
}
try {
lazy.ContextualIdentityService.remove(userContextId);
localContainersById.delete(userContextId);
} catch {
// Container may already be gone locally.
}
}
}
}
export const ZenSyncStore = new ZenSyncManager();

View File

@@ -1,404 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
Store,
SyncEngine,
Tracker,
} from "resource://services-sync/engines.sys.mjs";
import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ZenSyncStore: "resource:///modules/zen/ZenSyncManager.sys.mjs",
ContextualIdentityService:
"resource://gre/modules/ContextualIdentityService.sys.mjs",
});
const RECORD_ID_PREFIX_BY_TYPE = Object.freeze({
space: "s",
container: "c",
});
const RECORD_TYPE_BY_PREFIX = Object.freeze({
s: "space",
c: "container",
});
/**
* Sync record wrapper for workspace and container items stored in the
* Workspaces engine collection.
*/
export class ZenWorkspacesRecord extends CryptoWrapper {
_logName = "Sync.Record.ZenSpaces";
}
ZenWorkspacesRecord.prototype.type = "spaces";
function parseRecordId(id) {
const sep = id.indexOf("~");
if (sep === -1) {
return null;
}
const prefix = id.slice(0, sep);
const key = id.slice(sep + 1);
return { type: RECORD_TYPE_BY_PREFIX[prefix] || prefix, key };
}
function createRecordId(type, id) {
const prefix = RECORD_ID_PREFIX_BY_TYPE[type];
if (!prefix) {
throw new Error(`Unknown Spaces Sync record type: ${type}`);
}
return `${prefix}~${id}`;
}
function normalizeUserContextId(value) {
const normalized = typeof value === "string" ? Number(value) : value;
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
/**
* Strips the sync-envelope fields (`id` and `type`) from incoming record data
* and restores the item's real identity key where needed
*
* @param {object} data
*/
function stripSyncFields(data) {
const rest = { ...data };
delete rest.id;
delete rest.type;
return rest;
}
/**
* Sync store implementation that serializes local workspace and container
* state into records and applies incoming remote changes.
*/
class ZenWorkspacesStore extends Store {
constructor(name, engine) {
super(name, engine);
}
async getAllIDs() {
const ids = {};
const sidebar = lazy.ZenSyncStore.getSidebarData();
for (const space of sidebar.spaces || []) {
if (space.uuid) {
ids[createRecordId("space", space.uuid)] = true;
}
}
for (const c of lazy.ContextualIdentityService.getPublicIdentities()) {
ids[createRecordId("container", c.userContextId)] = true;
}
return ids;
}
async itemExists(id) {
const parsed = parseRecordId(id);
if (!parsed) {
return false;
}
const sidebar = lazy.ZenSyncStore.getSidebarData();
switch (parsed.type) {
case "space":
return (sidebar.spaces || []).some(s => s.uuid === parsed.key);
case "container":
return lazy.ContextualIdentityService.getPublicIdentities().some(
c => String(c.userContextId) === parsed.key
);
default:
return false;
}
}
async createRecord(id, collection) {
const record = new ZenWorkspacesRecord(collection, id);
const parsed = parseRecordId(id);
if (!parsed) {
record.deleted = true;
return record;
}
const sidebar = lazy.ZenSyncStore.getSidebarData();
switch (parsed.type) {
case "space": {
const spaces = sidebar.spaces || [];
const idx = spaces.findIndex(s => s.uuid === parsed.key);
if (idx === -1) {
record.deleted = true;
return record;
}
const rest = { ...spaces[idx] };
delete rest.syncStatus;
record.cleartext = { id, type: "space", ...rest, position: idx };
break;
}
case "container": {
const container =
lazy.ContextualIdentityService.getPublicIdentities().find(
c => String(c.userContextId) === parsed.key
);
if (!container) {
record.deleted = true;
return record;
}
record.cleartext = {
id,
type: "container",
userContextId: container.userContextId,
name: container.name,
icon: container.icon,
color: container.color,
};
break;
}
default:
record.deleted = true;
}
return record;
}
async applyIncomingBatch(records, _countTelemetry) {
const pulled = { spaces: [], containers: [] };
const removals = { spaces: [], containers: [] };
for (const record of records) {
if (record.deleted) {
this._collectRemoval(record.id, removals);
continue;
}
const data = record.cleartext;
if (!data?.type) {
continue;
}
const clean = stripSyncFields(data);
switch (data.type) {
case "space":
pulled.spaces.push(clean);
break;
case "container":
pulled.containers.push(clean);
break;
}
}
// Suppress change tracking while applying incoming data to prevent
// feedback loops where applied items get re-uploaded immediately.
this.engine._tracker.ignoreAll = true;
try {
await lazy.ZenSyncStore.applyIncomingBatch(pulled, removals);
} finally {
this.engine._tracker.ignoreAll = false;
}
return [];
}
_collectRemoval(id, removals) {
const parsed = parseRecordId(id);
if (!parsed) {
return;
}
switch (parsed.type) {
case "space":
removals.spaces.push({ uuid: parsed.key });
break;
case "container": {
const userContextId = normalizeUserContextId(parsed.key);
if (userContextId === null) {
console.warn(
"ZenWorkspacesStore: Ignoring container removal with invalid userContextId",
{ id }
);
break;
}
removals.containers.push({ userContextId });
break;
}
}
}
async create(record) {
await this._applySingle(record);
}
async update(record) {
await this._applySingle(record);
}
async _applySingle(record) {
this.engine._tracker.ignoreAll = true;
try {
if (record.deleted) {
const removals = { spaces: [], containers: [] };
this._collectRemoval(record.id, removals);
await lazy.ZenSyncStore.applyIncomingBatch(
{ spaces: [], containers: [] },
removals
);
return;
}
const data = record.cleartext;
if (!data?.type) {
return;
}
const clean = stripSyncFields(data);
const pulled = { spaces: [], containers: [] };
switch (data.type) {
case "space":
pulled.spaces.push(clean);
break;
case "container":
pulled.containers.push(clean);
break;
}
await lazy.ZenSyncStore.applyIncomingBatch(pulled, {
spaces: [],
containers: [],
});
} finally {
this.engine._tracker.ignoreAll = false;
}
}
async remove() {
// No-op: never delete user data on wipe
}
async wipe() {
// No-op: never delete user data on wipe
}
changeItemID() {
// No-op
}
}
/**
* Sync tracker that watches workspace and contextual identity observers and
* marks the corresponding record IDs as changed.
*/
class ZenWorkspacesTracker extends Tracker {
#changedIDs = {};
#ignoreAll = false;
get ignoreAll() {
return this.#ignoreAll;
}
set ignoreAll(value) {
this.#ignoreAll = value;
}
onStart() {
Services.obs.addObserver(this, "zen-workspace-item-changed");
Services.obs.addObserver(this, "contextual-identity-created");
Services.obs.addObserver(this, "contextual-identity-updated");
Services.obs.addObserver(this, "contextual-identity-deleted");
}
onStop() {
Services.obs.removeObserver(this, "zen-workspace-item-changed");
Services.obs.removeObserver(this, "contextual-identity-created");
Services.obs.removeObserver(this, "contextual-identity-updated");
Services.obs.removeObserver(this, "contextual-identity-deleted");
}
observe(subject, topic, _data) {
if (this.#ignoreAll) {
return;
}
if (topic === "zen-workspace-item-changed") {
const type = subject?.wrappedJSObject?.type;
const id = subject?.wrappedJSObject?.id;
if (type && id) {
this._trackChange({ type, id });
}
} else if (topic.startsWith("contextual-identity-")) {
const id = subject?.wrappedJSObject?.userContextId;
if (id && normalizeUserContextId(id) !== null) {
this._trackChange({ type: "container", id });
}
}
}
_trackChange(data) {
if (data.type && data.id) {
const id = createRecordId(data.type, data.id);
this.#changedIDs[id] = Date.now() / 1000;
this.score += SCORE_INCREMENT_XLARGE;
}
}
async getChangedIDs() {
return { ...this.#changedIDs };
}
async addChangedID(id, when) {
this.#changedIDs[id] = when;
return true;
}
async removeChangedID(...ids) {
for (const id of ids) {
delete this.#changedIDs[id];
}
return true;
}
clearChangedIDs() {
this.#changedIDs = {};
}
}
/**
* Sync engine entrypoint that wires the Workspaces record, store, and tracker
* implementations into Firefox Sync.
*/
export class ZenWorkspacesEngine extends SyncEngine {
static get name() {
return "Spaces";
}
constructor(service) {
super("Spaces", service);
}
get _storeObj() {
return ZenWorkspacesStore;
}
get _trackerObj() {
return ZenWorkspacesTracker;
}
get _recordObj() {
return ZenWorkspacesRecord;
}
get version() {
return 2;
}
get syncPriority() {
return 8;
}
get allowSkippedRecord() {
return false;
}
}

View File

@@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.zen += [
"ZenSyncManager.sys.mjs",
"ZenWorkspacesSync.sys.mjs",
"ZenSpacesSync.sys.mjs",
"ZenSpacesSyncApplier.sys.mjs",
"ZenSpacesSyncModel.sys.mjs",
]

View File

@@ -503,7 +503,7 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
}
}
addToEssentials(tab) {
addToEssentials(tab, { replicating = false } = {}) {
// eslint-disable-next-line no-nested-ternary
const tabs = tab
? // if it's already an array, dont make it [tab]
@@ -518,7 +518,12 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
// eslint-disable-next-line no-shadow
let tab = tabs[i];
const section = gZenWorkspaces.getEssentialsSection(tab);
if (!this.canEssentialBeAdded(tab)) {
// canEssentialBeAdded gates user-initiated adds on the *active* space's
// container. A replicated add mirrors a decision already made in another
// window or on another device, always into the tab's own container
// section.
if (!replicating && !this.canEssentialBeAdded(tab)) {
this.log(`addToEssentials rejected for ${tab.id}`);
movedAll = false;
continue;
}
@@ -570,6 +575,13 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
for (let i = 0; i < tabs.length; i++) {
// eslint-disable-next-line no-shadow
const tab = tabs[i];
if (this._canLog) {
// eslint-disable-next-line no-console
console.trace(
`ZenPinnedTabManager: removing tab ${tab.id} from essentials ` +
`(unpin=${unpin})`
);
}
tab.removeAttribute("zen-essential");
if (
gZenWorkspaces.workspaceEnabled &&

View File

@@ -538,8 +538,7 @@
:root[zen-single-toolbar="true"]:not([zen-has-empty-tab="true"]) & {
& #urlbar:not([breakout-extend]) .urlbar-input-container {
padding-left: 8px;
padding-right: 4px;
padding-inline: 4px;
}
}
}

View File

@@ -3,6 +3,36 @@
# 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/.
[alerts]
source = "browser/base/content/test/alerts"
is_direct_path = true
[backforward]
source = "browser/base/content/test/backforward"
is_direct_path = true
[caps]
source = "browser/base/content/test/caps"
is_direct_path = true
[gesture]
source = "browser/base/content/test/gesture"
is_direct_path = true
[gesture.replace-manifest]
"[DEFAULT]" = '''[DEFAULT]
prefs = [
"zen.window-sync.enabled=false",
]'''
[linkHandling]
source = "browser/base/content/test/linkHandling"
is_direct_path = true
disable = [
# Glance opens with alt+click, firefox doesnt know that.
"browser_javascript_links.js",
]
[readermode]
source = "toolkit/components/reader/tests/browser"
is_direct_path = true
@@ -13,10 +43,88 @@ disable = [
"browser_readerMode_colorSchemePref.js",
]
[remote-settings]
source = "services/settings/test/unit"
is_direct_path = true
xpcshell = true
[remote-settings.replace-manifest]
"../../../" = "../../../../services/"
[safebrowsing]
source = "browser/components/safebrowsing/content/test"
is_direct_path = true
[services-crypto]
source = "services/crypto/tests/unit"
is_direct_path = true
xpcshell = true
[services-crypto.replace-manifest]
"../../../" = "../../../../services/"
[sessionstore]
source = "browser/components/sessionstore/test"
is_direct_path = true
disable = [
"browser_1933485_tab_groups_history.js",
"browser_1953801_tab_groups_history_close_other.js",
"browser_aboutSessionRestore.js",
"browser_bfcache_telemetry.js",
"browser_closedId.js",
"browser_closed_objects_changed_notifications_tabs.js",
"browser_closed_objects_changed_notifications_windows.js",
"browser_closed_tabs_closed_windows.js",
"browser_closed_tabs_windows.js",
"browser_cookies_partitioned.js",
"browser_crashedTabs.js",
"browser_dying_cache.js",
"browser_firefoxView_restore.js",
"browser_firefoxView_selected_restore.js",
"browser_forget_closed_tab_window_byId.js",
"browser_movePendingTabToNewWindow.js",
"browser_newtab_userTypedValue.js",
"browser_pinned_tabs.js",
"browser_purge_domaindata.js",
"browser_reopen_all_windows.js",
"browser_restoreLastActionCorrectOrder.js",
"browser_restoreLastClosedTabOrWindowOrSession.js",
"browser_restore_container_tabs_oa.js",
"browser_restore_tabless_window.js",
"browser_restore_verticalPinnedTabs.js",
"browser_restored_window_features.js",
"browser_scrollPositions.js",
"browser_scrollPositionsReaderMode.js",
"browser_searchModeSwitcher_restore.js",
"browser_speculative_connect.js",
"browser_splitview_integer_ids.js",
"browser_splitview_restore_in_closed_window.js",
"browser_splitview_string_migration.js",
"browser_swapDocShells.js",
"browser_tab_groups_restore_closed_in_closed_window.js",
"browser_tab_groups_restore_closed_in_open_window.js",
"browser_tab_groups_restore_closed_many_tabs.js",
"browser_tab_groups_closed.js",
"browser_tab_groups_restore_multiple.js",
"browser_tab_groups_restore_saved.js",
"browser_tab_groups_restore_simple.js",
"browser_tab_groups_save_on_removeAllTabsBut.js",
"browser_tab_groups_save_on_removeTabsToTheEnd.js",
"browser_tab_groups_save_on_removeTabsToTheStart.js",
"browser_tab_groups_save_on_window_close.js",
"browser_tab_groups_state.js",
"browser_tab_groups_undo.js",
"browser_tab_label_during_restore.js",
"browser_undoCloseById.js",
"browser_urlbarSearchMode.js",
"browser_windowStateContainer.js",
"browser_wireframe_basic.js",
]
[sessionstore.replace-manifest]
'prefs = [' = '''prefs = [
"zen.window-sync.enabled=false",'''
[shell]
source = "browser/components/shell/test"
is_direct_path = true
@@ -25,6 +133,15 @@ disable = [
"browser_setDesktopBackgroundPreview.js",
]
[sync]
source = "services/sync/tests/unit"
is_direct_path = true
xpcshell = true
[sync.replace-manifest]
"../../../" = "../../../../services/"
'"identity.fxaccounts.enabled=true"' = '"identity.fxaccounts.enabled=true", "browser.formfill.enable=true"'
[tabMediaIndicator]
source = "browser/components/tabbrowser/test/browser/tabMediaIndicator"
is_direct_path = true
@@ -34,3 +151,10 @@ source = "toolkit/components/tooltiptext"
disable = [
"browser_input_file_tooltips.js",
]
[zoom]
source = "browser/base/content/test/zoom"
is_direct_path = true
[zoom.replace-manifest]
"../general/" = "../../../../browser/base/content/test/general/"

View File

@@ -0,0 +1,32 @@
[DEFAULT]
support-files = [
"head.js",
"file_dom_notifications.html",
]
["browser_notification_close.js"]
https_first_disabled = true
skip-if = [
"os == 'win'", # Bug 1227785
]
["browser_notification_do_not_disturb.js"]
https_first_disabled = true
["browser_notification_open_settings.js"]
https_first_disabled = true
skip-if = [
"os == 'win'", # Bug 1411118
]
["browser_notification_remove_permission.js"]
https_first_disabled = true
skip-if = [
"os == 'win'", # Bug 1411118
]
["browser_notification_tab_switching.js"]
https_first_disabled = true
skip-if = [
"os == 'win'", # Bug 1243263
]

View File

@@ -0,0 +1,78 @@
"use strict";
const { PlacesTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PlacesTestUtils.sys.mjs"
);
const { PermissionTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/PermissionTestUtils.sys.mjs"
);
let notificationURL =
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
add_task(async function test_notificationClose() {
await addNotificationPermission(notificationURL);
await BrowserTestUtils.withNewTab(
{
gBrowser,
url: notificationURL,
},
async function dummyTabTask(aBrowser) {
await openNotification(aBrowser, "showNotification2");
info("Notification alert showing");
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
if (!alertWindow) {
ok(true, "Notifications don't use XUL windows on all platforms.");
await closeNotification(aBrowser);
return;
}
let alertTitleLabel =
alertWindow.document.getElementById("alertTitleLabel");
is(
alertTitleLabel.value,
"Test title",
"Title text of notification should be present"
);
let alertTextLabel =
alertWindow.document.getElementById("alertTextLabel");
is(
alertTextLabel.textContent,
"Test body 2",
"Body text of notification should be present"
);
let alertCloseButton = alertWindow.document.querySelector(".close-icon");
is(alertCloseButton.localName, "toolbarbutton", "close button found");
let promiseBeforeUnloadEvent = BrowserTestUtils.waitForEvent(
alertWindow,
"beforeunload"
);
let closedTime = alertWindow.Date.now();
alertCloseButton.click();
info("Clicked on close button");
await promiseBeforeUnloadEvent;
ok(true, "Alert should close when the close button is clicked");
let currentTime = alertWindow.Date.now();
// The notification will self-close at 12 seconds, so this checks
// that the notification closed before the timeout.
Assert.less(
currentTime - closedTime,
5000,
"Close requested at " +
closedTime +
", actually closed at " +
currentTime
);
}
);
});
add_task(async function cleanup() {
PermissionTestUtils.remove(notificationURL, "desktop-notification");
});

View File

@@ -0,0 +1,159 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests that notifications can be silenced using nsIAlertsDoNotDisturb
* on systems where that interface and its methods are implemented for
* the nsIAlertService.
*/
const ALERT_SERVICE = Cc["@mozilla.org/alerts-service;1"]
.getService(Ci.nsIAlertsService)
.QueryInterface(Ci.nsIAlertsDoNotDisturb);
const PAGE =
"https://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
// The amount of time in seconds that we will wait for a notification
// to show up before we decide that it's not coming.
const NOTIFICATION_TIMEOUT_SECS = 2000;
add_setup(async function () {
await addNotificationPermission(PAGE);
});
/**
* Test that the manualDoNotDisturb attribute can prevent
* notifications from appearing.
*/
add_task(async function test_manualDoNotDisturb() {
try {
// Only run the test if the do-not-disturb
// interface has been implemented.
ALERT_SERVICE.manualDoNotDisturb;
ok(true, "Alert service implements do-not-disturb interface");
} catch (e) {
ok(
true,
"Alert service doesn't implement do-not-disturb interface, exiting test"
);
return;
}
// In the event that something goes wrong during this test, make sure
// we put the attribute back to the default setting when this test file
// exits.
registerCleanupFunction(() => {
ALERT_SERVICE.manualDoNotDisturb = false;
});
// Make sure that do-not-disturb is not enabled before we start.
ok(
!ALERT_SERVICE.manualDoNotDisturb,
"Alert service should not be disabled when test starts"
);
await BrowserTestUtils.withNewTab(PAGE, async browser => {
await openNotification(browser, "showNotification2");
info("Notification alert showing");
let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
// For now, only the XUL alert backend implements the manualDoNotDisturb
// method for nsIAlertsDoNotDisturb, so we expect there to be a XUL alert
// window. If the method gets implemented by native backends in the future,
// we'll probably want to branch here and set the manualDoNotDisturb
// attribute manually.
ok(alertWindow, "Expected a XUL alert window.");
// We're using the XUL notification backend. This means that there's
// a menuitem for enabling manualDoNotDisturb. We exercise that
// menuitem here.
let doNotDisturbMenuItem = alertWindow.document.getElementById(
"doNotDisturbMenuItem"
);
is(doNotDisturbMenuItem.localName, "menuitem", "menuitem found");
let unloadPromise = BrowserTestUtils.waitForEvent(
alertWindow,
"beforeunload"
);
doNotDisturbMenuItem.click();
info("Clicked on do-not-disturb menuitem");
await unloadPromise;
// At this point, we should be configured to not display notifications
// to the user.
ok(
ALERT_SERVICE.manualDoNotDisturb,
"Alert service should be disabled after clicking menuitem"
);
// The notification should not appear, but there is no way from the
// client-side to know that it was blocked, except for waiting some time
// and realizing that the "onshow" event never fired.
await Assert.rejects(
openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
/timed out/,
"The notification should never display."
);
ALERT_SERVICE.manualDoNotDisturb = false;
});
});
/**
* Test that the suppressForScreenSharing attribute can prevent
* notifications from appearing.
*/
add_task(async function test_suppressForScreenSharing() {
try {
// Only run the test if the do-not-disturb
// interface has been implemented.
ALERT_SERVICE.suppressForScreenSharing;
ok(true, "Alert service implements do-not-disturb interface");
} catch (e) {
ok(
true,
"Alert service doesn't implement do-not-disturb interface, exiting test"
);
return;
}
// In the event that something goes wrong during this test, make sure
// we put the attribute back to the default setting when this test file
// exits.
registerCleanupFunction(() => {
ALERT_SERVICE.suppressForScreenSharing = false;
});
// Make sure that do-not-disturb is not enabled before we start.
ok(
!ALERT_SERVICE.suppressForScreenSharing,
"Alert service should not be suppressing for screen sharing when test " +
"starts"
);
await BrowserTestUtils.withNewTab(PAGE, async browser => {
await openNotification(browser, "showNotification2");
info("Notification alert showing");
await closeNotification(browser);
ALERT_SERVICE.suppressForScreenSharing = true;
// The notification should not appear, but there is no way from the
// client-side to know that it was blocked, except for waiting some time
// and realizing that the "onshow" event never fired.
await Assert.rejects(
openNotification(browser, "showNotification2", NOTIFICATION_TIMEOUT_SECS),
/timed out/,
"The notification should never display."
);
});
ALERT_SERVICE.suppressForScreenSharing = false;
});

Some files were not shown because too many files have changed in this diff Show More