mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-07 13:57:18 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a323036995 | ||
|
|
abc69a1fcb | ||
|
|
8df45e5c9a | ||
|
|
58e83c137f |
@@ -26,3 +26,5 @@ files:
|
|||||||
translation: browser/browser/zen-space-routing.ftl
|
translation: browser/browser/zen-space-routing.ftl
|
||||||
- source: en-US/browser/browser/zen-command-palette.ftl
|
- source: en-US/browser/browser/zen-command-palette.ftl
|
||||||
translation: browser/browser/zen-command-palette.ftl
|
translation: browser/browser/zen-command-palette.ftl
|
||||||
|
- source: en-US/browser/browser/zen-share.ftl
|
||||||
|
translation: browser/browser/zen-share.ftl
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ zen-settings-workspaces-sync =
|
|||||||
.label = Sync your sidebar across devices
|
.label = Sync your sidebar across devices
|
||||||
.description = Keep your workspaces, pinned tabs and folders in sync on all your devices through your Mozilla account.
|
.description = Keep your workspaces, pinned tabs and folders in sync on all your devices through your Mozilla account.
|
||||||
|
|
||||||
|
zen-settings-normal-tabs-sync =
|
||||||
|
.label = Include unpinned tabs
|
||||||
|
.description = Also sync the regular tabs of each space, not just your pinned tabs and folders.
|
||||||
|
|
||||||
zen-settings-workspaces-sync-unpinned-tabs =
|
zen-settings-workspaces-sync-unpinned-tabs =
|
||||||
.label = Sync only pinned tabs in workspaces
|
.label = Sync only pinned tabs in workspaces
|
||||||
|
|
||||||
|
|||||||
45
locales/en-US/browser/browser/zen-share.ftl
Normal file
45
locales/en-US/browser/browser/zen-share.ftl
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# 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-share-space =
|
||||||
|
.label = Share Space…
|
||||||
|
zen-share-folder =
|
||||||
|
.label = Share Folder…
|
||||||
|
zen-share-split-view =
|
||||||
|
.label = Share Split View…
|
||||||
|
|
||||||
|
zen-share-link-copied-toast = Share link copied!
|
||||||
|
zen-share-link-expires-description = Anyone with the link can open it until { $date }
|
||||||
|
zen-share-link-permanent-description = Anyone with the link can open it
|
||||||
|
|
||||||
|
zen-share-error-toast = Couldn't create a share link
|
||||||
|
zen-share-error-auth-description = The share server rejected your key
|
||||||
|
zen-share-error-too-large-description = This share is too big to upload
|
||||||
|
zen-share-error-rate-limited-description = You're sharing too fast, try again in a minute
|
||||||
|
zen-share-error-network-description = Couldn't reach the share server
|
||||||
|
zen-share-error-empty-description = There's nothing shareable here
|
||||||
|
zen-share-error-server-description = The share server returned an error
|
||||||
|
|
||||||
|
zen-share-overlay-loading = Loading shared tabs…
|
||||||
|
zen-share-overlay-from-space = A Space from { $name }
|
||||||
|
zen-share-overlay-from-folder = A Folder from { $name }
|
||||||
|
zen-share-overlay-shared-with-you = Shared with you
|
||||||
|
zen-share-overlay-add-space = Add Space to Zen
|
||||||
|
zen-share-overlay-add-folder = Add Folder to Zen
|
||||||
|
zen-share-imported-toast = Added to your browser!
|
||||||
|
zen-share-import-error-toast = Couldn't import this share
|
||||||
|
zen-share-import-error-dead-description = The link has expired or doesn't exist
|
||||||
|
zen-share-import-error-invalid-description = The shared data is invalid
|
||||||
|
|
||||||
|
zen-share-confirm-window =
|
||||||
|
.title = Share with your buds
|
||||||
|
zen-share-confirm-title = Share with your buds
|
||||||
|
zen-share-confirm-dialog =
|
||||||
|
.buttonlabelaccept = Share
|
||||||
|
zen-share-confirm-description = Zen uploads a copy of these tabs to the share server and copies a link anyone can open. The link expires after 30 days.
|
||||||
|
zen-share-confirm-anonymous = Sharing is anonymous unless you add a name.
|
||||||
|
zen-share-confirm-name-input =
|
||||||
|
.placeholder = Your name (optional)
|
||||||
|
zen-share-confirm-dont-ask =
|
||||||
|
.label = Don't ask me again
|
||||||
@@ -2,5 +2,18 @@
|
|||||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
# 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/.
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
- name: zen.share.enabled
|
- name: zen.share.base-url
|
||||||
|
value: "https://share.zen-browser.app"
|
||||||
|
|
||||||
|
- name: zen.share.secret-key
|
||||||
|
value: ""
|
||||||
|
|
||||||
|
- name: zen.share.display-name
|
||||||
|
value: ""
|
||||||
|
|
||||||
|
# Allow zen.share.display-name to be synced across devices.
|
||||||
|
- name: services.sync.prefs.sync.zen.share.display-name
|
||||||
value: true
|
value: true
|
||||||
|
|
||||||
|
- name: zen.share.dont-ask-before-sharing
|
||||||
|
value: false
|
||||||
|
|||||||
@@ -8,3 +8,9 @@
|
|||||||
|
|
||||||
- name: zen.spaces-sync.debug
|
- name: zen.spaces-sync.debug
|
||||||
value: false
|
value: false
|
||||||
|
|
||||||
|
- name: zen.spaces-sync.normal-tabs
|
||||||
|
value: false
|
||||||
|
|
||||||
|
- name: services.sync.prefs.sync.zen.spaces-sync.normal-tabs
|
||||||
|
value: true
|
||||||
|
|||||||
@@ -21,3 +21,4 @@
|
|||||||
#include ../../../zen/boosts/jar.inc.mn
|
#include ../../../zen/boosts/jar.inc.mn
|
||||||
#include ../../../zen/live-folders/jar.inc.mn
|
#include ../../../zen/live-folders/jar.inc.mn
|
||||||
#include ../../../zen/space-routing/jar.inc.mn
|
#include ../../../zen/space-routing/jar.inc.mn
|
||||||
|
#include ../../../zen/share/jar.inc.mn
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<command id="cmd_contextZenRemoveFromEssentials" />
|
<command id="cmd_contextZenRemoveFromEssentials" />
|
||||||
|
|
||||||
<command id="cmd_zenCtxDeleteWorkspace" />
|
<command id="cmd_zenCtxDeleteWorkspace" />
|
||||||
|
<command id="cmd_zenCtxShareWorkspace" />
|
||||||
<command id="cmd_zenUnloadWorkspace" />
|
<command id="cmd_zenUnloadWorkspace" />
|
||||||
<command id="cmd_zenUnloadAllOtherWorkspace" />
|
<command id="cmd_zenUnloadAllOtherWorkspace" />
|
||||||
<command id="cmd_zenOpenSpaceRoutingSettings" />
|
<command id="cmd_zenOpenSpaceRoutingSettings" />
|
||||||
|
|||||||
@@ -12,4 +12,5 @@
|
|||||||
<link rel="localization" href="browser/zen-boosts.ftl"/>
|
<link rel="localization" href="browser/zen-boosts.ftl"/>
|
||||||
<link rel="localization" href="browser/zen-live-folders.ftl"/>
|
<link rel="localization" href="browser/zen-live-folders.ftl"/>
|
||||||
<link rel="localization" href="browser/zen-space-routing.ftl"/>
|
<link rel="localization" href="browser/zen-space-routing.ftl"/>
|
||||||
|
<link rel="localization" href="browser/zen-share.ftl"/>
|
||||||
</linkset>
|
</linkset>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<menuitem id="context_zenUnloadAllOtherWorkspace" data-l10n-id="zen-workspaces-panel-unload-others" command="cmd_zenUnloadAllOtherWorkspace"/>
|
<menuitem id="context_zenUnloadAllOtherWorkspace" data-l10n-id="zen-workspaces-panel-unload-others" command="cmd_zenUnloadAllOtherWorkspace"/>
|
||||||
<menuseparator/>
|
<menuseparator/>
|
||||||
<menuitem data-l10n-id="zen-panel-ui-workspaces-create" command="cmd_zenOpenWorkspaceCreation"/>
|
<menuitem data-l10n-id="zen-panel-ui-workspaces-create" command="cmd_zenOpenWorkspaceCreation"/>
|
||||||
|
<menuitem id="context_zenShareWorkspace" data-l10n-id="zen-share-space" command="cmd_zenCtxShareWorkspace"/>
|
||||||
<menuitem id="context_zenDeleteWorkspace" data-l10n-id="zen-workspaces-panel-context-delete" command="cmd_zenCtxDeleteWorkspace"/>
|
<menuitem id="context_zenDeleteWorkspace" data-l10n-id="zen-workspaces-panel-context-delete" command="cmd_zenCtxDeleteWorkspace"/>
|
||||||
<menuseparator/>
|
<menuseparator/>
|
||||||
<menuitem id="context_zenSpaceRoutingSettings" data-l10n-id="zen-space-routing-settings" command="cmd_zenOpenSpaceRoutingSettings"/>
|
<menuitem id="context_zenSpaceRoutingSettings" data-l10n-id="zen-space-routing-settings" command="cmd_zenOpenSpaceRoutingSettings"/>
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
<menupopup />
|
<menupopup />
|
||||||
</menu>
|
</menu>
|
||||||
<menuitem id="context_zenFolderToSpace" data-l10n-id="zen-folders-panel-convert-folder-to-space" />
|
<menuitem id="context_zenFolderToSpace" data-l10n-id="zen-folders-panel-convert-folder-to-space" />
|
||||||
|
<menuitem id="context_zenShareFolder" data-l10n-id="zen-share-folder"/>
|
||||||
<menuseparator />
|
<menuseparator />
|
||||||
<menuitem id="context_zenFolderUnpack" data-l10n-id="zen-folders-panel-unpack-folder"/>
|
<menuitem id="context_zenFolderUnpack" data-l10n-id="zen-folders-panel-unpack-folder"/>
|
||||||
<menuitem id="context_zenFolderDelete" data-l10n-id="zen-folders-panel-delete-folder"/>
|
<menuitem id="context_zenFolderDelete" data-l10n-id="zen-folders-panel-delete-folder"/>
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
|
diff --git a/browser/components/preferences/config/account-sync.mjs b/browser/components/preferences/config/account-sync.mjs
|
||||||
index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac3342975bd241a 100644
|
index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..7e3c2f6852b77192b4505bb3d5a7b58586da6548 100644
|
||||||
--- a/browser/components/preferences/config/account-sync.mjs
|
--- a/browser/components/preferences/config/account-sync.mjs
|
||||||
+++ b/browser/components/preferences/config/account-sync.mjs
|
+++ b/browser/components/preferences/config/account-sync.mjs
|
||||||
@@ -46,6 +46,7 @@ Preferences.addAll([
|
@@ -46,6 +46,8 @@ Preferences.addAll([
|
||||||
{ id: "services.sync.engine.creditcards", type: "bool" },
|
{ id: "services.sync.engine.creditcards", type: "bool" },
|
||||||
{ id: "services.sync.engine.addons", type: "bool" },
|
{ id: "services.sync.engine.addons", type: "bool" },
|
||||||
{ id: "services.sync.engine.prefs", type: "bool" },
|
{ id: "services.sync.engine.prefs", type: "bool" },
|
||||||
+ { id: "services.sync.engine.spaces", type: "bool" },
|
+ { id: "services.sync.engine.spaces", type: "bool" },
|
||||||
|
+ { id: "zen.spaces-sync.normal-tabs", type: "bool" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -557,6 +558,7 @@ const SYNC_ENGINE_SETTINGS = [
|
@@ -557,6 +559,7 @@ const SYNC_ENGINE_SETTINGS = [
|
||||||
type: "payments",
|
type: "payments",
|
||||||
},
|
},
|
||||||
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
|
{ id: "syncAddons", pref: "services.sync.engine.addons", type: "addons" },
|
||||||
@@ -18,7 +19,7 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
|
|||||||
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
|
{ id: "syncSettings", pref: "services.sync.engine.prefs", type: "settings" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -564,6 +566,12 @@ SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
|
@@ -564,6 +567,18 @@ SYNC_ENGINE_SETTINGS.forEach(({ id, pref }) => {
|
||||||
Preferences.addSetting({ id, pref });
|
Preferences.addSetting({ id, pref });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,11 +28,17 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
|
|||||||
+ id: "zenSyncWorkspaces",
|
+ id: "zenSyncWorkspaces",
|
||||||
+ pref: "services.sync.engine.spaces",
|
+ pref: "services.sync.engine.spaces",
|
||||||
+});
|
+});
|
||||||
|
+Preferences.addSetting({
|
||||||
|
+ id: "zenSyncNormalTabs",
|
||||||
|
+ pref: "zen.spaces-sync.normal-tabs",
|
||||||
|
+ deps: ["zenSyncWorkspaces"],
|
||||||
|
+ visible: deps => !!deps.zenSyncWorkspaces.value,
|
||||||
|
+});
|
||||||
+
|
+
|
||||||
Preferences.addSetting({
|
Preferences.addSetting({
|
||||||
id: "syncEngines",
|
id: "syncEngines",
|
||||||
deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id),
|
deps: SYNC_ENGINE_SETTINGS.map(({ id }) => id),
|
||||||
@@ -995,6 +1003,20 @@ SettingGroupManager.registerGroups({
|
@@ -995,6 +1010,27 @@ SettingGroupManager.registerGroups({
|
||||||
id: "syncEnginesList",
|
id: "syncEnginesList",
|
||||||
control: "sync-engines-list",
|
control: "sync-engines-list",
|
||||||
},
|
},
|
||||||
@@ -46,6 +53,13 @@ index b01fd4bb163ad53139fcbd8e79e12ef994d1fa13..dd60be4fe268618d6596cd5cfac33429
|
|||||||
+ controlAttrs: {
|
+ controlAttrs: {
|
||||||
+ inputlayout: "inline-end",
|
+ inputlayout: "inline-end",
|
||||||
+ },
|
+ },
|
||||||
|
+ items: [
|
||||||
|
+ {
|
||||||
|
+ id: "zenSyncNormalTabs",
|
||||||
|
+ l10nId: "zen-settings-normal-tabs-sync",
|
||||||
|
+ control: "moz-checkbox",
|
||||||
|
+ },
|
||||||
|
+ ],
|
||||||
+ },
|
+ },
|
||||||
+ ],
|
+ ],
|
||||||
+ },
|
+ },
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"chrome://browser/content/zen-components/ZenEmojiPicker.mjs",
|
"chrome://browser/content/zen-components/ZenEmojiPicker.mjs",
|
||||||
"chrome://browser/content/zen-components/ZenLiveFoldersUI.mjs",
|
"chrome://browser/content/zen-components/ZenLiveFoldersUI.mjs",
|
||||||
"chrome://browser/content/zen-components/ZenDownloadAnimation.mjs",
|
"chrome://browser/content/zen-components/ZenDownloadAnimation.mjs",
|
||||||
|
"resource:///modules/zen/share/ZenShareManager.mjs",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let script of scripts) {
|
for (let script of scripts) {
|
||||||
|
|||||||
31
src/zen/common/styles/squircle-exclusions.inc.css
Normal file
31
src/zen/common/styles/squircle-exclusions.inc.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Upstream elements that take their radius from --border-radius-circle (9999px).
|
||||||
|
They are meant to read as circles or pills, so a superellipse only makes them
|
||||||
|
look lumpy. */
|
||||||
|
.tab-icon-overlay:not([crashed]):is([soundplaying], [muted], [activemedia-blocked]),
|
||||||
|
.tab-note-icon-overlay,
|
||||||
|
.tab-group-overflow-count,
|
||||||
|
.tab-group-editor-swatch,
|
||||||
|
.tab-preview-container-indicator,
|
||||||
|
#tabbrowser-tabs[orient='vertical']:not([expanded]) .tab-close-button,
|
||||||
|
.all-tabs-item > toolbarbutton.all-tabs-container-indicator::before,
|
||||||
|
.urlbarView-button,
|
||||||
|
.downloadButton,
|
||||||
|
#downloads-indicator-progress-inner,
|
||||||
|
#smartwindow-ask-button-inner,
|
||||||
|
#PanelUI-fxa-menu-manage-account-button,
|
||||||
|
.fxa-menu-manage-account-avatar,
|
||||||
|
[fxastatus='signedin'] #fxa-avatar-image,
|
||||||
|
#profile-icon-image,
|
||||||
|
.appMenu-update-banner-pulse-ring,
|
||||||
|
:is(#appMenu-zoomReduce-button2, #appMenu-zoomEnlarge-button2) > .toolbarbutton-icon,
|
||||||
|
.color-swatches .userContext-icon,
|
||||||
|
.reusable-relay-masks > button,
|
||||||
|
.radio-check {
|
||||||
|
corner-shape: round;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
:root:not([inDOMFullscreen="true"]):not([chromehidden~="location"]):not([chromehidden~="toolbar"]) {
|
:root:not([inDOMFullscreen="true"]):not([chromehidden~="location"]):not([chromehidden~="toolbar"]) {
|
||||||
& #tabbrowser-tabbox #tabbrowser-tabpanels .browserSidebarContainer {
|
& #tabbrowser-tabbox #tabbrowser-tabpanels .browserSidebarContainer {
|
||||||
|
--zen-transparent-browser-background: light-dark(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.1));
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
|
||||||
:root:not([zen-no-padding="true"]) &:not(.zen-glance-overlay) {
|
:root:not([zen-no-padding="true"]) &:not(.zen-glance-overlay) {
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[transparent="true"] {
|
&[transparent="true"] {
|
||||||
background: light-dark(rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.1));
|
background: var(--zen-transparent-browser-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,3 +78,52 @@ dialog::part(dialog-button) {
|
|||||||
scale: 0.98;
|
scale: 0.98;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--zen-accent-button-color: light-dark(
|
||||||
|
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
|
||||||
|
oklch(from var(--zen-primary-color) clamp(0.7, l, 0.85) c h)
|
||||||
|
);
|
||||||
|
--zen-accent-button-background: light-dark(
|
||||||
|
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
|
||||||
|
oklch(from var(--zen-primary-color) clamp(0.55, l, 0.7) c h)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-big-accent-button {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 1.2rem 1rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--zen-accent-button-color);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
will-change: transform;
|
||||||
|
transition: scale 0.15s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.03;
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:hover {
|
||||||
|
scale: 1;
|
||||||
|
color: color-mix(in srgb, var(--zen-accent-button-color) 80%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: var(--zen-accent-button-background);
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:hover {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
) !important;
|
) !important;
|
||||||
--focus-outline-color: var(--color-accent-primary) !important;
|
--focus-outline-color: var(--color-accent-primary) !important;
|
||||||
|
|
||||||
--toolbarbutton-icon-fill-attention: var(--toolbar-color) !important;
|
--toolbarbutton-icon-fill-attention: var(--toolbar-color, currentColor) !important;
|
||||||
--toolbarbutton-icon-fill: currentColor !important;
|
--toolbarbutton-icon-fill: currentColor !important;
|
||||||
|
|
||||||
--button-background-color-primary: var(--in-content-primary-button-background) !important;
|
--button-background-color-primary: var(--in-content-primary-button-background) !important;
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
--zen-squircle-value: 1.3;
|
--zen-squircle-value: 1.3;
|
||||||
|
|
||||||
@media (-moz-platform: windows) {
|
@media (-moz-platform: windows) {
|
||||||
--zen-squircle-value: 2.1;
|
--zen-squircle-value: 2.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
--tab-icon-overlay-stroke: light-dark(white, black) !important;
|
--tab-icon-overlay-stroke: light-dark(white, black) !important;
|
||||||
@@ -361,4 +361,6 @@
|
|||||||
.zen-squircle-after::after {
|
.zen-squircle-after::after {
|
||||||
corner-shape: superellipse(var(--zen-squircle-value));
|
corner-shape: superellipse(var(--zen-squircle-value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
%include squircle-exclusions.inc.css
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ document.addEventListener(
|
|||||||
case "cmd_zenCtxDeleteWorkspace":
|
case "cmd_zenCtxDeleteWorkspace":
|
||||||
gZenWorkspaces.contextDeleteWorkspace(event);
|
gZenWorkspaces.contextDeleteWorkspace(event);
|
||||||
break;
|
break;
|
||||||
|
case "cmd_zenCtxShareWorkspace":
|
||||||
|
gZenWorkspaces.contextShareWorkspace();
|
||||||
|
break;
|
||||||
case "cmd_zenChangeWorkspaceName":
|
case "cmd_zenChangeWorkspaceName":
|
||||||
gZenVerticalTabsManager.renameTabStart({
|
gZenVerticalTabsManager.renameTabStart({
|
||||||
target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(
|
target: gZenWorkspaces.activeWorkspaceIndicator.querySelector(
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
// Windows defaults to 8px
|
// Windows defaults to 8px
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--zen-border-radius",
|
"--zen-border-radius",
|
||||||
"8px"
|
"9px"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
--zen-compact-float: var(--zen-element-separation);
|
--zen-compact-float: var(--zen-element-separation);
|
||||||
|
|
||||||
@media (-moz-mac-tahoe-theme) {
|
@media (-moz-mac-tahoe-theme) {
|
||||||
--zen-compact-mode-no-padding-radius-fix: -2px;
|
--zen-compact-mode-no-padding-radius-fix: -4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[zen-no-padding='true'] & {
|
:root[zen-no-padding='true'] & {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
&,
|
&,
|
||||||
&::before,
|
&::before,
|
||||||
&::after {
|
&::after {
|
||||||
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)) * var(--zen-squircle-value));
|
border-radius: calc((var(--zen-native-inner-radius) - var(--zen-compact-mode-no-padding-radius-fix, 0px)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,9 @@ class nsZenFolders extends nsZenDOMOperatedFeature {
|
|||||||
case "context_zenFolderChangeIcon":
|
case "context_zenFolderChangeIcon":
|
||||||
this.changeFolderUserIcon(this.#lastFolderContextMenu);
|
this.changeFolderUserIcon(this.#lastFolderContextMenu);
|
||||||
break;
|
break;
|
||||||
|
case "context_zenShareFolder":
|
||||||
|
gZenShareManager.shareFolder(this.#lastFolderContextMenu);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@
|
|||||||
|
|
||||||
content/browser/zen-components/ZenFolder.mjs (../../zen/folders/ZenFolder.mjs)
|
content/browser/zen-components/ZenFolder.mjs (../../zen/folders/ZenFolder.mjs)
|
||||||
content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
|
content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
|
||||||
content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
|
* content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
|
||||||
|
|||||||
19
src/zen/folders/zen-folder-icon.inc.css
Normal file
19
src/zen/folders/zen-folder-icon.inc.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
& g,
|
||||||
|
& rect,
|
||||||
|
& path {
|
||||||
|
transition:
|
||||||
|
transform 0.3s cubic-bezier(0.42, 0, 0, 1),
|
||||||
|
opacity 0.3s cubic-bezier(0.42, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[state="open"] .back {
|
||||||
|
transform: skewX(16deg) translate(-4px, 2px) scale(0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[state="open"] :is(.front, .dots, .icon) {
|
||||||
|
transform: skewX(-16deg) translate(8px, 2px) scale(0.85);
|
||||||
|
}
|
||||||
@@ -8,15 +8,17 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--zen-folder-behind-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color) 60%, gray), color-mix(in srgb, var(--zen-primary-color) 60%, #c1c1c1));
|
||||||
|
--zen-folder-front-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color), white 70%), color-mix(in srgb, var(--zen-primary-color), black 40%));
|
||||||
|
--zen-folder-stroke: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
|
||||||
|
}
|
||||||
|
|
||||||
zen-folder {
|
zen-folder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
|
|
||||||
--zen-folder-behind-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color) 60%, gray), color-mix(in srgb, var(--zen-primary-color) 60%, #c1c1c1));
|
|
||||||
--zen-folder-front-bgcolor: light-dark(color-mix(in srgb, var(--zen-primary-color), white 70%), color-mix(in srgb, var(--zen-primary-color), black 40%));
|
|
||||||
--zen-folder-stroke: light-dark(color-mix(in srgb, var(--zen-primary-color) 50%, black), color-mix(in srgb, var(--zen-colors-primary) 15%, #ebebeb));
|
|
||||||
|
|
||||||
-moz-window-dragging: no-drag;
|
-moz-window-dragging: no-drag;
|
||||||
transition: var(--zen-tabbox-element-indent-transition);
|
transition: var(--zen-tabbox-element-indent-transition);
|
||||||
|
|
||||||
@@ -114,21 +116,7 @@ zen-folder {
|
|||||||
transform: translate(8.5px, 10.5px);
|
transform: translate(8.5px, 10.5px);
|
||||||
}
|
}
|
||||||
|
|
||||||
& g,
|
%include zen-folder-icon.inc.css
|
||||||
& rect,
|
|
||||||
& path {
|
|
||||||
transition:
|
|
||||||
transform 0.3s cubic-bezier(0.42, 0, 0, 1),
|
|
||||||
opacity 0.3s cubic-bezier(0.42, 0, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[state="open"] .back {
|
|
||||||
transform: skewX(16deg) translate(-4px, 2px) scale(0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[state="open"] :is(.front, .dots, .icon) {
|
|
||||||
transform: skewX(-16deg) translate(8px, 2px) scale(0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .icon {
|
& .icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
|||||||
return Promise.resolve(this.#currentTab);
|
return Promise.resolve(this.#currentTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Existing-tab glances perform no navigation and are exempt; for fresh
|
// Existing-tab glances perform no navigation and are exempt. For fresh
|
||||||
// loads, refuse any URL the triggering principal isn't allowed to load
|
// loads, refuse any URL the triggering principal isn't allowed to load
|
||||||
// (e.g. a web page linking to file://).
|
// (e.g. a web page linking to file://).
|
||||||
if (!existingTab && !this.#isGlanceLoadAllowed(data)) {
|
if (!existingTab && !this.#isGlanceLoadAllowed(data)) {
|
||||||
|
|||||||
238
src/zen/share/ZenShareClient.sys.mjs
Normal file
238
src/zen/share/ZenShareClient.sys.mjs
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// 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 { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||||
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
JsonSchema: "resource://gre/modules/JsonSchema.sys.mjs",
|
||||||
|
clearTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||||
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"gBaseUrl",
|
||||||
|
"zen.share.base-url",
|
||||||
|
"",
|
||||||
|
null,
|
||||||
|
value => value.trim().replace(/\/+$/, "")
|
||||||
|
);
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"gSecretKey",
|
||||||
|
"zen.share.secret-key",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
const SCHEMA_URL = "resource:///modules/zen/share/share.schema.json";
|
||||||
|
|
||||||
|
// The server rejects bigger bodies at 25 MiB.
|
||||||
|
const MAX_SHARE_BYTES = 25 * 1024 * 1024;
|
||||||
|
const MAX_NAME_LENGTH = 200;
|
||||||
|
|
||||||
|
// Uploads can be tens of MiB, reads are small JSON.
|
||||||
|
const CREATE_TIMEOUT_MS = 120000;
|
||||||
|
const READ_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
|
// Public share page paths: /{slug}/{XXXX-XXXX-XXXX-XXXX id}.
|
||||||
|
const SHARE_PATH_RE =
|
||||||
|
/^\/(space|folder|split-view|split)\/([0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4}-[0-9A-Za-z]{4})\/?$/;
|
||||||
|
|
||||||
|
export class ZenShareError extends Error {
|
||||||
|
/**
|
||||||
|
* @param {string} code - One of "invalid-document", "auth", "too-large",
|
||||||
|
* "rate-limited", "not-found", "network", "server".
|
||||||
|
* @param {string} message
|
||||||
|
*/
|
||||||
|
constructor(code, message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ZenShareError";
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class nsZenShareClient {
|
||||||
|
#validatorPromise = null;
|
||||||
|
|
||||||
|
get #baseUrl() {
|
||||||
|
return lazy.gBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
#authHeaders() {
|
||||||
|
// A secret key authorizes on its own and makes the share permanent. Never
|
||||||
|
// send both keys.
|
||||||
|
if (lazy.gSecretKey) {
|
||||||
|
return { "x-secret-key": lazy.gSecretKey };
|
||||||
|
}
|
||||||
|
return { "x-api-key": AppConstants.MOZ_MOZILLA_API_KEY };
|
||||||
|
}
|
||||||
|
|
||||||
|
#getValidator() {
|
||||||
|
if (!this.#validatorPromise) {
|
||||||
|
this.#validatorPromise = fetch(SCHEMA_URL, { credentials: "omit" })
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(schema => new lazy.JsonSchema.Validator(schema));
|
||||||
|
}
|
||||||
|
return this.#validatorPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a share document against the schema.
|
||||||
|
*
|
||||||
|
* @param {object} doc
|
||||||
|
* @returns {Promise<{valid: boolean, errors: object[]}>}
|
||||||
|
*/
|
||||||
|
async validateDocument(doc) {
|
||||||
|
const validator = await this.#getValidator();
|
||||||
|
return validator.validate(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #assertValidDocument(doc) {
|
||||||
|
const result = await this.validateDocument(doc);
|
||||||
|
if (!result.valid) {
|
||||||
|
console.error("ZenShare: document fails the schema", result.errors);
|
||||||
|
throw new ZenShareError(
|
||||||
|
"invalid-document",
|
||||||
|
"share document fails the schema"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #request(url, options, timeoutMs = READ_TIMEOUT_MS) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = lazy.setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(url, {
|
||||||
|
credentials: "omit",
|
||||||
|
signal: controller.signal,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw new ZenShareError(
|
||||||
|
"network",
|
||||||
|
`could not reach ${url}: ${e.message}`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
lazy.clearTimeout(timer);
|
||||||
|
}
|
||||||
|
if (response.ok) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
let message = `${response.status}`;
|
||||||
|
try {
|
||||||
|
message = (await response.json()).error || message;
|
||||||
|
} catch (e) {}
|
||||||
|
switch (response.status) {
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
|
throw new ZenShareError("auth", message);
|
||||||
|
case 404:
|
||||||
|
throw new ZenShareError("not-found", message);
|
||||||
|
case 413:
|
||||||
|
throw new ZenShareError("too-large", message);
|
||||||
|
case 429:
|
||||||
|
throw new ZenShareError("rate-limited", message);
|
||||||
|
case 400:
|
||||||
|
case 422:
|
||||||
|
throw new ZenShareError("invalid-document", message);
|
||||||
|
default:
|
||||||
|
throw new ZenShareError("server", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a share document.
|
||||||
|
*
|
||||||
|
* @param {object} doc - A document conforming to share.schema.json.
|
||||||
|
* @param {{name?: string}} options - Optional sharer display name, shown on
|
||||||
|
* the share page as "A Space from {name}".
|
||||||
|
* @returns {Promise<object>} The create response, plus `link`: the absolute
|
||||||
|
* public page URL to hand to the user.
|
||||||
|
*/
|
||||||
|
async createShare(doc, { name } = {}) {
|
||||||
|
const base = this.#baseUrl;
|
||||||
|
const headers = this.#authHeaders();
|
||||||
|
await this.#assertValidDocument(doc);
|
||||||
|
const body = JSON.stringify(doc);
|
||||||
|
if (new TextEncoder().encode(body).length > MAX_SHARE_BYTES) {
|
||||||
|
throw new ZenShareError("too-large", "share document is over 25 MiB");
|
||||||
|
}
|
||||||
|
let url = `${base}/api/shares`;
|
||||||
|
if (name) {
|
||||||
|
url += `?name=${encodeURIComponent(name.slice(0, MAX_NAME_LENGTH))}`;
|
||||||
|
}
|
||||||
|
const response = await this.#request(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { ...headers, "content-type": "application/json" },
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
CREATE_TIMEOUT_MS
|
||||||
|
);
|
||||||
|
const created = await response.json();
|
||||||
|
return { ...created, link: base + created.webUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recognizes public share page URLs on the configured server.
|
||||||
|
*
|
||||||
|
* @param {string} spec
|
||||||
|
* @returns {?{type: string, id: string}} null when the URL is not a share
|
||||||
|
* page (or no server is configured).
|
||||||
|
*/
|
||||||
|
parseShareUrl(spec) {
|
||||||
|
let uri;
|
||||||
|
let baseUri;
|
||||||
|
try {
|
||||||
|
uri = Services.io.newURI(spec);
|
||||||
|
baseUri = Services.io.newURI(this.#baseUrl);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (uri.prePath !== baseUri.prePath) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const match = uri.filePath.match(SHARE_PATH_RE);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const slug = match[1];
|
||||||
|
const type = slug === "split" ? "split-view" : slug;
|
||||||
|
return { type, id: match[2].toUpperCase() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches everything needed to preview and import a share: the validated
|
||||||
|
* document, plus the sharer's display name.
|
||||||
|
*
|
||||||
|
* @param {{id: string}} share - As returned by parseShareUrl.
|
||||||
|
* @returns {Promise<{doc: object, name: ?string}>}
|
||||||
|
*/
|
||||||
|
async fetchSharePreview(share) {
|
||||||
|
const meta = await this.getShare(share.id);
|
||||||
|
await this.#assertValidDocument(meta.data);
|
||||||
|
return { doc: meta.data, name: meta.name || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a share with its metadata (authed):
|
||||||
|
* { id, name, createdAt, expiresAt, size, data }.
|
||||||
|
*
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
async getShare(id) {
|
||||||
|
const response = await this.#request(`${this.#baseUrl}/api/shares/${id}`, {
|
||||||
|
headers: this.#authHeaders(),
|
||||||
|
});
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ZenShareClient = new nsZenShareClient();
|
||||||
832
src/zen/share/ZenShareManager.mjs
Normal file
832
src/zen/share/ZenShareManager.mjs
Normal file
@@ -0,0 +1,832 @@
|
|||||||
|
// 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";
|
||||||
|
import { nsZenThemePicker } from "resource:///modules/zen/ZenGradientGenerator.mjs";
|
||||||
|
|
||||||
|
const lazy = {};
|
||||||
|
|
||||||
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
|
TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs",
|
||||||
|
ZenShareClient: "resource:///modules/zen/share/ZenShareClient.sys.mjs",
|
||||||
|
ZenShareError: "resource:///modules/zen/share/ZenShareClient.sys.mjs",
|
||||||
|
});
|
||||||
|
|
||||||
|
const SHARE_DOCUMENT_VERSION = "1";
|
||||||
|
|
||||||
|
const SHAREABLE_ICON_RE = /^(data:|chrome:)/;
|
||||||
|
const FOLDER_ICON_RE = /^chrome:\/\//;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares spaces, folders and split views through the share server, and
|
||||||
|
* imports them back from share links.
|
||||||
|
*/
|
||||||
|
class nsZenShareManager extends nsZenDOMOperatedFeature {
|
||||||
|
init() {
|
||||||
|
this.#insertSplitViewMenuItem();
|
||||||
|
delayedStartupPromise.then(() => {
|
||||||
|
gBrowser.addTabsProgressListener({
|
||||||
|
onLocationChange: (browser, webProgress, request, aLocation) => {
|
||||||
|
this.#onLocationChange(browser, webProgress, aLocation);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark: sharing
|
||||||
|
|
||||||
|
async shareSpace(workspaceId) {
|
||||||
|
const workspace = gZenWorkspaces.getWorkspaceFromId(workspaceId);
|
||||||
|
const element = gZenWorkspaces.workspaceElement(workspaceId);
|
||||||
|
if (!workspace || !element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = {
|
||||||
|
type: "space",
|
||||||
|
name: workspace.name || "Space",
|
||||||
|
items: [
|
||||||
|
...this.#serializeItems(element.pinnedTabsContainer.children, {
|
||||||
|
pinned: true,
|
||||||
|
}),
|
||||||
|
...this.#serializeItems(element.tabsContainer.children, {
|
||||||
|
pinned: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const theme = this.#serializeTheme(workspace.theme);
|
||||||
|
if (theme) {
|
||||||
|
item.theme = theme;
|
||||||
|
}
|
||||||
|
await this.#createAndCopyLink(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async shareFolder(folder) {
|
||||||
|
if (!folder?.isZenFolder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.#createAndCopyLink(this.#serializeFolder(folder));
|
||||||
|
}
|
||||||
|
|
||||||
|
async shareSplitView(group) {
|
||||||
|
if (!group?.hasAttribute("split-view-group")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const item = this.#serializeSplitGroup(group);
|
||||||
|
if (!item) {
|
||||||
|
this.#showCreateError(
|
||||||
|
new lazy.ZenShareError("empty", "nothing to share")
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.#createAndCopyLink(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeSplitGroup(group) {
|
||||||
|
const tabs = group.tabs
|
||||||
|
.filter(tab => !tab.hasAttribute("zen-empty-tab"))
|
||||||
|
.map(tab => this.#serializeTab(tab, { pinned: tab.pinned }))
|
||||||
|
.filter(Boolean);
|
||||||
|
return tabs.length >= 2
|
||||||
|
? { type: "splitView", tabs: tabs.slice(0, gZenViewSplitter.MAX_TABS) }
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #createAndCopyLink(item) {
|
||||||
|
if (!(await this.#confirmShare())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const created = await lazy.ZenShareClient.createShare(
|
||||||
|
{ version: SHARE_DOCUMENT_VERSION, shared: [item] },
|
||||||
|
{ name: this.#displayName() }
|
||||||
|
);
|
||||||
|
Cc["@mozilla.org/widget/clipboardhelper;1"]
|
||||||
|
.getService(Ci.nsIClipboardHelper)
|
||||||
|
.copyString(created.link);
|
||||||
|
// The description's l10n args come from the whole options object.
|
||||||
|
gZenUIManager.showToast("zen-share-link-copied-toast", {
|
||||||
|
descriptionId: created.expiresAt
|
||||||
|
? "zen-share-link-expires-description"
|
||||||
|
: "zen-share-link-permanent-description",
|
||||||
|
date: created.expiresAt
|
||||||
|
? new Date(created.expiresAt).toLocaleDateString()
|
||||||
|
: "",
|
||||||
|
timeout: 6000,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
this.#showCreateError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#displayName() {
|
||||||
|
return (
|
||||||
|
Services.prefs.getStringPref("zen.share.display-name", "") || undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #confirmShare() {
|
||||||
|
if (
|
||||||
|
Services.prefs.getBoolPref("zen.share.dont-ask-before-sharing", false)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const args = {
|
||||||
|
accepted: false,
|
||||||
|
name: Services.prefs.getStringPref("zen.share.display-name", ""),
|
||||||
|
};
|
||||||
|
await gDialogBox.open(
|
||||||
|
"chrome://browser/content/zen-components/windows/zen-share-confirm.xhtml",
|
||||||
|
args
|
||||||
|
);
|
||||||
|
if (args.accepted) {
|
||||||
|
Services.prefs.setStringPref("zen.share.display-name", args.name);
|
||||||
|
if (args.dontAsk) {
|
||||||
|
Services.prefs.setBoolPref("zen.share.dont-ask-before-sharing", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args.accepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
#showCreateError(error) {
|
||||||
|
console.error("ZenShare: could not create share:", error);
|
||||||
|
const descriptions = {
|
||||||
|
auth: "zen-share-error-auth-description",
|
||||||
|
"too-large": "zen-share-error-too-large-description",
|
||||||
|
"rate-limited": "zen-share-error-rate-limited-description",
|
||||||
|
network: "zen-share-error-network-description",
|
||||||
|
empty: "zen-share-error-empty-description",
|
||||||
|
};
|
||||||
|
gZenUIManager.showToast("zen-share-error-toast", {
|
||||||
|
descriptionId:
|
||||||
|
descriptions[error.code] || "zen-share-error-server-description",
|
||||||
|
timeout: 6000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark: serialization
|
||||||
|
|
||||||
|
#serializeItems(children, { pinned }) {
|
||||||
|
const items = [];
|
||||||
|
for (const child of children) {
|
||||||
|
if (gBrowser.isTab(child)) {
|
||||||
|
if (
|
||||||
|
child.hasAttribute("zen-empty-tab") ||
|
||||||
|
child.hasAttribute("zen-glance-tab")
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tab = this.#serializeTab(child, { pinned });
|
||||||
|
if (tab) {
|
||||||
|
items.push(tab);
|
||||||
|
}
|
||||||
|
} else if (child.isZenFolder) {
|
||||||
|
items.push(this.#serializeFolder(child));
|
||||||
|
} else if (gBrowser.isTabGroup(child)) {
|
||||||
|
const split = child.hasAttribute("split-view-group")
|
||||||
|
? this.#serializeSplitGroup(child)
|
||||||
|
: null;
|
||||||
|
if (split) {
|
||||||
|
items.push(split);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Plain tab groups (and degenerate splits) keep their member tabs.
|
||||||
|
for (const tab of child.tabs) {
|
||||||
|
if (tab.hasAttribute("zen-empty-tab")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const serialized = this.#serializeTab(tab, { pinned });
|
||||||
|
if (serialized) {
|
||||||
|
items.push(serialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeFolder(folder) {
|
||||||
|
const item = {
|
||||||
|
type: "folder",
|
||||||
|
name: folder.label || "Folder",
|
||||||
|
items: this.#serializeItems(folder.allItems, { pinned: false }),
|
||||||
|
};
|
||||||
|
const icon = folder.iconURL;
|
||||||
|
if (icon) {
|
||||||
|
item.icon = icon;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeTab(tab, { pinned = false } = {}) {
|
||||||
|
const url = tab.linkedBrowser?.currentURI?.spec;
|
||||||
|
if (!url || !/^https?:\/\//.test(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const item = { type: "tab", url };
|
||||||
|
const label = tab.label;
|
||||||
|
if (label) {
|
||||||
|
item.label = label;
|
||||||
|
}
|
||||||
|
const image = this.#serializeIcon(tab);
|
||||||
|
if (image) {
|
||||||
|
item.image = image;
|
||||||
|
}
|
||||||
|
if (pinned) {
|
||||||
|
item.isPinned = true;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeIcon(tab) {
|
||||||
|
let icon = tab.getAttribute("image") || "";
|
||||||
|
if (icon.startsWith("moz-remote-image:")) {
|
||||||
|
try {
|
||||||
|
icon =
|
||||||
|
new URLSearchParams(Services.io.newURI(icon).query).get("url") || "";
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SHAREABLE_ICON_RE.test(icon) ? icon : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeTheme(theme) {
|
||||||
|
if (theme?.type !== "gradient" || !Array.isArray(theme.gradientColors)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const gradientColors = theme.gradientColors
|
||||||
|
.map(color => this.#serializeGradientColor(color))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!gradientColors.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { type: "gradient", gradientColors };
|
||||||
|
}
|
||||||
|
|
||||||
|
#serializeGradientColor(color) {
|
||||||
|
let c = color?.c;
|
||||||
|
if (typeof c === "string") {
|
||||||
|
c = this.#parseCssColor(c);
|
||||||
|
} else if (Array.isArray(c)) {
|
||||||
|
c = c.slice(0, 3).map(Number);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!Array.isArray(c) ||
|
||||||
|
c.length !== 3 ||
|
||||||
|
c.some(n => !Number.isFinite(n))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const out = { c, type: typeof color.type === "string" ? color.type : "" };
|
||||||
|
if (typeof color.isCustom === "boolean") {
|
||||||
|
out.isCustom = color.isCustom;
|
||||||
|
}
|
||||||
|
if (typeof color.isPrimary === "boolean") {
|
||||||
|
out.isPrimary = color.isPrimary;
|
||||||
|
}
|
||||||
|
if (typeof color.algorithm === "string") {
|
||||||
|
out.algorithm = color.algorithm;
|
||||||
|
}
|
||||||
|
if (color.lightness !== undefined && color.lightness !== null) {
|
||||||
|
out.lightness = String(color.lightness);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
Number.isFinite(color.position?.x) &&
|
||||||
|
Number.isFinite(color.position?.y)
|
||||||
|
) {
|
||||||
|
out.position = { x: color.position.x, y: color.position.y };
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#parseCssColor(value) {
|
||||||
|
const hex = value.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i)?.[1];
|
||||||
|
if (hex) {
|
||||||
|
const full =
|
||||||
|
hex.length === 3 ? [...hex].map(ch => ch + ch).join("") : hex;
|
||||||
|
return [0, 2, 4].map(i => parseInt(full.slice(i, i + 2), 16));
|
||||||
|
}
|
||||||
|
const numbers = value
|
||||||
|
.match(/\d+(\.\d+)?/g)
|
||||||
|
?.slice(0, 3)
|
||||||
|
.map(Number);
|
||||||
|
return numbers?.length === 3 ? numbers : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark: importing
|
||||||
|
|
||||||
|
#stylesLoaded = false;
|
||||||
|
|
||||||
|
// The overlay styles are only needed once a share link is opened.
|
||||||
|
#ensureStyles() {
|
||||||
|
if (this.#stylesLoaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#stylesLoaded = true;
|
||||||
|
window.windowUtils.loadSheetUsingURIString(
|
||||||
|
"chrome://browser/content/zen-styles/zen-share.css",
|
||||||
|
window.windowUtils.AUTHOR_SHEET
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setHostTabSubtitle(browser, subtitle) {
|
||||||
|
const tab = gBrowser.getTabForBrowser(browser);
|
||||||
|
if (!tab) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab.setAttribute("zen-show-sublabel", subtitle);
|
||||||
|
document.l10n.setArgs(tab.querySelector(".zen-tab-sublabel"), {
|
||||||
|
tabSubtitle: subtitle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#clearHostTabSubtitle(browser) {
|
||||||
|
const tab = gBrowser.getTabForBrowser(browser);
|
||||||
|
if (!tab?.hasAttribute("zen-show-sublabel")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab.removeAttribute("zen-show-sublabel");
|
||||||
|
document.l10n.setArgs(tab.querySelector(".zen-tab-sublabel"), {
|
||||||
|
tabSubtitle: "zen-default-pinned",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#onLocationChange(browser, webProgress, aLocation) {
|
||||||
|
if (!webProgress.isTopLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (browser._zenShareOverlay) {
|
||||||
|
browser._zenShareOverlay.remove();
|
||||||
|
delete browser._zenShareOverlay;
|
||||||
|
browser.removeAttribute("zen-share-overlay-showing");
|
||||||
|
this.#clearHostTabSubtitle(browser);
|
||||||
|
}
|
||||||
|
const share = lazy.ZenShareClient.parseShareUrl(aLocation.spec);
|
||||||
|
if (!share) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (share.type === "split-view") {
|
||||||
|
// Split views are light enough to open right away, no preview step.
|
||||||
|
this.#openSharedSplitView(browser, share);
|
||||||
|
} else {
|
||||||
|
this.#showImportOverlay(browser, share);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #openSharedSplitView(browser, share) {
|
||||||
|
let item;
|
||||||
|
const shareTab = gBrowser.getTabForBrowser(browser);
|
||||||
|
shareTab.style.display = "none";
|
||||||
|
const failure = () => {
|
||||||
|
shareTab.style.removeProperty("display");
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const { doc } = await lazy.ZenShareClient.fetchSharePreview(share);
|
||||||
|
item = doc.shared.find(entry => entry.type === "splitView");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("ZenShare: could not load shared split view:", e);
|
||||||
|
const descriptions = {
|
||||||
|
"not-found": "zen-share-import-error-dead-description",
|
||||||
|
"invalid-document": "zen-share-import-error-invalid-description",
|
||||||
|
};
|
||||||
|
gZenUIManager.showToast("zen-share-import-error-toast", {
|
||||||
|
descriptionId:
|
||||||
|
descriptions[e.code] || "zen-share-error-network-description",
|
||||||
|
timeout: 6000,
|
||||||
|
});
|
||||||
|
failure();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!item) {
|
||||||
|
failure();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workspaceId = gZenWorkspaces.activeWorkspace;
|
||||||
|
const pinned = item.tabs.every(child => child.isPinned);
|
||||||
|
const tabs = item.tabs.map(child => {
|
||||||
|
// Eager browsers so every pane renders as soon as the split shows.
|
||||||
|
const tab = this.#importTab(child, workspaceId, { lazyBrowser: false });
|
||||||
|
if (pinned) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
|
this.#placeTab(tab, workspaceId, pinned);
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
gZenViewSplitter.splitTabs(tabs, "grid", 0);
|
||||||
|
gBrowser.removeTab(shareTab, { closeWindowWithLastTab: false });
|
||||||
|
// Shake the new split group in the sidebar.
|
||||||
|
const group = tabs[0]?.group;
|
||||||
|
if (group?.hasAttribute("split-view-group")) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
gZenUIManager.motion
|
||||||
|
.animate(
|
||||||
|
group,
|
||||||
|
{ x: [-14, 0] },
|
||||||
|
{ type: "spring", bounce: 0.8, duration: 1.2 }
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
group.style.removeProperty("transform");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #showImportOverlay(browser, share) {
|
||||||
|
const stack = browser.closest(".browserStack");
|
||||||
|
if (!stack) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#ensureStyles();
|
||||||
|
// Only the status text shows until the preview loads.
|
||||||
|
const overlay = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<html:div class="zen-share-overlay">
|
||||||
|
<html:div class="zen-share-overlay-card">
|
||||||
|
<html:div class="zen-share-overlay-items"/>
|
||||||
|
<html:div class="zen-share-overlay-status"
|
||||||
|
data-l10n-id="zen-share-overlay-loading"/>
|
||||||
|
<html:div class="zen-share-overlay-footer" hidden="hidden">
|
||||||
|
<html:div class="zen-share-overlay-badge-stack">
|
||||||
|
<html:div class="zen-share-overlay-badge-sheet second"/>
|
||||||
|
<html:div class="zen-share-overlay-badge-sheet first"/>
|
||||||
|
<html:div class="zen-share-overlay-badge">
|
||||||
|
<html:img class="zen-share-overlay-badge-icon"
|
||||||
|
draggable="false"/>
|
||||||
|
<html:div class="zen-share-overlay-badge-text">
|
||||||
|
<html:div class="zen-share-overlay-badge-title"/>
|
||||||
|
<html:div class="zen-share-overlay-badge-subtitle"/>
|
||||||
|
</html:div>
|
||||||
|
</html:div>
|
||||||
|
</html:div>
|
||||||
|
</html:div>
|
||||||
|
</html:div>
|
||||||
|
<html:button class="zen-big-accent-button primary zen-share-overlay-add"
|
||||||
|
hidden="hidden" disabled="disabled"/>
|
||||||
|
</html:div>
|
||||||
|
`).firstElementChild;
|
||||||
|
const items = overlay.querySelector(".zen-share-overlay-items");
|
||||||
|
const statusEl = overlay.querySelector(".zen-share-overlay-status");
|
||||||
|
const footer = overlay.querySelector(".zen-share-overlay-footer");
|
||||||
|
const badgeIcon = overlay.querySelector(".zen-share-overlay-badge-icon");
|
||||||
|
const badgeTitle = overlay.querySelector(".zen-share-overlay-badge-title");
|
||||||
|
const badgeSubtitle = overlay.querySelector(
|
||||||
|
".zen-share-overlay-badge-subtitle"
|
||||||
|
);
|
||||||
|
const button = overlay.querySelector(".zen-share-overlay-add");
|
||||||
|
|
||||||
|
stack.appendChild(overlay);
|
||||||
|
browser._zenShareOverlay = overlay;
|
||||||
|
browser.setAttribute("zen-share-overlay-showing", "true");
|
||||||
|
|
||||||
|
let preview;
|
||||||
|
try {
|
||||||
|
preview = await lazy.ZenShareClient.fetchSharePreview(share);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("ZenShare: could not load share preview:", e);
|
||||||
|
const descriptions = {
|
||||||
|
"not-found": "zen-share-import-error-dead-description",
|
||||||
|
"invalid-document": "zen-share-import-error-invalid-description",
|
||||||
|
};
|
||||||
|
document.l10n.setAttributes(
|
||||||
|
statusEl,
|
||||||
|
descriptions[e.code] || "zen-share-error-network-description"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (browser._zenShareOverlay !== overlay) {
|
||||||
|
// Navigated away while loading.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { doc, name: sharerName } = preview;
|
||||||
|
const first = doc.shared[0];
|
||||||
|
const type = first.type;
|
||||||
|
|
||||||
|
badgeTitle.textContent = first.name;
|
||||||
|
this.#renderPreviewRows(items, first.items);
|
||||||
|
if (type === "folder") {
|
||||||
|
// The real zen folder icon, with the shared custom icon when set.
|
||||||
|
const folderIcon = this.#buildFolderIcon(first.icon);
|
||||||
|
folderIcon.classList.add("zen-share-overlay-badge-icon");
|
||||||
|
folderIcon.setAttribute("state", "open");
|
||||||
|
badgeIcon.replaceWith(folderIcon);
|
||||||
|
} else {
|
||||||
|
badgeIcon.src = "chrome://browser/skin/zen-icons/duplicate-tab.svg";
|
||||||
|
}
|
||||||
|
if (sharerName) {
|
||||||
|
document.l10n.setAttributes(
|
||||||
|
badgeSubtitle,
|
||||||
|
`zen-share-overlay-from-${type}`,
|
||||||
|
{ name: sharerName }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
document.l10n.setAttributes(
|
||||||
|
badgeSubtitle,
|
||||||
|
"zen-share-overlay-shared-with-you"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Mirror the badge subtitle under the tab's label, like live folders do.
|
||||||
|
const [tabSubtitle] = await document.l10n.formatValues([
|
||||||
|
sharerName
|
||||||
|
? { id: `zen-share-overlay-from-${type}`, args: { name: sharerName } }
|
||||||
|
: { id: "zen-share-overlay-shared-with-you" },
|
||||||
|
]);
|
||||||
|
this.#setHostTabSubtitle(browser, tabSubtitle);
|
||||||
|
statusEl.remove();
|
||||||
|
footer.hidden = false;
|
||||||
|
document.l10n.setAttributes(button, `zen-share-overlay-add-${type}`);
|
||||||
|
button.hidden = false;
|
||||||
|
button.disabled = false;
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
button.disabled = true;
|
||||||
|
try {
|
||||||
|
await this.#importDocument(doc);
|
||||||
|
gZenUIManager.showToast("zen-share-imported-toast");
|
||||||
|
const tab = gBrowser.getTabForBrowser(browser);
|
||||||
|
if (tab) {
|
||||||
|
gBrowser.removeTab(tab, { closeWindowWithLastTab: false });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("ZenShare: could not import share:", e);
|
||||||
|
gZenUIManager.showToast("zen-share-import-error-toast", {
|
||||||
|
timeout: 6000,
|
||||||
|
});
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#renderPreviewRows(container, entries) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.type === "folder") {
|
||||||
|
container.appendChild(this.#buildFolderRow(entry));
|
||||||
|
} else if (entry.type === "splitView") {
|
||||||
|
const split = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<html:div class="zen-share-overlay-split"/>
|
||||||
|
`).firstElementChild;
|
||||||
|
for (const tab of entry.tabs) {
|
||||||
|
split.appendChild(this.#buildTabRow(tab));
|
||||||
|
}
|
||||||
|
container.appendChild(split);
|
||||||
|
} else {
|
||||||
|
container.appendChild(this.#buildTabRow(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildTabRow(entry) {
|
||||||
|
const row = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<html:div class="zen-share-overlay-row">
|
||||||
|
<html:img class="zen-share-overlay-row-icon" draggable="false"/>
|
||||||
|
<html:div class="zen-share-overlay-row-label"/>
|
||||||
|
</html:div>
|
||||||
|
`).firstElementChild;
|
||||||
|
row.title = entry.url;
|
||||||
|
row.querySelector(".zen-share-overlay-row-icon").src =
|
||||||
|
entry.image && SHAREABLE_ICON_RE.test(entry.image)
|
||||||
|
? entry.image
|
||||||
|
: `page-icon:${entry.url}`;
|
||||||
|
row.querySelector(".zen-share-overlay-row-label").textContent =
|
||||||
|
entry.label || entry.url;
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
const stackRect = row.closest(".browserStack").getBoundingClientRect();
|
||||||
|
const data = {
|
||||||
|
url: entry.url,
|
||||||
|
clientX: stackRect.width / 2,
|
||||||
|
clientY: stackRect.height / 2,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
triggeringPrincipal:
|
||||||
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
||||||
|
};
|
||||||
|
gZenGlanceManager.lastLinkClickData = data;
|
||||||
|
gZenGlanceManager.openGlance(data);
|
||||||
|
});
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildFolderRow(entry) {
|
||||||
|
const wrapper = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<html:div class="zen-share-overlay-folder" collapsed="true">
|
||||||
|
<html:div class="zen-share-overlay-row">
|
||||||
|
<html:div class="zen-share-overlay-row-label"/>
|
||||||
|
</html:div>
|
||||||
|
<html:div class="zen-share-overlay-children-clip">
|
||||||
|
<html:div class="zen-share-overlay-children"/>
|
||||||
|
</html:div>
|
||||||
|
</html:div>
|
||||||
|
`).firstElementChild;
|
||||||
|
const row = wrapper.querySelector(".zen-share-overlay-row");
|
||||||
|
const label = row.querySelector(".zen-share-overlay-row-label");
|
||||||
|
label.textContent = entry.name;
|
||||||
|
const icon = this.#buildFolderIcon(entry.icon);
|
||||||
|
row.insertBefore(icon, label);
|
||||||
|
this.#renderPreviewRows(
|
||||||
|
wrapper.querySelector(".zen-share-overlay-children"),
|
||||||
|
entry.items
|
||||||
|
);
|
||||||
|
row.addEventListener("click", () => {
|
||||||
|
const collapsed = wrapper.toggleAttribute("collapsed");
|
||||||
|
if (collapsed) {
|
||||||
|
icon.removeAttribute("state");
|
||||||
|
} else {
|
||||||
|
icon.setAttribute("state", "open");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
#buildFolderIcon(customIcon) {
|
||||||
|
const { nsZenFolder } = ChromeUtils.importESModule(
|
||||||
|
"chrome://browser/content/zen-components/ZenFolder.mjs",
|
||||||
|
{ global: "current" }
|
||||||
|
);
|
||||||
|
const svg = nsZenFolder.rawIcon.cloneNode(true);
|
||||||
|
svg.classList.add("zen-share-overlay-folder-icon");
|
||||||
|
if (customIcon && FOLDER_ICON_RE.test(customIcon)) {
|
||||||
|
svg.querySelector(".icon image").setAttribute("href", customIcon);
|
||||||
|
}
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #importDocument(doc) {
|
||||||
|
for (const item of doc.shared) {
|
||||||
|
switch (item.type) {
|
||||||
|
case "space":
|
||||||
|
await this.#importSpace(item);
|
||||||
|
break;
|
||||||
|
case "folder":
|
||||||
|
this.#importFolder(item, gZenWorkspaces.activeWorkspace);
|
||||||
|
break;
|
||||||
|
case "splitView":
|
||||||
|
this.#importSplitView(item);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #importSpace(item) {
|
||||||
|
const workspace = await gZenWorkspaces.createAndSaveWorkspace(
|
||||||
|
item.name,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
beforeChangeCallback: async newWorkspace => {
|
||||||
|
for (const child of item.items) {
|
||||||
|
if (child.type === "tab") {
|
||||||
|
const tab = this.#importTab(child, newWorkspace.uuid);
|
||||||
|
if (child.isPinned) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
|
this.#placeTab(tab, newWorkspace.uuid, !!child.isPinned);
|
||||||
|
} else if (child.type === "folder") {
|
||||||
|
this.#importFolder(child, newWorkspace.uuid);
|
||||||
|
} else if (child.type === "splitView") {
|
||||||
|
this.#importSplitView(child, newWorkspace.uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workspace.name = item.name;
|
||||||
|
const theme = item.theme;
|
||||||
|
if (theme) {
|
||||||
|
workspace.theme = nsZenThemePicker.getTheme(
|
||||||
|
theme.gradientColors.map(color => this.#importGradientColor(color))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
gZenWorkspaces.saveWorkspace(workspace);
|
||||||
|
gZenThemePicker.onWorkspaceChange(workspace);
|
||||||
|
gZenStartup.playWindowSweepAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
#importGradientColor(color) {
|
||||||
|
if (color.isCustom) {
|
||||||
|
const [r, g, b] = color.c;
|
||||||
|
return { ...color, c: `rgb(${r}, ${g}, ${b})` };
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
#importFolder(item, workspaceId, parentFolder = null) {
|
||||||
|
const folder = gZenFolders.createFolder([], {
|
||||||
|
label: item.name,
|
||||||
|
collapsed: true,
|
||||||
|
workspaceId,
|
||||||
|
insertAfter: parentFolder
|
||||||
|
? parentFolder.groupContainer.lastElementChild
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
if (item.icon && FOLDER_ICON_RE.test(item.icon)) {
|
||||||
|
gZenFolders.setFolderUserIcon(folder, item.icon);
|
||||||
|
}
|
||||||
|
for (const child of item.items) {
|
||||||
|
if (child.type === "tab") {
|
||||||
|
const tab = this.#importTab(child, workspaceId);
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
folder.addTabs([tab]);
|
||||||
|
} else if (child.type === "folder") {
|
||||||
|
this.#importFolder(child, workspaceId, folder);
|
||||||
|
} else if (child.type === "splitView") {
|
||||||
|
this.#importSplitView(child, workspaceId, folder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
#importSplitView(item, workspaceId, folder = null) {
|
||||||
|
workspaceId ??= gZenWorkspaces.activeWorkspace;
|
||||||
|
const pinned = item.tabs.every(child => child.isPinned);
|
||||||
|
const tabs = item.tabs.map(child => {
|
||||||
|
const tab = this.#importTab(child, workspaceId);
|
||||||
|
if (folder) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
folder.addTabs([tab]);
|
||||||
|
} else {
|
||||||
|
if (pinned) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
|
this.#placeTab(tab, workspaceId, pinned);
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
gZenViewSplitter.splitTabs(tabs, "grid", -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deterministic placement at the end of the right section;
|
||||||
|
// moveTabToWorkspace's anchor follows the new-tab-button position pref.
|
||||||
|
#placeTab(tab, workspaceId, pinned) {
|
||||||
|
const element = gZenWorkspaces.workspaceElement(workspaceId);
|
||||||
|
const container = pinned
|
||||||
|
? element.pinnedTabsContainer
|
||||||
|
: element.tabsContainer;
|
||||||
|
const anchor = pinned
|
||||||
|
? container.querySelector(".pinned-tabs-container-separator")
|
||||||
|
: container.querySelector("#tabbrowser-arrowscrollbox-periphery");
|
||||||
|
gBrowser.zenHandleTabMove(tab, () => {
|
||||||
|
tab.setAttribute("zen-workspace-id", workspaceId);
|
||||||
|
container.insertBefore(tab, anchor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#importTab(item, workspaceId, { lazyBrowser = true } = {}) {
|
||||||
|
const tab = gBrowser.addTrustedTab(item.url, {
|
||||||
|
createLazyBrowser: lazyBrowser,
|
||||||
|
inBackground: true,
|
||||||
|
skipAnimation: true,
|
||||||
|
skipBackgroundNotify: true,
|
||||||
|
lazyTabTitle: item.label || undefined,
|
||||||
|
skipRoute: true,
|
||||||
|
});
|
||||||
|
if (workspaceId) {
|
||||||
|
tab.setAttribute("zen-workspace-id", workspaceId);
|
||||||
|
}
|
||||||
|
if (item.image && SHAREABLE_ICON_RE.test(item.image)) {
|
||||||
|
try {
|
||||||
|
gBrowser.setIcon(tab, item.image);
|
||||||
|
lazy.TabStateCache.update(tab.linkedBrowser.permanentKey, {
|
||||||
|
image: null,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("ZenShare: could not set imported tab icon:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark: split view context menu
|
||||||
|
|
||||||
|
#insertSplitViewMenuItem() {
|
||||||
|
const fragment = window.MozXULElement.parseXULToFragment(`
|
||||||
|
<menuitem id="context_zenShareSplitView"
|
||||||
|
data-lazy-l10n-id="zen-share-split-view"
|
||||||
|
hidden="true"/>
|
||||||
|
`);
|
||||||
|
document.getElementById("context_moveTabToSplitView").before(fragment);
|
||||||
|
const menuItem = document.getElementById("context_zenShareSplitView");
|
||||||
|
menuItem.addEventListener("command", () => {
|
||||||
|
const group = TabContextMenu.contextTab?.group;
|
||||||
|
if (group?.hasAttribute("split-view-group")) {
|
||||||
|
this.shareSplitView(group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document
|
||||||
|
.getElementById("tabContextMenu")
|
||||||
|
.addEventListener("popupshowing", () => {
|
||||||
|
const contextTab = TabContextMenu.contextTab;
|
||||||
|
const selectedTabs = contextTab?.multiselected
|
||||||
|
? gBrowser.selectedTabs
|
||||||
|
: [contextTab];
|
||||||
|
menuItem.hidden =
|
||||||
|
!contextTab ||
|
||||||
|
!selectedTabs.every(tab =>
|
||||||
|
tab?.group?.hasAttribute("split-view-group")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.gZenShareManager = new nsZenShareManager();
|
||||||
7
src/zen/share/jar.inc.mn
Normal file
7
src/zen/share/jar.inc.mn
Normal file
@@ -0,0 +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/.
|
||||||
|
|
||||||
|
* content/browser/zen-styles/zen-share.css (../../zen/share/zen-share.css)
|
||||||
|
* content/browser/zen-components/windows/zen-share-confirm.xhtml (../../zen/share/zen-share-confirm.inc.xhtml)
|
||||||
|
content/browser/zen-components/windows/zen-share-confirm.js (../../zen/share/zen-share-confirm.js)
|
||||||
@@ -4,4 +4,6 @@
|
|||||||
|
|
||||||
EXTRA_JS_MODULES.zen.share += [
|
EXTRA_JS_MODULES.zen.share += [
|
||||||
"share.schema.json",
|
"share.schema.json",
|
||||||
|
"ZenShareClient.sys.mjs",
|
||||||
|
"ZenShareManager.mjs",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -22,21 +22,10 @@
|
|||||||
"oneOf": [
|
"oneOf": [
|
||||||
{ "$ref": "#/$defs/space" },
|
{ "$ref": "#/$defs/space" },
|
||||||
{ "$ref": "#/$defs/folder" },
|
{ "$ref": "#/$defs/folder" },
|
||||||
{ "$ref": "#/$defs/splitView" },
|
{ "$ref": "#/$defs/splitView" }
|
||||||
{ "$ref": "#/$defs/gradientPreset" }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"gradientPreset": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"type": { "const": "gradientPreset" },
|
|
||||||
"theme": { "$ref": "#/$defs/theme" }
|
|
||||||
},
|
|
||||||
"required": ["type", "theme"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"space": {
|
"space": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -144,11 +133,19 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"spaceItem": {
|
"spaceItem": {
|
||||||
"oneOf": [{ "$ref": "#/$defs/tab" }, { "$ref": "#/$defs/folder" }]
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/tab" },
|
||||||
|
{ "$ref": "#/$defs/folder" },
|
||||||
|
{ "$ref": "#/$defs/splitView" }
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"folderItem": {
|
"folderItem": {
|
||||||
"oneOf": [{ "$ref": "#/$defs/tab" }, { "$ref": "#/$defs/folder" }]
|
"oneOf": [
|
||||||
|
{ "$ref": "#/$defs/tab" },
|
||||||
|
{ "$ref": "#/$defs/folder" },
|
||||||
|
{ "$ref": "#/$defs/splitView" }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/zen/share/zen-share-confirm.inc.xhtml
Normal file
61
src/zen/share/zen-share-confirm.inc.xhtml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#filter substitution
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
# -*- Mode: HTML -*-
|
||||||
|
#
|
||||||
|
# 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/.
|
||||||
|
|
||||||
|
<!DOCTYPE window>
|
||||||
|
|
||||||
|
<window
|
||||||
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
||||||
|
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||||
|
id="zenShareConfirmDialog"
|
||||||
|
data-l10n-id="zen-share-confirm-window"
|
||||||
|
data-l10n-attrs="title"
|
||||||
|
>
|
||||||
|
<dialog
|
||||||
|
buttons="accept,cancel"
|
||||||
|
data-l10n-id="zen-share-confirm-dialog"
|
||||||
|
data-l10n-attrs="buttonlabelaccept"
|
||||||
|
>
|
||||||
|
<linkset>
|
||||||
|
<html:link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="chrome://global/skin/global.css"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
<html:link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="chrome://browser/content/zen-styles/zen-theme.css"
|
||||||
|
type="text/css"
|
||||||
|
/>
|
||||||
|
<html:link rel="localization" href="browser/zen-share.ftl" />
|
||||||
|
</linkset>
|
||||||
|
|
||||||
|
<script src="chrome://browser/content/zen-components/windows/zen-share-confirm.js"></script>
|
||||||
|
|
||||||
|
<vbox>
|
||||||
|
<html:h2
|
||||||
|
data-l10n-id="zen-share-confirm-title"
|
||||||
|
></html:h2>
|
||||||
|
<description data-l10n-id="zen-share-confirm-description" />
|
||||||
|
<description
|
||||||
|
style="margin: 12px 0 16px;"
|
||||||
|
data-l10n-id="zen-share-confirm-anonymous"
|
||||||
|
/>
|
||||||
|
<html:input
|
||||||
|
id="zen-share-confirm-name"
|
||||||
|
type="text"
|
||||||
|
maxlength="200"
|
||||||
|
data-l10n-id="zen-share-confirm-name-input"
|
||||||
|
/>
|
||||||
|
<checkbox
|
||||||
|
id="zen-share-confirm-dont-ask"
|
||||||
|
data-l10n-id="zen-share-confirm-dont-ask"
|
||||||
|
/>
|
||||||
|
</vbox>
|
||||||
|
</dialog>
|
||||||
|
</window>
|
||||||
27
src/zen/share/zen-share-confirm.js
Normal file
27
src/zen/share/zen-share-confirm.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
{
|
||||||
|
const args = window.arguments?.[0] || {};
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"load",
|
||||||
|
() => {
|
||||||
|
const nameInput = document.getElementById("zen-share-confirm-name");
|
||||||
|
nameInput.value = args.name || "";
|
||||||
|
nameInput.focus();
|
||||||
|
|
||||||
|
document.addEventListener("dialogaccept", () => {
|
||||||
|
args.accepted = true;
|
||||||
|
args.name = nameInput.value.trim();
|
||||||
|
args.dontAsk = document.getElementById(
|
||||||
|
"zen-share-confirm-dont-ask"
|
||||||
|
).checked;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
259
src/zen/share/zen-share.css
Normal file
259
src/zen/share/zen-share.css
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
/* 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-share-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 32px;
|
||||||
|
background: var(--zen-transparent-browser-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
browser[zen-share-overlay-showing] {
|
||||||
|
-moz-subtree-hidden-only-visually: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 325px;
|
||||||
|
max-height: 76%;
|
||||||
|
border-radius: 30px;
|
||||||
|
background: var(--zen-dialog-background);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 3px rgba(0, 0, 0, 0.07),
|
||||||
|
0 14px 40px -10px rgba(0, 0, 0, 0.28);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: zen-share-overlay-enter 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-add {
|
||||||
|
min-width: 260px;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 16px 48px;
|
||||||
|
border-radius: 18px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
animation: zen-share-overlay-enter 0.35s cubic-bezier(0.2, 0.8, 0.2, 1) 0.06s
|
||||||
|
both;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.5;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes zen-share-overlay-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px) scale(0.98);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
padding: 22px 20px 16px;
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
transparent,
|
||||||
|
black 24px,
|
||||||
|
black calc(100% - 20px),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color-mix(in srgb, currentColor 7%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: color-mix(in srgb, currentColor 11%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-row-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-folder-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: -3px;
|
||||||
|
|
||||||
|
%include ../folders/zen-folder-icon.inc.css
|
||||||
|
|
||||||
|
/* The dots stand in for the icon on active sidebar folders; the preview
|
||||||
|
* always shows the plain folder. */
|
||||||
|
& .dots {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-row-label {
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-split {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
& .zen-share-overlay-row {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .zen-share-overlay-row:not(:first-child) {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset-block: 6px;
|
||||||
|
inset-inline-start: -5.5px;
|
||||||
|
width: 1px;
|
||||||
|
background: var(--zen-colors-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-children-clip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
transition: grid-template-rows 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-folder[collapsed] > .zen-share-overlay-children-clip {
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-children {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-inline-start: 28px;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-status {
|
||||||
|
padding: 28px;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-footer {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 22px 20px 20px;
|
||||||
|
border-top: 2px dashed
|
||||||
|
color-mix(in srgb, var(--zen-primary-color) 22%, var(--zen-colors-border));
|
||||||
|
background: color-mix(
|
||||||
|
in srgb,
|
||||||
|
var(--zen-primary-color) 7%,
|
||||||
|
var(--zen-dialog-background)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge-stack {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid
|
||||||
|
light-dark(
|
||||||
|
color-mix(in srgb, currentColor 12%, var(--zen-colors-border)),
|
||||||
|
color-mix(in srgb, currentColor 8%, transparent)
|
||||||
|
);
|
||||||
|
background: light-dark(#ffffff, #1c1c1e);
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge-sheet {
|
||||||
|
position: absolute;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid
|
||||||
|
light-dark(
|
||||||
|
color-mix(in srgb, currentColor 12%, var(--zen-colors-border)),
|
||||||
|
color-mix(in srgb, currentColor 8%, transparent)
|
||||||
|
);
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07);
|
||||||
|
|
||||||
|
&.first {
|
||||||
|
z-index: 1;
|
||||||
|
inset-inline: 10px;
|
||||||
|
top: -7px;
|
||||||
|
background: color-mix(in srgb, currentColor 2%, light-dark(#ffffff, #1c1c1e));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.second {
|
||||||
|
z-index: 0;
|
||||||
|
inset-inline: 22px;
|
||||||
|
top: -13px;
|
||||||
|
background: color-mix(in srgb, currentColor 4%, light-dark(#ffffff, #1c1c1e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge-icon {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
-moz-context-properties: fill;
|
||||||
|
fill: color-mix(in srgb, var(--zen-accent-button-color) 60%, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge-text {
|
||||||
|
min-width: 0;
|
||||||
|
color: color-mix(in srgb, var(--zen-accent-button-color) 30%, currentColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge-title {
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zen-share-overlay-badge-subtitle {
|
||||||
|
opacity: 0.6;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
@@ -2341,9 +2341,13 @@ class nsZenWorkspaces {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
tabToSelect = lastSelectedTab;
|
tabToSelect = lastSelectedTab;
|
||||||
|
} else if (!onInit && !tabToSelect) {
|
||||||
|
// Create new tab if needed and no suitable tab was found
|
||||||
|
tabToSelect = this._emptyTab;
|
||||||
}
|
}
|
||||||
// Find first suitable tab
|
// Find first suitable tab
|
||||||
else {
|
// If we found a tab to select, select it
|
||||||
|
if (!tabToSelect || tabToSelect.closing) {
|
||||||
tabToSelect = gBrowser.visibleTabs.find(tab => !tab.pinned);
|
tabToSelect = gBrowser.visibleTabs.find(tab => !tab.pinned);
|
||||||
if (!tabToSelect && gBrowser.visibleTabs.length) {
|
if (!tabToSelect && gBrowser.visibleTabs.length) {
|
||||||
tabToSelect = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
|
tabToSelect = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
|
||||||
@@ -2354,12 +2358,6 @@ class nsZenWorkspaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we found a tab to select, select it
|
|
||||||
if (!onInit && !tabToSelect) {
|
|
||||||
// Create new tab if needed and no suitable tab was found
|
|
||||||
const newTab = this.selectEmptyTab();
|
|
||||||
tabToSelect = newTab;
|
|
||||||
}
|
|
||||||
if (tabToSelect && !onInit) {
|
if (tabToSelect && !onInit) {
|
||||||
tabToSelect._visuallySelected = true;
|
tabToSelect._visuallySelected = true;
|
||||||
}
|
}
|
||||||
@@ -2890,6 +2888,12 @@ class nsZenWorkspaces {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
contextShareWorkspace() {
|
||||||
|
const workspaceId =
|
||||||
|
this.#contextMenuData?.workspaceId || this.activeWorkspace;
|
||||||
|
gZenShareManager.shareSpace(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
async contextDeleteWorkspace() {
|
async contextDeleteWorkspace() {
|
||||||
const workspaceId =
|
const workspaceId =
|
||||||
this.#contextMenuData?.workspaceId || this.activeWorkspace;
|
this.#contextMenuData?.workspaceId || this.activeWorkspace;
|
||||||
|
|||||||
@@ -329,7 +329,6 @@ zen-workspace {
|
|||||||
color-mix(in srgb, rgba(255, 255, 255, 0.18) 95%, var(--zen-primary-color))
|
color-mix(in srgb, rgba(255, 255, 255, 0.18) 95%, var(--zen-primary-color))
|
||||||
) !important;
|
) !important;
|
||||||
--tab-box-shadow-selected: 0 0.8px 1.5px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.05)) !important;
|
--tab-box-shadow-selected: 0 0.8px 1.5px 0px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.05)) !important;
|
||||||
--tab-selected-textcolor: color-mix(in srgb, var(--toolbox-textcolor) 95%, var(--zen-primary-color)) !important;
|
|
||||||
|
|
||||||
@media not (prefers-reduced-motion: reduce) {
|
@media not (prefers-reduced-motion: reduce) {
|
||||||
transition: padding-top 0.1s;
|
transition: padding-top 0.1s;
|
||||||
|
|||||||
@@ -75,10 +75,9 @@ class ZenSpacesSyncStore extends Store {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tracker for the Spaces engine. There is no per-event bookkeeping: every
|
* Tracker for the Spaces engine. The model diffs the current projections
|
||||||
* time fresh sidebar data is collected (or a container changes), the model
|
* against the last-uploaded snapshot. The score is only bumped when
|
||||||
* diffs the current projections against the last-uploaded snapshot; the
|
* something actually differs.
|
||||||
* score is only bumped when something actually differs.
|
|
||||||
*/
|
*/
|
||||||
class ZenSpacesSyncTracker extends Tracker {
|
class ZenSpacesSyncTracker extends Tracker {
|
||||||
_ignoreAll = false;
|
_ignoreAll = false;
|
||||||
|
|||||||
@@ -11,8 +11,17 @@ import {
|
|||||||
ZenSpacesSyncModel,
|
ZenSpacesSyncModel,
|
||||||
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
|
} from "resource:///modules/zen/ZenSpacesSyncModel.sys.mjs";
|
||||||
|
|
||||||
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
||||||
|
|
||||||
const lazy = {};
|
const lazy = {};
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"syncNormalTabs",
|
||||||
|
"zen.spaces-sync.normal-tabs",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
ChromeUtils.defineESModuleGetters(lazy, {
|
ChromeUtils.defineESModuleGetters(lazy, {
|
||||||
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
|
SessionSaver: "resource:///modules/sessionstore/SessionSaver.sys.mjs",
|
||||||
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs",
|
||||||
@@ -84,6 +93,11 @@ class nsZenSpacesSyncApplier {
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (data.pinned === false && !lazy.syncNormalTabs) {
|
||||||
|
// Normal-tab syncing is off here: leave the record untouched and
|
||||||
|
// unacknowledged, like an unknown kind.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const entry = { key: record.id, data, record };
|
const entry = { key: record.id, data, record };
|
||||||
switch (record.cleartext.kind) {
|
switch (record.cleartext.kind) {
|
||||||
case RECORD_KINDS.CONTAINER:
|
case RECORD_KINDS.CONTAINER:
|
||||||
@@ -567,7 +581,9 @@ class nsZenSpacesSyncApplier {
|
|||||||
if (data.workspaceUuid) {
|
if (data.workspaceUuid) {
|
||||||
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
|
tab.setAttribute("zen-workspace-id", data.workspaceUuid);
|
||||||
}
|
}
|
||||||
|
if (data.pinned !== false) {
|
||||||
win.gBrowser.pinTab(tab);
|
win.gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
if (data.workspaceUuid) {
|
if (data.workspaceUuid) {
|
||||||
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
|
win.gZenWorkspaces.moveTabToWorkspace(tab, data.workspaceUuid);
|
||||||
}
|
}
|
||||||
@@ -593,7 +609,10 @@ class nsZenSpacesSyncApplier {
|
|||||||
const identityChanged =
|
const identityChanged =
|
||||||
initial?.entry?.url !== data.url ||
|
initial?.entry?.url !== data.url ||
|
||||||
(initial?.entry?.title || "") !== (data.title || "");
|
(initial?.entry?.title || "") !== (data.title || "");
|
||||||
if (identityChanged || syncableIconUrl(initial?.image || "") !== icon) {
|
if (
|
||||||
|
data.pinned !== false &&
|
||||||
|
(identityChanged || syncableIconUrl(initial?.image || "") !== icon)
|
||||||
|
) {
|
||||||
lazy.ZenWindowSync.setPinnedInitialState(
|
lazy.ZenWindowSync.setPinnedInitialState(
|
||||||
tab,
|
tab,
|
||||||
{ url: data.url, title: data.title || "" },
|
{ url: data.url, title: data.title || "" },
|
||||||
@@ -745,6 +764,17 @@ class nsZenSpacesSyncApplier {
|
|||||||
// The split record governs placement of its members.
|
// The split record governs placement of its members.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const wantPinned = data.pinned !== false;
|
||||||
|
if (wantPinned !== tab.pinned) {
|
||||||
|
if (wantPinned) {
|
||||||
|
win.gBrowser.pinTab(tab);
|
||||||
|
} else {
|
||||||
|
win.gBrowser.unpinTab(tab);
|
||||||
|
// Pin identity would otherwise freeze the projection of what is now
|
||||||
|
// a normal tab.
|
||||||
|
delete tab._zenPinnedInitialState;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.#applyFolderMembership(win, tab, data.folderId);
|
this.#applyFolderMembership(win, tab, data.folderId);
|
||||||
if (
|
if (
|
||||||
!data.folderId &&
|
!data.folderId &&
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ XPCOMUtils.defineLazyPreferenceGetter(
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyPreferenceGetter(
|
||||||
|
lazy,
|
||||||
|
"syncNormalTabs",
|
||||||
|
"zen.spaces-sync.normal-tabs",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debug logging for the whole Spaces sync pipeline.
|
* Debug logging for the whole Spaces sync pipeline.
|
||||||
*
|
*
|
||||||
@@ -258,19 +265,31 @@ class nsZenSpacesSyncModel {
|
|||||||
|
|
||||||
/* Mark: projections */
|
/* Mark: projections */
|
||||||
|
|
||||||
#isSyncableTab(tabData) {
|
#isTabRecordMaterial(tabData) {
|
||||||
return !!(
|
return !!(
|
||||||
tabData &&
|
tabData &&
|
||||||
tabData.zenSyncId &&
|
tabData.zenSyncId &&
|
||||||
(tabData.pinned || tabData.zenEssential) &&
|
|
||||||
!tabData.zenIsEmpty &&
|
!tabData.zenIsEmpty &&
|
||||||
!tabData.zenIsGlance &&
|
!tabData.zenIsGlance &&
|
||||||
!tabData.zenLiveFolderItemId
|
!tabData.zenLiveFolderItemId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#isSyncableTab(tabData) {
|
||||||
|
return (
|
||||||
|
this.#isTabRecordMaterial(tabData) &&
|
||||||
|
(!!tabData.pinned || !!tabData.zenEssential || lazy.syncNormalTabs)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#tabIdentity(tabData) {
|
#tabIdentity(tabData) {
|
||||||
const initial = tabData._zenPinnedInitialState;
|
// Pin identity freezes url/title at pin time. A normal tab's identity
|
||||||
|
// follows its live entry (a leftover initial state from a past pin must
|
||||||
|
// not shadow it).
|
||||||
|
const initial =
|
||||||
|
tabData.pinned || tabData.zenEssential
|
||||||
|
? tabData._zenPinnedInitialState
|
||||||
|
: null;
|
||||||
let url = initial?.entry?.url;
|
let url = initial?.entry?.url;
|
||||||
let title = initial?.entry?.title;
|
let title = initial?.entry?.title;
|
||||||
if (!url || url === "about:blank") {
|
if (!url || url === "about:blank") {
|
||||||
@@ -473,6 +492,7 @@ class nsZenSpacesSyncModel {
|
|||||||
create: true,
|
create: true,
|
||||||
}),
|
}),
|
||||||
essential,
|
essential,
|
||||||
|
pinned: !!(tab.pinned || tab.zenEssential),
|
||||||
workspaceUuid: essential ? null : tab.zenWorkspace || null,
|
workspaceUuid: essential ? null : tab.zenWorkspace || null,
|
||||||
folderId: ctx.folderOf(tab.groupId || null),
|
folderId: ctx.folderOf(tab.groupId || null),
|
||||||
staticLabel:
|
staticLabel:
|
||||||
@@ -494,8 +514,52 @@ class nsZenSpacesSyncModel {
|
|||||||
const map = new Map();
|
const map = new Map();
|
||||||
const pending = new Set();
|
const pending = new Set();
|
||||||
const ctx = this.#projectionContext(sidebar);
|
const ctx = this.#projectionContext(sidebar);
|
||||||
const { tabs, folders, splits, splitParents, splitWs } = ctx;
|
const spaces = sidebar.spaces || [];
|
||||||
|
|
||||||
|
this.#collectHeldIds(ctx, sidebar, pending);
|
||||||
|
this.#projectContainers(map);
|
||||||
|
this.#projectSpaces(map, ctx, spaces);
|
||||||
|
this.#projectFolders(map, ctx, pending);
|
||||||
|
this.#projectTabs(map, ctx);
|
||||||
|
this.#projectSplits(map, ctx);
|
||||||
|
this.#projectLayout(map, ctx, spaces);
|
||||||
|
|
||||||
|
this.#cache = { stamp, map, pending };
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Items excluded only by the normal-tabs option are held back, not
|
||||||
|
* deleted. Flipping the option off must not tombstone them remotely.
|
||||||
|
*
|
||||||
|
* @param {object} ctx - The projection context.
|
||||||
|
* @param {object} sidebar - The collected sidebar data.
|
||||||
|
* @param {Set<string>} pending - Receives the held-back ids.
|
||||||
|
*/
|
||||||
|
#collectHeldIds(ctx, sidebar, pending) {
|
||||||
|
if (lazy.syncNormalTabs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const held = new Set();
|
||||||
|
for (const tab of ctx.allTabs) {
|
||||||
|
if (this.#isTabRecordMaterial(tab) && !this.#isSyncableTab(tab)) {
|
||||||
|
held.add(tab.zenSyncId);
|
||||||
|
pending.add(tab.zenSyncId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const split of sidebar.splitViewData || []) {
|
||||||
|
if (
|
||||||
|
split?.groupId &&
|
||||||
|
!ctx.splitIds.has(split.groupId) &&
|
||||||
|
Array.isArray(split.tabs) &&
|
||||||
|
split.tabs.some(id => held.has(id))
|
||||||
|
) {
|
||||||
|
pending.add(split.groupId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#projectContainers(map) {
|
||||||
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
|
for (const identity of lazy.ContextualIdentityService.getPublicIdentities()) {
|
||||||
if (!identity.name) {
|
if (!identity.name) {
|
||||||
continue;
|
continue;
|
||||||
@@ -516,8 +580,9 @@ class nsZenSpacesSyncModel {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const spaces = sidebar.spaces || [];
|
#projectSpaces(map, ctx, spaces) {
|
||||||
for (const space of spaces) {
|
for (const space of spaces) {
|
||||||
if (!space?.uuid) {
|
if (!space?.uuid) {
|
||||||
continue;
|
continue;
|
||||||
@@ -537,8 +602,10 @@ class nsZenSpacesSyncModel {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const folder of folders) {
|
#projectFolders(map, ctx, pending) {
|
||||||
|
for (const folder of ctx.folders) {
|
||||||
const fid = folder.id;
|
const fid = folder.id;
|
||||||
let live = null;
|
let live = null;
|
||||||
if (folder.isLiveFolder) {
|
if (folder.isLiveFolder) {
|
||||||
@@ -565,31 +632,37 @@ class nsZenSpacesSyncModel {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.#projectTabs(map, ctx);
|
#projectSplits(map, ctx) {
|
||||||
|
const tabById = new Map(ctx.tabs.map(t => [t.zenSyncId, t]));
|
||||||
for (const split of splits) {
|
for (const split of ctx.splits) {
|
||||||
|
const member = tabById.get(split.tabs[0]);
|
||||||
map.set(split.groupId, {
|
map.set(split.groupId, {
|
||||||
kind: RECORD_KINDS.SPLIT,
|
kind: RECORD_KINDS.SPLIT,
|
||||||
data: {
|
data: {
|
||||||
splitId: split.groupId,
|
splitId: split.groupId,
|
||||||
gridType: split.gridType || "grid",
|
gridType: split.gridType || "grid",
|
||||||
|
pinned: !!(member?.pinned || member?.zenEssential),
|
||||||
tabs: [...split.tabs],
|
tabs: [...split.tabs],
|
||||||
workspaceUuid: splitWs.get(split.groupId) ?? null,
|
workspaceUuid: ctx.splitWs.get(split.groupId) ?? null,
|
||||||
folderId: splitParents.get(split.groupId) || null,
|
folderId: ctx.splitParents.get(split.groupId) || null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (spaces.length) {
|
#projectLayout(map, ctx, spaces) {
|
||||||
|
if (!spaces.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const essentials = {};
|
const essentials = {};
|
||||||
for (const tab of tabs) {
|
for (const tab of ctx.tabs) {
|
||||||
if (!tab.zenEssential) {
|
if (!tab.zenEssential) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const key =
|
const key =
|
||||||
this.guidForContextId(tab.userContextId, { create: true }) ||
|
this.guidForContextId(tab.userContextId, { create: true }) || "default";
|
||||||
"default";
|
|
||||||
(essentials[key] ||= []).push(tab.zenSyncId);
|
(essentials[key] ||= []).push(tab.zenSyncId);
|
||||||
}
|
}
|
||||||
map.set(LAYOUT_RECORD_ID, {
|
map.set(LAYOUT_RECORD_ID, {
|
||||||
@@ -601,10 +674,6 @@ class nsZenSpacesSyncModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#cache = { stamp, map, pending };
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ids that are locally present but deliberately not projected this cycle
|
* Ids that are locally present but deliberately not projected this cycle
|
||||||
* (live folders whose provider config isn't available yet). The diff must
|
* (live folders whose provider config isn't available yet). The diff must
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ BROWSER_CHROME_MANIFESTS += [
|
|||||||
"site_control/browser.toml",
|
"site_control/browser.toml",
|
||||||
"space_routing/browser.toml",
|
"space_routing/browser.toml",
|
||||||
"spaces/browser.toml",
|
"spaces/browser.toml",
|
||||||
|
"spaces_sync/browser.toml",
|
||||||
"split_view/browser.toml",
|
"split_view/browser.toml",
|
||||||
"tabs/browser.toml",
|
"tabs/browser.toml",
|
||||||
"ub-actions/browser.toml",
|
"ub-actions/browser.toml",
|
||||||
|
|||||||
17
src/zen/tests/spaces_sync/browser.toml
Normal file
17
src/zen/tests/spaces_sync/browser.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# 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/.
|
||||||
|
|
||||||
|
[DEFAULT]
|
||||||
|
prefs = [
|
||||||
|
"zen.window-sync.enabled=true",
|
||||||
|
"zen.urlbar.replace-newtab=false",
|
||||||
|
"zen.spaces-sync.first-sync-animation-shown=true",
|
||||||
|
]
|
||||||
|
support-files = [
|
||||||
|
"head.js",
|
||||||
|
]
|
||||||
|
|
||||||
|
["browser_normal_tabs_apply.js"]
|
||||||
|
|
||||||
|
["browser_normal_tabs_projection.js"]
|
||||||
129
src/zen/tests/spaces_sync/browser_normal_tabs_apply.js
Normal file
129
src/zen/tests/spaces_sync/browser_normal_tabs_apply.js
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
add_task(async function test_ApplySkippedWhileOptionOff() {
|
||||||
|
await gZenWorkspaces.promiseInitialized;
|
||||||
|
const id = "test-spaces-sync-normal-off";
|
||||||
|
const failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(
|
||||||
|
failed,
|
||||||
|
[],
|
||||||
|
"A skipped normal-tab record should not be reported as failed"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!document.getElementById(id),
|
||||||
|
"No tab should materialize while the option is off"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ApplyCreatesUnpinnedTab() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const id = "test-spaces-sync-normal-on";
|
||||||
|
const failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The record should apply cleanly");
|
||||||
|
|
||||||
|
const tab = document.getElementById(id);
|
||||||
|
Assert.ok(gBrowser.isTab(tab), "The tab should materialize");
|
||||||
|
Assert.ok(!tab.pinned, "The tab should not be pinned");
|
||||||
|
Assert.equal(
|
||||||
|
tab.getAttribute("zen-workspace-id"),
|
||||||
|
gZenWorkspaces.activeWorkspace,
|
||||||
|
"The tab should land in the record's workspace"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!tab._zenPinnedInitialState,
|
||||||
|
"A normal tab should carry no pin identity"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_UpdateRetargetsUnloadedNormalTab() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const id = "test-spaces-sync-retarget";
|
||||||
|
let failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false, url: "https://example.com/first" }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The record should apply cleanly");
|
||||||
|
const tab = document.getElementById(id);
|
||||||
|
Assert.ok(!tab.linkedPanel, "The synced tab should stay unloaded");
|
||||||
|
|
||||||
|
failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false, url: "https://example.com/second" }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The navigation record should apply cleanly");
|
||||||
|
Assert.ok(!tab.linkedPanel, "The tab should stay unloaded after retarget");
|
||||||
|
const state = JSON.parse(SessionStore.getTabState(tab));
|
||||||
|
Assert.equal(
|
||||||
|
state.entries.at(-1)?.url,
|
||||||
|
"https://example.com/second",
|
||||||
|
"The unloaded tab's state should point at the new url"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_UpdateRetargetsDiscardedNormalTab() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const tab = await openSyncableTab("https://example.com/?loadedfirst");
|
||||||
|
const id = tab.id;
|
||||||
|
await BrowserTestUtils.switchTab(gBrowser, gBrowser.tabs[0]);
|
||||||
|
gBrowser.discardBrowser(tab);
|
||||||
|
Assert.ok(!tab.linkedPanel, "The tab should be discarded");
|
||||||
|
|
||||||
|
const failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false, url: "https://example.com/?remotenav" }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The navigation record should apply cleanly");
|
||||||
|
Assert.ok(!tab.linkedPanel, "The tab should stay unloaded after retarget");
|
||||||
|
const state = JSON.parse(SessionStore.getTabState(tab));
|
||||||
|
Assert.equal(
|
||||||
|
state.entries.at(-1)?.url,
|
||||||
|
"https://example.com/?remotenav",
|
||||||
|
"The discarded tab's state should point at the new url"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_ApplyPinStateTransition() {
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const id = "test-spaces-sync-transition";
|
||||||
|
let failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: true }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The pinned record should apply cleanly");
|
||||||
|
const tab = document.getElementById(id);
|
||||||
|
Assert.ok(gBrowser.isTab(tab), "The tab should materialize");
|
||||||
|
Assert.ok(tab.pinned, "A pinned record should materialize pinned");
|
||||||
|
await TestUtils.waitForCondition(
|
||||||
|
() => tab._zenPinnedInitialState,
|
||||||
|
"waiting for the pin identity to settle"
|
||||||
|
);
|
||||||
|
|
||||||
|
failed = await ZenSpacesSyncApplier.applyBatch([
|
||||||
|
tabRecord(id, { pinned: false }),
|
||||||
|
]);
|
||||||
|
Assert.deepEqual(failed, [], "The demoting record should apply cleanly");
|
||||||
|
Assert.ok(!tab.pinned, "A record demotion should unpin the tab");
|
||||||
|
Assert.ok(
|
||||||
|
!tab._zenPinnedInitialState,
|
||||||
|
"Demotion should clear the pin identity"
|
||||||
|
);
|
||||||
|
|
||||||
|
ZenSpacesSyncModel.noteApplied(id, null);
|
||||||
|
BrowserTestUtils.removeTab(tab);
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
});
|
||||||
81
src/zen/tests/spaces_sync/browser_normal_tabs_projection.js
Normal file
81
src/zen/tests/spaces_sync/browser_normal_tabs_projection.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
add_task(async function test_ProjectionGatedByPref() {
|
||||||
|
await gZenWorkspaces.promiseInitialized;
|
||||||
|
const pinnedTab = await openSyncableTab("https://example.com/?pinned", {
|
||||||
|
pinned: true,
|
||||||
|
});
|
||||||
|
const normalTab = await openSyncableTab("https://example.com/?normal");
|
||||||
|
|
||||||
|
let projections = await collectProjections();
|
||||||
|
const pinnedRecord = projections.get(pinnedTab.id);
|
||||||
|
Assert.ok(pinnedRecord, "Pinned tab should project with the option off");
|
||||||
|
Assert.equal(
|
||||||
|
pinnedRecord.data.pinned,
|
||||||
|
true,
|
||||||
|
"Pinned tab record should carry pinned: true"
|
||||||
|
);
|
||||||
|
Assert.ok(
|
||||||
|
!projections.get(normalTab.id),
|
||||||
|
"Normal tab should not project with the option off"
|
||||||
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
ZenSpacesSyncModel.invalidate();
|
||||||
|
projections = ZenSpacesSyncModel.projections();
|
||||||
|
|
||||||
|
const record = projections.get(normalTab.id);
|
||||||
|
Assert.ok(record, "Normal tab should project with the option on");
|
||||||
|
Assert.equal(
|
||||||
|
record.data.pinned,
|
||||||
|
false,
|
||||||
|
"Normal tab record should carry pinned: false"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
record.data.workspaceUuid,
|
||||||
|
gZenWorkspaces.activeWorkspace,
|
||||||
|
"Normal tab record should name its workspace"
|
||||||
|
);
|
||||||
|
const space = projections.get(gZenWorkspaces.activeWorkspace);
|
||||||
|
Assert.ok(
|
||||||
|
space?.data?.children?.includes(normalTab.id),
|
||||||
|
"The space's child order should include the normal tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
BrowserTestUtils.removeTab(normalTab);
|
||||||
|
BrowserTestUtils.removeTab(pinnedTab);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(async function test_DisablingOptionHoldsBackInsteadOfTombstoning() {
|
||||||
|
await gZenWorkspaces.promiseInitialized;
|
||||||
|
await SpecialPowers.pushPrefEnv({ set: [[NORMAL_TABS_PREF, true]] });
|
||||||
|
const normalTab = await openSyncableTab("https://example.com/?heldback");
|
||||||
|
|
||||||
|
await collectProjections();
|
||||||
|
ZenSpacesSyncModel.markUploaded([normalTab.id]);
|
||||||
|
let changes = ZenSpacesSyncModel.computeChangedIDs();
|
||||||
|
Assert.ok(
|
||||||
|
!(normalTab.id in changes),
|
||||||
|
"An uploaded normal tab should report no pending change"
|
||||||
|
);
|
||||||
|
|
||||||
|
await SpecialPowers.popPrefEnv();
|
||||||
|
ZenSpacesSyncModel.invalidate();
|
||||||
|
Assert.ok(
|
||||||
|
!ZenSpacesSyncModel.projections().get(normalTab.id),
|
||||||
|
"Normal tab should stop projecting once the option is off"
|
||||||
|
);
|
||||||
|
changes = ZenSpacesSyncModel.computeChangedIDs();
|
||||||
|
Assert.ok(
|
||||||
|
!(normalTab.id in changes),
|
||||||
|
"Disabling the option should not tombstone the tab"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drop the snapshot entry so this task leaves no sync state behind.
|
||||||
|
ZenSpacesSyncModel.noteApplied(normalTab.id, null);
|
||||||
|
BrowserTestUtils.removeTab(normalTab);
|
||||||
|
});
|
||||||
74
src/zen/tests/spaces_sync/head.js
Normal file
74
src/zen/tests/spaces_sync/head.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
const { ZenSpacesSyncModel } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/zen/ZenSpacesSyncModel.sys.mjs"
|
||||||
|
);
|
||||||
|
const { ZenSpacesSyncApplier } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/zen/ZenSpacesSyncApplier.sys.mjs"
|
||||||
|
);
|
||||||
|
const { SessionSaver } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/sessionstore/SessionSaver.sys.mjs"
|
||||||
|
);
|
||||||
|
const { TabStateFlusher } = ChromeUtils.importESModule(
|
||||||
|
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
|
||||||
|
);
|
||||||
|
const { TestUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/TestUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
const { E10SUtils } = ChromeUtils.importESModule(
|
||||||
|
"resource://gre/modules/E10SUtils.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const NORMAL_TABS_PREF = "zen.spaces-sync.normal-tabs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a full session save (which collects the sidebar data) and returns
|
||||||
|
* fresh sync projections built from it.
|
||||||
|
*/
|
||||||
|
async function collectProjections() {
|
||||||
|
await SessionSaver.run();
|
||||||
|
ZenSpacesSyncModel.invalidate();
|
||||||
|
return ZenSpacesSyncModel.projections();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openSyncableTab(url, { pinned = false } = {}) {
|
||||||
|
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
|
||||||
|
if (pinned) {
|
||||||
|
gBrowser.pinTab(tab);
|
||||||
|
}
|
||||||
|
await TabStateFlusher.flush(tab.linkedBrowser);
|
||||||
|
ok(tab.id, "the tab should have a sync id");
|
||||||
|
return tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabRecord(
|
||||||
|
id,
|
||||||
|
{ pinned, url = "https://example.com/", current = null, workspaceUuid } = {}
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
deleted: false,
|
||||||
|
cleartext: {
|
||||||
|
kind: "tab",
|
||||||
|
data: {
|
||||||
|
tabId: id,
|
||||||
|
url,
|
||||||
|
title: "Synced tab",
|
||||||
|
current,
|
||||||
|
icon: null,
|
||||||
|
containerGuid: null,
|
||||||
|
essential: false,
|
||||||
|
pinned,
|
||||||
|
workspaceUuid: workspaceUuid ?? gZenWorkspaces.activeWorkspace,
|
||||||
|
folderId: null,
|
||||||
|
staticLabel: null,
|
||||||
|
hasStaticIcon: false,
|
||||||
|
defaultContainer: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -281,8 +281,8 @@
|
|||||||
for (const button of this.currentPage.buttons) {
|
for (const button of this.currentPage.buttons) {
|
||||||
const element = document.createElement("button");
|
const element = document.createElement("button");
|
||||||
element.className = button.primary
|
element.className = button.primary
|
||||||
? "zen-welcome-button primary"
|
? "zen-big-accent-button primary"
|
||||||
: "zen-welcome-button";
|
: "zen-big-accent-button";
|
||||||
document.l10n.setAttributes(element, button.l10n);
|
document.l10n.setAttributes(element, button.l10n);
|
||||||
element.addEventListener("click", () => {
|
element.addEventListener("click", () => {
|
||||||
if (button.onclick?.(this) !== false) {
|
if (button.onclick?.(this) !== false) {
|
||||||
|
|||||||
@@ -40,14 +40,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#zen-welcome {
|
#zen-welcome {
|
||||||
--zen-welcome-accent: light-dark(
|
--zen-welcome-accent: var(--zen-accent-button-color);
|
||||||
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
|
|
||||||
oklch(from var(--zen-primary-color) clamp(0.7, l, 0.85) c h)
|
|
||||||
);
|
|
||||||
--zen-welcome-accent-button: light-dark(
|
|
||||||
oklch(from var(--zen-primary-color) clamp(0.35, l, 0.6) c h),
|
|
||||||
oklch(from var(--zen-primary-color) clamp(0.55, l, 0.7) c h)
|
|
||||||
);
|
|
||||||
--zen-welcome-sidebar-bg: light-dark(#ffffff, #1c1c1e);
|
--zen-welcome-sidebar-bg: light-dark(#ffffff, #1c1c1e);
|
||||||
--zen-welcome-content-bg: light-dark(#f6f6f8f5, #2b2b30ed);
|
--zen-welcome-content-bg: light-dark(#f6f6f8f5, #2b2b30ed);
|
||||||
--zen-welcome-surface-bg: light-dark(#ffffff, #1c1c1e);
|
--zen-welcome-surface-bg: light-dark(#ffffff, #1c1c1e);
|
||||||
@@ -62,39 +55,6 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.zen-welcome-button {
|
|
||||||
appearance: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.2rem 1rem;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--zen-welcome-accent);
|
|
||||||
font: inherit;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
will-change: transform;
|
|
||||||
transition: scale 0.15s ease-out;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
scale: 1.03;
|
|
||||||
filter: brightness(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
scale: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: var(--zen-welcome-accent-button);
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
filter: brightness(1.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#zen-welcome-start {
|
#zen-welcome-start {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
-moz-window-dragging: drag;
|
-moz-window-dragging: drag;
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export default [
|
|||||||
|
|
||||||
"gZenViewSplitter",
|
"gZenViewSplitter",
|
||||||
|
|
||||||
|
"gZenShareManager",
|
||||||
|
|
||||||
"gZenSpaceRoutingManager",
|
"gZenSpaceRoutingManager",
|
||||||
|
|
||||||
"Ci",
|
"Ci",
|
||||||
|
|||||||
Reference in New Issue
Block a user