diff --git a/src/zen/common/styles/zen-library-media-preview.css b/src/zen/common/styles/zen-library-media-preview.css index 0ea53bf8b..025f6701b 100644 --- a/src/zen/common/styles/zen-library-media-preview.css +++ b/src/zen/common/styles/zen-library-media-preview.css @@ -14,13 +14,11 @@ .zen-library-media-preview-backdrop { position: absolute; inset: 0; - -moz-window-dragging: drag; background: light-dark(rgba(255, 255, 255, 0.55), rgba(0, 0, 0, 0.45)); } .zen-library-media-preview-panel { position: relative; - -moz-window-dragging: no-drag; display: flex; flex-direction: column; width: min(78vw, 1100px); diff --git a/src/zen/common/styles/zen-library-widget.css b/src/zen/common/styles/zen-library-widget.css index 9ba4e5fec..9252b2a26 100644 --- a/src/zen/common/styles/zen-library-widget.css +++ b/src/zen/common/styles/zen-library-widget.css @@ -124,7 +124,7 @@ background-image: var(--download-image); width: 20px; height: 20px; - border-radius: var(--border-radius-small); + border-radius: var(--border-radius-xsmall); background-size: cover; scale: 2; diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 24ab45dd6..bb1deb731 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -100,7 +100,10 @@ class nsZenFolders extends nsZenDOMOperatedFeature { const folderActionsMenu = document.getElementById("zenFolderActions"); folderActionsMenu.addEventListener("popupshowing", event => { - const target = event.explicitOriginalTarget; + const target = + event.target === folderActionsMenu + ? (folderActionsMenu.triggerNode ?? event.explicitOriginalTarget) + : event.explicitOriginalTarget; let folder; if (gBrowser.isTabGroupLabel(target)) { folder = target.group; @@ -498,16 +501,6 @@ class nsZenFolders extends nsZenDOMOperatedFeature { await this.animateCollapse(group); } - /** - * Settles a collapsed folder's own height. The library toggles folders - * through copies, so the real one needs telling once the dust settles. - * - * @param {Element} group - A folder - */ - relayoutCollapsedFolder(group) { - this.#queueCollapsedRelayout(group); - } - #queueCollapsedRelayout(group) { if (this.#collapsedRelayoutQueue.size === 0) { requestAnimationFrame(() => this.#flushCollapsedRelayout()); diff --git a/src/zen/library/ZenLibrary.mjs b/src/zen/library/ZenLibrary.mjs index e5686a3b8..e324e8612 100644 --- a/src/zen/library/ZenLibrary.mjs +++ b/src/zen/library/ZenLibrary.mjs @@ -33,6 +33,7 @@ ChromeUtils.defineESModuleGetters( ); const LAST_TAB_PREF = "zen.library.last-tab"; +const CLEANUP_DELAY_MS = 30000; ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () { return document.getElementById("zen-appcontent-wrapper"); @@ -183,18 +184,8 @@ export class ZenLibrary extends MozLitElement { } if (isOpen && !wasOpen) { - this.setAttribute("open", "true"); - document - .getElementById("zen-sidebar-splitter") - .setAttribute("zen-library-open", "true"); this.#init(); } else if (!isOpen && wasOpen) { - this.removeAttribute("open"); - this.#mounted = new Set([this.activeTab]); - this.requestUpdate(); - document - .getElementById("zen-sidebar-splitter") - .removeAttribute("zen-library-open"); this.#cleanup(); } @@ -293,20 +284,29 @@ export class ZenLibrary extends MozLitElement { return this.#stylesLoaded; } + #cleanupTimer = null; #idleCleanup = null; #scheduleIdleCleanup() { - this.#idleCleanup = window.requestIdleCallback(() => { - this.#idleCleanup = null; - this.#stylesLoaded = null; + this.#cancelIdleCleanup(); + this.#cleanupTimer = window.setTimeout(() => { + this.#cleanupTimer = null; + this.#idleCleanup = window.requestIdleCallback(() => { + this.#idleCleanup = null; + this.#stylesLoaded = null; - this.#contentMounted = false; - this.#mounted = new Set([this.activeTab]); - this.requestUpdate(); - }); + this.#contentMounted = false; + this.#mounted = new Set([this.activeTab]); + this.requestUpdate(); + }); + }, CLEANUP_DELAY_MS); } #cancelIdleCleanup() { + if (this.#cleanupTimer) { + window.clearTimeout(this.#cleanupTimer); + this.#cleanupTimer = null; + } if (this.#idleCleanup) { window.cancelIdleCallback(this.#idleCleanup); this.#idleCleanup = null; @@ -376,11 +376,6 @@ export class ZenLibrary extends MozLitElement { lib.openProgress = target; lib.#springControls = null; lib.removeAttribute("transitioning"); - if (target === 0) { - lib.#mounted = new Set([lib.activeTab]); - lib.requestUpdate(); - lib.#scheduleIdleCleanup(); - } }, } ); @@ -401,12 +396,12 @@ export class ZenLibrary extends MozLitElement { lib.#springControls = null; } - lib.style.setProperty("pointer-events", "none"); + lib.style.pointerEvents = "none"; } static stopSwipe(direction) { const lib = this.getInstance(); - lib.style.setProperty("pointer-events", "unset"); + lib.style.pointerEvents = ""; lib.#canSwipe = false; if (lib.#libraryOnRight) { @@ -418,7 +413,6 @@ export class ZenLibrary extends MozLitElement { this.animateProgress(target); } - // Return library open state return lib.#isOpen; } @@ -499,6 +493,15 @@ export class ZenLibrary extends MozLitElement { } #init() { + this.#cancelIdleCleanup(); + if (!this.#contentMounted) { + this.#contentMounted = true; + this.requestUpdate(); + } + this.setAttribute("open", "true"); + document + .getElementById("zen-sidebar-splitter") + .setAttribute("zen-library-open", "true"); document.addEventListener("keydown", this, true); window.addEventListener("TabOpen", this); @@ -511,11 +514,18 @@ export class ZenLibrary extends MozLitElement { } #cleanup() { - this.#cancelIdleCleanup(); + this.removeAttribute("open"); + this.#mounted = new Set([this.activeTab]); + this.requestUpdate(); + document + .getElementById("zen-sidebar-splitter") + .removeAttribute("zen-library-open"); + if (this.#springControls) { this.#springControls.stop(); this.#springControls = null; } + this.removeAttribute("transitioning"); this.#detachWrapperOfSwipe(); if (this.#gestureControl) { @@ -531,6 +541,7 @@ export class ZenLibrary extends MozLitElement { document.removeEventListener("keydown", this, true); window.removeEventListener("TabOpen", this); this.isHidden = true; + this.#scheduleIdleCleanup(); } get #isCompactMode() { diff --git a/src/zen/library/ZenLibraryMediaPreview.mjs b/src/zen/library/ZenLibraryMediaPreview.mjs index 641a8daa4..cacc548a6 100644 --- a/src/zen/library/ZenLibraryMediaPreview.mjs +++ b/src/zen/library/ZenLibraryMediaPreview.mjs @@ -43,6 +43,8 @@ export class ZenLibraryMediaPreview { #root = null; #parts = {}; #menu = null; + /** @type {object|null} The media the open menu acts on */ + #menuItem = null; #items = []; #index = -1; #origin = null; @@ -135,6 +137,9 @@ export class ZenLibraryMediaPreview { openButton.addEventListener("click", () => this.#launch()); const stage = make("div", "zen-library-media-preview-stage", panel); + stage.addEventListener("contextmenu", event => + this.openContextMenu(event, this.#item) + ); this.#parts = { panel, @@ -472,8 +477,7 @@ export class ZenLibraryMediaPreview { event.dataTransfer.setData("text/uri-list", item.url); } - #launch() { - const item = this.#item; + #launch(item = this.#item) { if (!item) { return; } @@ -484,15 +488,13 @@ export class ZenLibraryMediaPreview { } } - #reveal() { - const item = this.#item; + #reveal(item = this.#item) { if (item) { new lazy.FileUtils.File(item.path).reveal(); } } - #copy() { - const item = this.#item; + #copy(item = this.#item) { if (!item) { return; } @@ -513,6 +515,40 @@ export class ZenLibraryMediaPreview { } #openMenu(event) { + this.#menuItem = this.#item; + this.#ensureMenu().openPopup( + event.currentTarget, + "after_end", + 0, + 0, + false, + false + ); + } + + /** + * Opens the same menu as the more button, at the pointer, for any media + * in the grid or the one on show. + * + * @param {MouseEvent} event - The right click + * @param {object} item - The media it was on + */ + openContextMenu(event, item) { + event.preventDefault(); + event.stopPropagation(); + if (!item) { + return; + } + this.#menuItem = item; + this.#ensureMenu().openPopupAtScreen( + event.screenX, + event.screenY, + true, + event + ); + } + + #ensureMenu() { if (!this.#menu) { this.#menu = this.#window.MozXULElement.parseXULToFragment(` @@ -523,20 +559,21 @@ export class ZenLibraryMediaPreview { `).firstElementChild; this.#menu.addEventListener("command", menuEvent => { + const item = this.#menuItem; switch (menuEvent.target.dataset.action) { case "open": - this.#launch(); + this.#launch(item); break; case "show": - this.#reveal(); + this.#reveal(item); break; case "copy": - this.#copy(); + this.#copy(item); break; } }); this.#document.getElementById("mainPopupSet").appendChild(this.#menu); } - this.#menu.openPopup(event.currentTarget, "after_end", 0, 0, false, false); + return this.#menu; } } diff --git a/src/zen/library/sections/ZenLibraryMediaSection.mjs b/src/zen/library/sections/ZenLibraryMediaSection.mjs index ae51204f1..bca77065e 100644 --- a/src/zen/library/sections/ZenLibraryMediaSection.mjs +++ b/src/zen/library/sections/ZenLibraryMediaSection.mjs @@ -18,7 +18,6 @@ const FILE_MIME = "application/x-moz-file"; const HTML_NS = "http://www.w3.org/1999/xhtml"; const FOLDERS_PREF = "zen.library.media.folders"; const ENABLED_PREF = "zen.library.media.enabled"; -const SKELETON_CARDS = 10; const FOLDERS = { downloads: { key: "DfltDwnld", home: "Downloads" }, @@ -287,6 +286,11 @@ export class ZenLibraryMediaSection extends ZenLibrarySearchSection { dataTransfer.addElement(event.currentTarget); } + #onItemContextMenu(event, item) { + this.#preview ??= new ZenLibraryMediaPreview(window); + this.#preview.openContextMenu(event, item); + } + #onItemClick(event, item) { this.#preview ??= new ZenLibraryMediaPreview(window); const shown = this.#shown; @@ -313,6 +317,9 @@ export class ZenLibraryMediaSection extends ZenLibrarySearchSection { card.title = item.name; card.addEventListener("dragstart", event => this.#onDragStart(event, item)); card.addEventListener("click", event => this.#onItemClick(event, item)); + card.addEventListener("contextmenu", event => + this.#onItemContextMenu(event, item) + ); const frame = document.createElementNS(HTML_NS, "div"); frame.className = "zen-library-media-frame"; @@ -443,12 +450,7 @@ export class ZenLibraryMediaSection extends ZenLibrarySearchSection { */ #renderOptIn() { return html` -
- ${Array.from( - { length: SKELETON_CARDS }, - () => html`
` - )} -
+
diff --git a/src/zen/library/sections/ZenLibrarySpacesSection.mjs b/src/zen/library/sections/ZenLibrarySpacesSection.mjs index 2c3698ef7..e8f5203c6 100644 --- a/src/zen/library/sections/ZenLibrarySpacesSection.mjs +++ b/src/zen/library/sections/ZenLibrarySpacesSection.mjs @@ -108,6 +108,13 @@ export class ZenLibrarySpacesSection extends MozLitElement { #pendingGroups = []; /** @type {WeakMap} copied tab or group to the real one */ #realElements = new WeakMap(); + /** + * Folders are opened and closed here without touching the real ones, so + * rebuilt copies keep what was chosen here. + * + * @type {WeakMap} real group to its copy's collapsed state + */ + #collapsedCopies = new WeakMap(); #dnd = new ZenLibraryDragAndDrop(this); #dragIndex = -1; #dropIndex = -1; @@ -455,6 +462,9 @@ export class ZenLibrarySpacesSection extends MozLitElement { if (GROUP_TAGS.includes(node.localName)) { const copy = node.cloneNode(false); this.#renameIds(copy); + if (this.#collapsedCopies.has(node)) { + copy.toggleAttribute("collapsed", this.#collapsedCopies.get(node)); + } container.appendChild(copy); this.#realElements.set(copy, node); const icon = copy.querySelector(".tab-group-folder-icon"); @@ -502,15 +512,12 @@ export class ZenLibrarySpacesSection extends MozLitElement { const group = this.#realElements.get(copy); if (group) { event.stopPropagation(); - const collapsed = !group.collapsed; - group.collapsed = collapsed; - copy.collapsed = collapsed; - if (collapsed) { + this.#collapsedCopies.set(group, copy.collapsed); + if (copy.collapsed) { gZenFolders.animateCollapse(copy); } else { gZenFolders.animateExpand(copy); } - gZenFolders.relayoutCollapsedFolder(group); } return; } @@ -527,6 +534,35 @@ export class ZenLibrarySpacesSection extends MozLitElement { this.library?.constructor.toggle(); }; + /** + * A copy carries the real element's context menu, which would then act on + * the copy. The menu opens for the real tab or folder instead. + * + * @param {MouseEvent} event + */ + #onStripContextMenu = event => { + let real = null; + const label = event.target.closest(".tab-group-label-container"); + if (label) { + const group = this.#realElements.get(label.closest(GROUP_TAGS.join())); + real = group?.labelElement?.parentElement; + } else { + real = this.#realElements.get(event.target.closest("tab")); + } + const menu = real && document.getElementById(real.getAttribute("context")); + event.preventDefault(); + event.stopPropagation(); + if (!menu) { + return; + } + const trigger = new MouseEvent("ZenLibraryContextMenu", { + screenX: event.screenX, + screenY: event.screenY, + }); + real.dispatchEvent(trigger); + menu.openPopupAtScreen(event.screenX, event.screenY, true, trigger); + }; + /** * @param {Element} copy - A copied tab, group or folder * @returns {Element|undefined} The real element it stands for @@ -591,6 +627,7 @@ export class ZenLibrarySpacesSection extends MozLitElement { orient="vertical" expanded="true" @click=${this.#onStripClick} + @contextmenu=${this.#onStripContextMenu} @mouseover=${this.#containHover} @mouseout=${this.#containHover} @dragstart=${this.#onStripDragStart} diff --git a/src/zen/library/zen-library.css b/src/zen/library/zen-library.css index e05caaac9..b5c10d76c 100644 --- a/src/zen/library/zen-library.css +++ b/src/zen/library/zen-library.css @@ -338,33 +338,22 @@ zen-library[zen-library-downloading] .zen-library-tab[data-section="downloads"]: box-shadow: none; } -#zen-library-panel - :is(.zen-library-section, .zen-library-search-results) - > * { - animation: zen-library-section-fade 0.22s ease; -} - -@keyframes zen-library-section-fade { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - .zen-library-media-skeleton { + position: absolute; + inset: 0; pointer-events: none; - & .zen-library-media-item { + &::before { + content: ""; + position: absolute; + inset: 0; + background: var(--zen-library-pill-bg); + mask-image: url("data:image/svg+xml,"); + mask-size: 150px auto; + mask-repeat: round repeat; animation: zen-library-media-wait 1.8s ease-in-out infinite; } - & .zen-library-media-item:nth-child(even) { - animation-delay: 0.3s; - } - &[behind] { opacity: 0.7; } @@ -821,6 +810,7 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head width: 46px; height: 46px; margin-inline: 2px 1px; + color-scheme: light; &::before { content: ""; diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index 6ba4270cc..4220d5e22 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -474,14 +474,17 @@ class nsZenPinnedTabManager extends nsZenDOMOperatedFeature { } // Remove everything except the entry we want to keep + let url; + try { + url = Services.io.newURI(initialState.entry.url); + } catch {} state.entries = [ { ...initialState.entry, triggeringPrincipal_base64: E10SUtils.serializePrincipal( - Services.scriptSecurityManager.createContentPrincipal( - Services.io.newURI(initialState.entry.url), - {} - ) + url + ? Services.scriptSecurityManager.createContentPrincipal(url, {}) + : Services.scriptSecurityManager.createNullPrincipal() ), }, ];