mirror of
https://github.com/zen-browser/desktop.git
synced 2026-09-23 21:03:55 +00:00
gh-15438: Performance optimizations and media tab for Library (gh-15483)
Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> Signed-off-by: fen4flo <75260616+FlorianButz@users.noreply.github.com>
This commit is contained in:
@@ -35,6 +35,28 @@ library-history-search-placeholder =
|
||||
.placeholder = Search History…
|
||||
library-history-empty = No history found
|
||||
library-media-empty = No media found
|
||||
library-media-search-placeholder =
|
||||
.placeholder = Search Media…
|
||||
library-media-loading = Looking for media…
|
||||
library-media-opt-in-title = Your Media in the Library
|
||||
library-media-opt-in-body = Quickly grab screenshots, images and videos from your computer by showing them in the Zen Library.
|
||||
library-media-opt-in-button = Show Media
|
||||
library-media-opt-in-note = Media never leaves your computer and is not viewable by anyone but you.
|
||||
library-media-filter-title = Filter Media…
|
||||
library-media-filter-folders = Where to look
|
||||
library-media-open-in = Open in { $app }
|
||||
library-media-close =
|
||||
.title = Close
|
||||
library-media-previous =
|
||||
.title = Previous
|
||||
library-media-next =
|
||||
.title = Next
|
||||
library-media-more =
|
||||
.title = More options
|
||||
library-media-menu-open =
|
||||
.label = Open
|
||||
library-media-menu-copy =
|
||||
.label = Copy
|
||||
library-history-forget-button =
|
||||
.title = Remove from history
|
||||
library-history-reopen-button =
|
||||
|
||||
@@ -7,3 +7,9 @@
|
||||
|
||||
- name: zen.library.last-tab
|
||||
value: "history"
|
||||
|
||||
- name: zen.library.media.enabled
|
||||
value: false
|
||||
|
||||
- name: zen.library.media.folders
|
||||
value: "downloads,documents,pictures"
|
||||
|
||||
@@ -652,7 +652,10 @@ class nsZenBoostsManager {
|
||||
);
|
||||
|
||||
const progressListener = {
|
||||
QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener"]),
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIWebProgressListener",
|
||||
"nsISupportsWeakReference",
|
||||
]),
|
||||
onLocationChange: webProgress => {
|
||||
if (webProgress.isTopLevel) {
|
||||
editor.close();
|
||||
|
||||
@@ -100,14 +100,14 @@ body,
|
||||
display: flex;
|
||||
opacity: var(--zen-grainy-background-opacity, 0);
|
||||
|
||||
:root:not([swipe-gesture="true"]) & {
|
||||
&:not([swipe-gesture="true"]) {
|
||||
/* note: Keep in sync with kGlobalAnimationDuration */
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:root[swipe-gesture] #tabbrowser-arrowscrollbox {
|
||||
#tabbrowser-arrowscrollbox[swipe-gesture] {
|
||||
/* See gh-13703 for rationale. */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
96
src/zen/common/styles/zen-library-media-preview.css
Normal file
96
src/zen/common/styles/zen-library-media-preview.css
Normal file
@@ -0,0 +1,96 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
.zen-library-media-preview {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
--zen-main-app-child-z-index: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.zen-library-media-preview-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
-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);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: light-dark(rgb(245, 245, 247), rgb(28, 30, 32));
|
||||
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.zen-library-media-preview-header {
|
||||
--zen-toolbar-button-size: 15px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
flex: none;
|
||||
color: light-dark(rgb(28, 30, 32), rgb(233, 234, 237));
|
||||
}
|
||||
|
||||
.zen-library-media-preview-name {
|
||||
margin-inline-start: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zen-library-media-preview-date {
|
||||
font-size: 13px;
|
||||
opacity: 0.6;
|
||||
margin-inline-end: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.zen-library-media-preview .toolbarbutton-1 > .toolbarbutton-icon {
|
||||
box-sizing: border-box;
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--toolbarbutton-icon-fill);
|
||||
}
|
||||
|
||||
.zen-library-media-preview-open {
|
||||
& > .toolbarbutton-text {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--toolbarbutton-border-radius);
|
||||
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&:hover > .toolbarbutton-text {
|
||||
background: color-mix(in srgb, currentColor 20%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-media-preview-stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: min(64vh, 620px);
|
||||
padding: 16px;
|
||||
background: light-dark(rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.28));
|
||||
border-top: 1px solid light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.07));
|
||||
}
|
||||
|
||||
.zen-library-media-preview-media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 8px;
|
||||
object-fit: contain;
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
width: var(--zen-library-sprite-size);
|
||||
height: var(--zen-library-sprite-size);
|
||||
background-image: url("chrome://browser/skin/zen-icons/library/library-sprite.svg");
|
||||
scale: 1.08;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@@ -87,7 +88,7 @@
|
||||
}
|
||||
|
||||
&[downloading] {
|
||||
transform: scale(calc((var(--zen-library-progress) * 0.2 + 0.81) * 100%));
|
||||
transform: scale(calc((var(--zen-library-progress) * 0.2 + 0.8) * 100%));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +124,7 @@
|
||||
background-image: var(--download-image);
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--border-radius-small);
|
||||
background-size: cover;
|
||||
scale: 2;
|
||||
|
||||
@@ -140,7 +141,7 @@
|
||||
--border-color: color-mix(in srgb, var(--zen-primary-color), light-dark(rgba(0,0,0,.8), white) 70%);
|
||||
|
||||
background-color: color-mix(in srgb, var(--zen-primary-color), light-dark(white, rgba(0,0,0,.5)) 80%);
|
||||
border: 3px solid var(--border-color);
|
||||
border: 3px solid transparent;
|
||||
box-shadow: 0 0 6px 2px rgba(0,0,0,.1);
|
||||
color: var(--border-color);
|
||||
width: 25px;
|
||||
@@ -175,11 +176,14 @@
|
||||
--thickness: 3px;
|
||||
--progress: var(--zen-accent-button-background);
|
||||
|
||||
background: conic-gradient(
|
||||
from -0deg,
|
||||
var(--progress) calc(var(--value) * 1%),
|
||||
transparent 0
|
||||
);
|
||||
background:
|
||||
conic-gradient(
|
||||
from 0deg,
|
||||
var(--progress) calc(var(--value) * 1%),
|
||||
transparent 0
|
||||
)
|
||||
border-box,
|
||||
var(--border-color) border-box;
|
||||
|
||||
mask: radial-gradient(
|
||||
circle closest-side,
|
||||
@@ -188,6 +192,10 @@
|
||||
);
|
||||
}
|
||||
|
||||
#zen-sidebar-foot-buttons:is([zen-library-stack-open], [zen-library-stack-closing]) {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#zen-library-download-list {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -812,3 +812,4 @@
|
||||
}
|
||||
|
||||
%include zen-library-widget.css
|
||||
%include zen-library-media-preview.css
|
||||
|
||||
@@ -498,6 +498,16 @@ 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());
|
||||
|
||||
@@ -69,22 +69,19 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
return this.#detached?.browser ?? null;
|
||||
}
|
||||
|
||||
get #detachedHost() {
|
||||
let host = document.getElementById("zen-glance-detached");
|
||||
if (!host) {
|
||||
host = window.MozXULElement.parseXULToFragment(`
|
||||
<html:div id="zen-glance-detached" hidden="true">
|
||||
<html:div class="zen-glance-detached-backdrop"/>
|
||||
<html:div class="browserContainer">
|
||||
<html:div class="browserStack"/>
|
||||
</html:div>
|
||||
#createDetachedHost() {
|
||||
const host = window.MozXULElement.parseXULToFragment(`
|
||||
<html:div id="zen-glance-detached">
|
||||
<html:div class="zen-glance-detached-backdrop"/>
|
||||
<html:div class="browserContainer">
|
||||
<html:div class="browserStack"/>
|
||||
</html:div>
|
||||
`).firstElementChild;
|
||||
host
|
||||
.querySelector(".zen-glance-detached-backdrop")
|
||||
.addEventListener("click", () => this.closeDetachedGlance());
|
||||
document.getElementById("zen-main-app-wrapper").appendChild(host);
|
||||
}
|
||||
</html:div>
|
||||
`).firstElementChild;
|
||||
host
|
||||
.querySelector(".zen-glance-detached-backdrop")
|
||||
.addEventListener("click", () => this.closeDetachedGlance());
|
||||
document.getElementById("zen-main-app-wrapper").appendChild(host);
|
||||
return host;
|
||||
}
|
||||
|
||||
@@ -101,7 +98,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
if (this.#detached || !this.#isGlanceLoadAllowed(data)) {
|
||||
return null;
|
||||
}
|
||||
const host = this.#detachedHost;
|
||||
const host = this.#createDetachedHost();
|
||||
const wrapper = host.querySelector(".browserContainer");
|
||||
const browser = document.createXULElement("browser");
|
||||
browser.setAttribute("type", "content");
|
||||
@@ -122,7 +119,6 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
wrapper.querySelector(selector)?.remove();
|
||||
}
|
||||
|
||||
host.hidden = false;
|
||||
const hostRect = host.getBoundingClientRect();
|
||||
const startPoint = {
|
||||
clientX: data.clientX - hostRect.left,
|
||||
@@ -202,10 +198,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
|
||||
);
|
||||
} finally {
|
||||
browser.destroy();
|
||||
browser.remove();
|
||||
wrapper.removeAttribute("animate");
|
||||
host.removeAttribute("fade-out");
|
||||
host.hidden = true;
|
||||
host.remove();
|
||||
this.#detached = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,14 +202,10 @@
|
||||
|
||||
#zen-main-app-wrapper > #zen-glance-detached {
|
||||
position: fixed;
|
||||
inset: 24px;
|
||||
inset: calc(var(--zen-element-separation) / 2);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
|
||||
&[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .zen-glance-detached-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import {
|
||||
html,
|
||||
nothing,
|
||||
repeat,
|
||||
} from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
|
||||
const { ZenLibraryWidget } = ChromeUtils.importESModule(
|
||||
@@ -36,6 +40,8 @@ ChromeUtils.defineLazyGetter(lazy, "appContentWrapper", function () {
|
||||
|
||||
export class ZenLibrary extends MozLitElement {
|
||||
static instance = null;
|
||||
#contentMounted = true;
|
||||
#mounted = new Set();
|
||||
#progress = 0;
|
||||
|
||||
#springControls = null;
|
||||
@@ -51,8 +57,8 @@ export class ZenLibrary extends MozLitElement {
|
||||
#canSwipe = false;
|
||||
#isOpen = false;
|
||||
|
||||
#isWrapperSwipeAttached = false;
|
||||
#wrapperGestureControl = null;
|
||||
#gestureControl = null;
|
||||
|
||||
#resizeObserver = new ResizeObserver(() => {
|
||||
this.openProgress = this.#progress;
|
||||
@@ -83,6 +89,7 @@ export class ZenLibrary extends MozLitElement {
|
||||
};
|
||||
const lastTab = Services.prefs.getStringPref(LAST_TAB_PREF, "history");
|
||||
this.activeTab = lastTab in this.zenLibrarySections ? lastTab : "history";
|
||||
this.#mounted.add(this.activeTab);
|
||||
this.#hijackFirefoxCommands();
|
||||
}
|
||||
|
||||
@@ -101,11 +108,17 @@ export class ZenLibrary extends MozLitElement {
|
||||
return lib.openProgress > 0.001;
|
||||
}
|
||||
|
||||
set isHidden(value) {
|
||||
this.requestUpdate();
|
||||
this.hidden = value;
|
||||
}
|
||||
|
||||
set activeTab(value) {
|
||||
if (this._activeTab === value) {
|
||||
return;
|
||||
}
|
||||
this._activeTab = value;
|
||||
this.#mounted.add(value);
|
||||
Services.prefs.setStringPref(LAST_TAB_PREF, value);
|
||||
}
|
||||
|
||||
@@ -125,31 +138,64 @@ export class ZenLibrary extends MozLitElement {
|
||||
const isPastWindowButtonSwitchPoint = p > stealWindowButtonsPastPoint;
|
||||
const isOpen = p > 0.001;
|
||||
|
||||
let libraryWidth = window.windowUtils.getBoundsWithoutFlushing(this).width;
|
||||
const compactModeOffsetDirection = this.#libraryOnRight
|
||||
? -this.#toolboxWidth
|
||||
: this.#toolboxWidth;
|
||||
const compactModeOffset = this.#isCompactMode
|
||||
? compactModeOffsetDirection
|
||||
: 0;
|
||||
let webOffset =
|
||||
(this.#libraryOnRight ? -1 : 1) * (libraryWidth - this.#toolboxWidth) +
|
||||
compactModeOffset;
|
||||
if (this.#stylesLoaded) {
|
||||
let libraryWidth =
|
||||
window.windowUtils.getBoundsWithoutFlushing(this).width;
|
||||
const compactModeOffsetDirection = this.#libraryOnRight
|
||||
? -this.#toolboxWidth
|
||||
: this.#toolboxWidth;
|
||||
const compactModeOffset = this.#isCompactMode
|
||||
? compactModeOffsetDirection
|
||||
: 0;
|
||||
|
||||
lazy.appContentWrapper?.style.setProperty(
|
||||
"--library-wrapper-target-px",
|
||||
`${webOffset}px`
|
||||
);
|
||||
[this, lazy.appContentWrapper, gNavToolbox].forEach(elem => {
|
||||
elem?.style.setProperty("--library-progress", String(p));
|
||||
});
|
||||
let webOffset =
|
||||
(this.#libraryOnRight ? -1 : 1) * (libraryWidth - this.#toolboxWidth) +
|
||||
compactModeOffset;
|
||||
|
||||
this.style.setProperty(
|
||||
"transform",
|
||||
`translateX(calc(-100% * (1 - ${value})))`
|
||||
);
|
||||
lazy.appContentWrapper?.style.setProperty(
|
||||
"transform",
|
||||
`translateX(${value * webOffset}px)`
|
||||
);
|
||||
|
||||
const toolboxProgress = Math.min(1, value * 1.5);
|
||||
if (this.#isCompactMode) {
|
||||
if (this.#libraryOnRight) {
|
||||
gNavToolbox.style.setProperty(
|
||||
"transform",
|
||||
`translateX(calc(100% * ${toolboxProgress}))`
|
||||
);
|
||||
} else {
|
||||
gNavToolbox.style.setProperty(
|
||||
"transform",
|
||||
`translateX(calc(-100% * ${toolboxProgress}))`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const toolboxScale = 1 - toolboxProgress * 0.04;
|
||||
const toolboxOpacity = 1 - toolboxProgress;
|
||||
gNavToolbox?.style.setProperty("transform", `scale(${toolboxScale})`);
|
||||
gNavToolbox?.style.setProperty("opacity", `${toolboxOpacity}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen && !wasOpen) {
|
||||
this.setAttribute("open", "true");
|
||||
document.documentElement.setAttribute("zen-library-open", "true");
|
||||
document
|
||||
.getElementById("zen-sidebar-splitter")
|
||||
.setAttribute("zen-library-open", "true");
|
||||
this.#init();
|
||||
} else if (!isOpen && wasOpen) {
|
||||
this.removeAttribute("open");
|
||||
document.documentElement.removeAttribute("zen-library-open");
|
||||
this.#mounted = new Set([this.activeTab]);
|
||||
this.requestUpdate();
|
||||
document
|
||||
.getElementById("zen-sidebar-splitter")
|
||||
.removeAttribute("zen-library-open");
|
||||
this.#cleanup();
|
||||
}
|
||||
|
||||
if (isPastWindowButtonSwitchPoint && this.#coversWindowButtons) {
|
||||
@@ -253,8 +299,10 @@ export class ZenLibrary extends MozLitElement {
|
||||
this.#idleCleanup = window.requestIdleCallback(() => {
|
||||
this.#idleCleanup = null;
|
||||
this.#stylesLoaded = null;
|
||||
ZenLibrary.instance = null;
|
||||
this.remove();
|
||||
|
||||
this.#contentMounted = false;
|
||||
this.#mounted = new Set([this.activeTab]);
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,7 +342,6 @@ export class ZenLibrary extends MozLitElement {
|
||||
|
||||
static async animateProgress(target) {
|
||||
const lib = this.getInstance();
|
||||
lib.#detachWrapperOfSwipe();
|
||||
lib.#cancelIdleCleanup();
|
||||
await lib.#whenStylesLoaded();
|
||||
lib.style.visibility = "";
|
||||
@@ -310,6 +357,7 @@ export class ZenLibrary extends MozLitElement {
|
||||
lib.#isOpen = true;
|
||||
} else if (target === 0) {
|
||||
lib.#isOpen = false;
|
||||
lib.#canSwipe = false;
|
||||
}
|
||||
|
||||
lib.setAttribute("transitioning", "true");
|
||||
@@ -329,6 +377,8 @@ export class ZenLibrary extends MozLitElement {
|
||||
lib.#springControls = null;
|
||||
lib.removeAttribute("transitioning");
|
||||
if (target === 0) {
|
||||
lib.#mounted = new Set([lib.activeTab]);
|
||||
lib.requestUpdate();
|
||||
lib.#scheduleIdleCleanup();
|
||||
}
|
||||
},
|
||||
@@ -343,9 +393,6 @@ export class ZenLibrary extends MozLitElement {
|
||||
await lib.#whenStylesLoaded();
|
||||
lib.style.visibility = "";
|
||||
await window.promiseDocumentFlushed(() => {});
|
||||
if (!lib.#canSwipe) {
|
||||
return;
|
||||
}
|
||||
|
||||
lib.#onOpenLibrary();
|
||||
|
||||
@@ -355,7 +402,6 @@ export class ZenLibrary extends MozLitElement {
|
||||
}
|
||||
|
||||
lib.style.setProperty("pointer-events", "none");
|
||||
lib.#attachWrapperToSwipe();
|
||||
}
|
||||
|
||||
static stopSwipe(direction) {
|
||||
@@ -371,7 +417,6 @@ export class ZenLibrary extends MozLitElement {
|
||||
const target = Math.max(-direction, 0);
|
||||
this.animateProgress(target);
|
||||
}
|
||||
lib.#detachWrapperOfSwipe();
|
||||
|
||||
// Return library open state
|
||||
return lib.#isOpen;
|
||||
@@ -387,29 +432,33 @@ export class ZenLibrary extends MozLitElement {
|
||||
}
|
||||
|
||||
#attachWrapperToSwipe() {
|
||||
if (!this.#isWrapperSwipeAttached) {
|
||||
if (!this.#wrapperGestureControl) {
|
||||
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||
this.#wrapperGestureControl =
|
||||
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(
|
||||
appWrapper
|
||||
);
|
||||
this.#isWrapperSwipeAttached = true;
|
||||
}
|
||||
}
|
||||
|
||||
#detachWrapperOfSwipe() {
|
||||
if (this.#isWrapperSwipeAttached || this.#wrapperGestureControl) {
|
||||
if (this.#wrapperGestureControl) {
|
||||
const appWrapper = document.getElementById("zen-main-app-wrapper");
|
||||
window.gZenWorkspaces._swipeManager?.detachWorkspaceSwipeGestures(
|
||||
appWrapper,
|
||||
this.#wrapperGestureControl
|
||||
);
|
||||
this.#wrapperGestureControl = null;
|
||||
this.#isWrapperSwipeAttached = false;
|
||||
}
|
||||
}
|
||||
|
||||
#onOpenLibrary() {
|
||||
this.#cancelIdleCleanup();
|
||||
if (!this.#contentMounted) {
|
||||
this.#contentMounted = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
gURLBar.view.close();
|
||||
// Get the width from the css property,
|
||||
// getBoundsWithoutFlushing will fail as it takes the
|
||||
@@ -449,49 +498,60 @@ export class ZenLibrary extends MozLitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
document.addEventListener("keydown", this.onKeyDown, true);
|
||||
#init() {
|
||||
document.addEventListener("keydown", this, true);
|
||||
window.addEventListener("TabOpen", this);
|
||||
|
||||
this.#attachWrapperToSwipe();
|
||||
this.#gestureControl =
|
||||
window.gZenWorkspaces._swipeManager.attachWorkspaceSwipeGestures(this);
|
||||
this.#resizeObserver.observe(this);
|
||||
|
||||
window.gZenWorkspaces._swipeManager?.attachWorkspaceSwipeGestures(this);
|
||||
|
||||
this._tabOpen = this.onTabOpen.bind(this);
|
||||
window.addEventListener("TabOpen", this._tabOpen);
|
||||
|
||||
ZenLibraryWidget.attachLibrary(this);
|
||||
this.isHidden = false;
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
#cleanup() {
|
||||
this.#cancelIdleCleanup();
|
||||
if (this.#springControls) {
|
||||
this.#springControls.stop();
|
||||
this.#springControls = null;
|
||||
}
|
||||
|
||||
this.#detachWrapperOfSwipe();
|
||||
if (this.#gestureControl) {
|
||||
window.gZenWorkspaces._swipeManager.detachWorkspaceSwipeGestures(
|
||||
this,
|
||||
this.#gestureControl
|
||||
);
|
||||
}
|
||||
|
||||
this.#restoreWindowButtons();
|
||||
ZenLibraryWidget.detachLibrary(this);
|
||||
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener("keydown", this.onKeyDown, true);
|
||||
this.#resizeObserver.disconnect();
|
||||
|
||||
if (this._tabOpen) {
|
||||
window.removeEventListener("TabOpen", this._tabOpen);
|
||||
this._tabOpen = null;
|
||||
}
|
||||
document.removeEventListener("keydown", this, true);
|
||||
window.removeEventListener("TabOpen", this);
|
||||
this.isHidden = true;
|
||||
}
|
||||
|
||||
get #isCompactMode() {
|
||||
return (
|
||||
window.gZenCompactModeManager.preference &&
|
||||
Services.prefs.getBoolPref("zen.view.compact.hide-tabbar")
|
||||
(Services.prefs.getBoolPref("zen.view.compact.hide-tabbar") ||
|
||||
Services.prefs.getBoolPref("zen.view.use-single-toolbar"))
|
||||
);
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
switch (e.type) {
|
||||
case "TabOpen":
|
||||
this.onTabOpen();
|
||||
break;
|
||||
case "keydown":
|
||||
this.onKeyDown(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onTabOpen() {
|
||||
if (this.#isOpen) {
|
||||
ZenLibrary.animateProgress(0);
|
||||
@@ -543,11 +603,30 @@ export class ZenLibrary extends MozLitElement {
|
||||
}
|
||||
}
|
||||
|
||||
#animateTabIcon(tab) {
|
||||
tab.removeAttribute("animate");
|
||||
// Flush styles so re-adding the attribute restarts the animation.
|
||||
void tab.offsetWidth;
|
||||
tab.setAttribute("animate", "true");
|
||||
updated(changedProperties) {
|
||||
super.updated?.(changedProperties);
|
||||
this.#updateMountedSections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the section being looked at and puts the others out of sight. A
|
||||
* section is told which it is, so one that reaches outside itself, such as
|
||||
* spaces setting the library's width, only does so while it is on show.
|
||||
*/
|
||||
#updateMountedSections() {
|
||||
for (const section of this._content?.children ?? []) {
|
||||
const id = section.dataset?.section;
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
const showing = id === this.activeTab;
|
||||
const wasShowing = section.hasAttribute("showing");
|
||||
section.hidden = !showing;
|
||||
section.toggleAttribute("showing", showing);
|
||||
if (showing !== wasShowing) {
|
||||
(showing ? section.onShown : section.onHidden)?.call(section);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -569,7 +648,14 @@ export class ZenLibrary extends MozLitElement {
|
||||
@click=${event => {
|
||||
if (this.activeTab !== Section.id) {
|
||||
this.activeTab = Section.id;
|
||||
this.#animateTabIcon(event.currentTarget);
|
||||
const previousTab =
|
||||
event.currentTarget.parentNode.querySelector(
|
||||
`.zen-library-tab[animate="true"]`
|
||||
);
|
||||
if (previousTab) {
|
||||
previousTab.removeAttribute("animate");
|
||||
}
|
||||
event.currentTarget.setAttribute("animate", "true");
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -589,7 +675,15 @@ export class ZenLibrary extends MozLitElement {
|
||||
></toolbar>
|
||||
</vbox>
|
||||
<vbox id="zen-library-content">
|
||||
${this.activeSection.render(this)}
|
||||
${
|
||||
this.#contentMounted
|
||||
? repeat(
|
||||
[...this.#mounted],
|
||||
id => id,
|
||||
id => this.zenLibrarySections[id].render(this)
|
||||
)
|
||||
: nothing
|
||||
}
|
||||
</vbox>
|
||||
</hbox>
|
||||
`;
|
||||
|
||||
@@ -165,9 +165,7 @@ export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
|
||||
gBrowser.moveTabsBefore(moving, firstExisting);
|
||||
}
|
||||
} else {
|
||||
const pinned = gBrowser.isTab(element)
|
||||
? element.pinned
|
||||
: (element.tabs?.[0]?.pinned ?? element.pinned);
|
||||
const pinned = this.#pinnedAt(event);
|
||||
for (const tab of tabs) {
|
||||
if (pinned && !tab.pinned) {
|
||||
gBrowser.pinTab(tab);
|
||||
@@ -185,6 +183,23 @@ export class ZenLibraryDragAndDrop extends window.ZenDragAndDrop {
|
||||
this.#land(tabs[0], placeBefore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a drop belongs to the pinned part of a card.
|
||||
*
|
||||
* @param {DragEvent} event - The drop
|
||||
* @returns {boolean} Whether the tab should end up pinned
|
||||
*/
|
||||
#pinnedAt(event) {
|
||||
const strip = event.currentTarget
|
||||
.closest(".zen-library-space")
|
||||
?.querySelector(".zen-library-space-tabs");
|
||||
const pinnedRows = [...(strip?.querySelectorAll("tab[pinned]") ?? [])];
|
||||
if (!pinnedRows.length) {
|
||||
return false;
|
||||
}
|
||||
return event.clientY <= pinnedRows.at(-1).getBoundingClientRect().bottom;
|
||||
}
|
||||
|
||||
get #landingSupported() {
|
||||
return AppConstants.platform === "macosx" && !gReduceMotion;
|
||||
}
|
||||
|
||||
110
src/zen/library/ZenLibraryFileTypes.sys.mjs
Normal file
110
src/zen/library/ZenLibraryFileTypes.sys.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const lazy = {};
|
||||
|
||||
ChromeUtils.defineLazyGetter(lazy, "mimeService", () =>
|
||||
Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService)
|
||||
);
|
||||
|
||||
/**
|
||||
* Groups the browser cannot name on its own. Pictures, video and sound come
|
||||
* from the content type instead, so they are not listed here.
|
||||
*/
|
||||
const GROUP_EXTENSIONS = {
|
||||
documents:
|
||||
"pdf doc docx xls xlsx ppt pptx txt md rtf odt ods odp csv epub pages numbers key",
|
||||
archives: "zip rar 7z tar gz bz2 xz tgz zst",
|
||||
apps: "dmg pkg exe msi app deb rpm appimage apk jar",
|
||||
};
|
||||
|
||||
/**
|
||||
* The few the browser has no type for, or types differently from how these
|
||||
* lists read them. Matroska is unknown to it, and Ogg is reported as a
|
||||
* container rather than as sound.
|
||||
*/
|
||||
const EXTRA_KINDS = new Map([
|
||||
["mkv", "video"],
|
||||
["ogg", "audio"],
|
||||
]);
|
||||
|
||||
const GROUP_BY_EXTENSION = new Map();
|
||||
for (const [group, extensions] of Object.entries(GROUP_EXTENSIONS)) {
|
||||
for (const extension of extensions.split(" ")) {
|
||||
GROUP_BY_EXTENSION.set(extension, group);
|
||||
}
|
||||
}
|
||||
|
||||
/** The groups a file can be filtered by, in the order they are offered. */
|
||||
export const FILE_GROUPS = [
|
||||
"images",
|
||||
"video",
|
||||
"audio",
|
||||
...Object.keys(GROUP_EXTENSIONS),
|
||||
];
|
||||
|
||||
/**
|
||||
* A picture the platform renders as a document rather than a bitmap. It is
|
||||
* left out of thumbnails, which are drawn inside the browser's own chrome.
|
||||
*/
|
||||
const NOT_DRAWN = "image/svg+xml";
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name, with or without its path
|
||||
* @returns {string} Its extension, lowercased, or "" when it has none
|
||||
*/
|
||||
export function extensionOf(fileName) {
|
||||
const name = fileName?.split(/[/\\]/).pop() ?? "";
|
||||
const dot = name.lastIndexOf(".");
|
||||
return dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {string} What the browser believes the file holds, or "" when it
|
||||
* has no idea
|
||||
*/
|
||||
export function contentTypeOf(fileName) {
|
||||
const extension = extensionOf(fileName);
|
||||
if (!extension) {
|
||||
return "";
|
||||
}
|
||||
return lazy.mimeService.getTypeFromExtension(extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {string|null} "image", "video" or "audio", or null for anything
|
||||
* that is none of those
|
||||
*/
|
||||
export function mediaKindOf(fileName) {
|
||||
const type = contentTypeOf(fileName);
|
||||
for (const kind of ["image", "video", "audio"]) {
|
||||
if (type.startsWith(`${kind}/`)) {
|
||||
return kind;
|
||||
}
|
||||
}
|
||||
return EXTRA_KINDS.get(extensionOf(fileName)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {string|null} The group it belongs to, of {@link FILE_GROUPS}
|
||||
*/
|
||||
export function fileGroupOf(fileName) {
|
||||
const kind = mediaKindOf(fileName);
|
||||
if (kind) {
|
||||
return kind === "image" ? "images" : kind;
|
||||
}
|
||||
return GROUP_BY_EXTENSION.get(extensionOf(fileName)) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file's name
|
||||
* @returns {boolean} Whether the file itself can stand in for its icon
|
||||
*/
|
||||
export function canDrawThumbnail(fileName) {
|
||||
const type = contentTypeOf(fileName);
|
||||
return type.startsWith("image/") && type !== NOT_DRAWN;
|
||||
}
|
||||
542
src/zen/library/ZenLibraryMediaPreview.mjs
Normal file
542
src/zen/library/ZenLibraryMediaPreview.mjs
Normal file
@@ -0,0 +1,542 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
});
|
||||
|
||||
ChromeUtils.defineLazyGetter(
|
||||
lazy,
|
||||
"relativeDayFormat",
|
||||
() =>
|
||||
new Intl.RelativeTimeFormat(undefined, {
|
||||
numeric: "auto",
|
||||
style: "short",
|
||||
})
|
||||
);
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const MS_PER_DAY = 86400000;
|
||||
const MORPH_MS = 250;
|
||||
// How far in the picture gives way to the dialog around it.
|
||||
// Matches the corner the grid's pictures have.
|
||||
const MEDIA_RADIUS = 8;
|
||||
const HANDOVER = 0.3;
|
||||
// How soon the dialog's surfaces are gone on the way back.
|
||||
const CHROME_OUT = 0.25;
|
||||
// How long to hold the dialog back while the picture readies itself.
|
||||
const IMAGE_WAIT_MS = 400;
|
||||
const VIDEO_WAIT_MS = 900;
|
||||
const MORPH_EASING = "cubic-bezier(0.22, 1, 0.36, 1)";
|
||||
|
||||
/**
|
||||
* The media tab's full view. It lives beside the library rather than inside
|
||||
* it, so it can sit in the middle of the window while the library slides.
|
||||
*/
|
||||
export class ZenLibraryMediaPreview {
|
||||
#window;
|
||||
#document;
|
||||
#root = null;
|
||||
#parts = {};
|
||||
#menu = null;
|
||||
#items = [];
|
||||
#index = -1;
|
||||
#origin = null;
|
||||
#onKeyDown = event => this.#handleKey(event);
|
||||
|
||||
constructor(win) {
|
||||
this.#window = win;
|
||||
this.#document = win.document;
|
||||
}
|
||||
|
||||
get isOpen() {
|
||||
return !!this.#root?.isConnected;
|
||||
}
|
||||
|
||||
get #item() {
|
||||
return this.#items[this.#index] ?? null;
|
||||
}
|
||||
|
||||
// Building
|
||||
|
||||
#build() {
|
||||
const doc = this.#document;
|
||||
const make = (tag, className, into) => {
|
||||
const element = doc.createElementNS(HTML_NS, tag);
|
||||
if (className) {
|
||||
element.className = className;
|
||||
}
|
||||
into?.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
|
||||
const root = make("div", "zen-library-media-preview");
|
||||
const backdrop = make("div", "zen-library-media-preview-backdrop", root);
|
||||
backdrop.addEventListener("click", () => this.close());
|
||||
|
||||
const panel = make("div", "zen-library-media-preview-panel", root);
|
||||
const header = doc.createXULElement("toolbar");
|
||||
header.className = "zen-library-media-preview-header";
|
||||
panel.appendChild(header);
|
||||
|
||||
const button = (className, icon, l10nId, onClick) => {
|
||||
const element = doc.createXULElement("toolbarbutton");
|
||||
element.className = `toolbarbutton-1 ${className}`;
|
||||
header.appendChild(element);
|
||||
const image = doc.createElementNS(HTML_NS, "img");
|
||||
image.className = "toolbarbutton-icon";
|
||||
image.src = icon;
|
||||
image.alt = "";
|
||||
element.appendChild(image);
|
||||
doc.l10n.setAttributes(element, l10nId);
|
||||
element.addEventListener("command", onClick);
|
||||
element.addEventListener("click", onClick);
|
||||
return element;
|
||||
};
|
||||
|
||||
button(
|
||||
"close",
|
||||
"chrome://browser/skin/zen-icons/close.svg",
|
||||
"library-media-close",
|
||||
() => this.close()
|
||||
);
|
||||
const prev = button(
|
||||
"prev",
|
||||
"chrome://browser/skin/zen-icons/arrow-left.svg",
|
||||
"library-media-previous",
|
||||
() => this.#step(-1)
|
||||
);
|
||||
const next = button(
|
||||
"next",
|
||||
"chrome://browser/skin/zen-icons/arrow-right.svg",
|
||||
"library-media-next",
|
||||
() => this.#step(1)
|
||||
);
|
||||
|
||||
const nameLabel = make("span", "zen-library-media-preview-name", header);
|
||||
const date = make("span", "zen-library-media-preview-date", header);
|
||||
const more = button(
|
||||
"more",
|
||||
"chrome://global/skin/icons/more.svg",
|
||||
"library-media-more",
|
||||
event => this.#openMenu(event)
|
||||
);
|
||||
const openButton = doc.createXULElement("toolbarbutton");
|
||||
openButton.className = "toolbarbutton-1 zen-library-media-preview-open";
|
||||
header.appendChild(openButton);
|
||||
const openText = doc.createXULElement("label");
|
||||
openText.className = "toolbarbutton-text";
|
||||
openButton.appendChild(openText);
|
||||
openButton.addEventListener("command", () => this.#launch());
|
||||
openButton.addEventListener("click", () => this.#launch());
|
||||
|
||||
const stage = make("div", "zen-library-media-preview-stage", panel);
|
||||
|
||||
this.#parts = {
|
||||
panel,
|
||||
backdrop,
|
||||
header,
|
||||
openText,
|
||||
stage,
|
||||
nameLabel,
|
||||
date,
|
||||
openButton,
|
||||
prev,
|
||||
next,
|
||||
more,
|
||||
};
|
||||
this.#root = root;
|
||||
this.#mountRoot.appendChild(root);
|
||||
}
|
||||
|
||||
get #mountRoot() {
|
||||
return (
|
||||
this.#document.getElementById("zen-main-app-wrapper") ??
|
||||
this.#document.documentElement
|
||||
);
|
||||
}
|
||||
|
||||
// Opening and closing
|
||||
|
||||
/**
|
||||
* @param {object} options - Where to start
|
||||
* @param {object[]} options.items - Everything the grid is showing
|
||||
* @param {number} options.index - The item that was clicked
|
||||
* @param {Element} options.from - The thumbnail it grows out of
|
||||
*/
|
||||
open({ items, index, from }) {
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
if (!this.#root) {
|
||||
this.#build();
|
||||
}
|
||||
this.#items = items;
|
||||
this.#index = index;
|
||||
this.#origin = from;
|
||||
this.#window.addEventListener("keydown", this.#onKeyDown, true);
|
||||
this.#show();
|
||||
this.#root.style.visibility = "hidden";
|
||||
this.#whenMediaReady().then(() => {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.#root.style.visibility = "";
|
||||
this.#morph(from, /* opening = */ true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise} Settles once the picture has a size to animate from,
|
||||
* or right away when it already has one.
|
||||
*/
|
||||
#whenMediaReady() {
|
||||
const media = this.#parts.stage.querySelector(
|
||||
".zen-library-media-preview-media"
|
||||
);
|
||||
if (!media) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
const isVideo = media.localName === "video";
|
||||
const ready = () =>
|
||||
isVideo ? media.readyState >= media.HAVE_CURRENT_DATA : media.complete;
|
||||
if (ready()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const done = () => resolve();
|
||||
media.addEventListener("load", done, { once: true });
|
||||
media.addEventListener("loadeddata", done, { once: true });
|
||||
media.addEventListener("error", done, { once: true });
|
||||
this.#window.setTimeout(done, isVideo ? VIDEO_WAIT_MS : IMAGE_WAIT_MS);
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
this.#window.removeEventListener("keydown", this.#onKeyDown, true);
|
||||
const done = this.#morph(this.#origin, /* opening = */ false);
|
||||
const finish = () => {
|
||||
this.#root?.remove();
|
||||
this.#root = null;
|
||||
this.#parts = {};
|
||||
this.#origin = null;
|
||||
};
|
||||
if (done) {
|
||||
done.finished.then(finish, finish);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#window.removeEventListener("keydown", this.#onKeyDown, true);
|
||||
this.#menu?.remove();
|
||||
this.#menu = null;
|
||||
this.#root?.remove();
|
||||
this.#root = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog straight on top of the card's picture, matching its box
|
||||
* exactly, and grows it into the middle of the window. Closing is the same
|
||||
* run played backwards.
|
||||
*
|
||||
* @param {Element} thumbnail - The card's picture
|
||||
* @param {boolean} opening - Which way round
|
||||
* @returns {Animation} The dialog's animation
|
||||
*/
|
||||
#morph(thumbnail, opening) {
|
||||
const { panel, backdrop, header, stage } = this.#parts;
|
||||
const base = {
|
||||
duration: MORPH_MS,
|
||||
easing: MORPH_EASING,
|
||||
fill: "both",
|
||||
};
|
||||
const steps = keyframes =>
|
||||
opening
|
||||
? keyframes
|
||||
: [...keyframes]
|
||||
.reverse()
|
||||
.map(frame =>
|
||||
frame.offset === undefined
|
||||
? frame
|
||||
: { ...frame, offset: 1 - frame.offset }
|
||||
);
|
||||
const chrome = { ...base, easing: "linear" };
|
||||
const fadeOut = arriving =>
|
||||
opening
|
||||
? arriving
|
||||
: [
|
||||
{ ...arriving.at(-1), offset: 0 },
|
||||
{ ...arriving[0], offset: CHROME_OUT },
|
||||
{ ...arriving[0], offset: 1 },
|
||||
];
|
||||
backdrop.animate(fadeOut([{ opacity: 0 }, { opacity: 1 }]), chrome);
|
||||
const source = thumbnail?.querySelector(".zen-library-media-thumb");
|
||||
const from = source?.getBoundingClientRect();
|
||||
const to = panel.getBoundingClientRect();
|
||||
if (!from?.width || !to.width) {
|
||||
return backdrop.animate(
|
||||
fadeOut([{ opacity: 0 }, { opacity: 1 }]),
|
||||
chrome
|
||||
);
|
||||
}
|
||||
const media = stage?.querySelector(".zen-library-media-preview-media");
|
||||
const shown = media?.getBoundingClientRect();
|
||||
const target = shown?.width ? shown : to;
|
||||
const scale = from.width / target.width;
|
||||
const centre = box => ({
|
||||
x: box.left + box.width / 2,
|
||||
y: box.top + box.height / 2,
|
||||
});
|
||||
const panelCentre = centre(to);
|
||||
const shownCentre = centre(target);
|
||||
const cardCentre = centre(from);
|
||||
const atCard =
|
||||
`translate(${cardCentre.x - panelCentre.x - scale * (shownCentre.x - panelCentre.x)}px, ` +
|
||||
`${cardCentre.y - panelCentre.y - scale * (shownCentre.y - panelCentre.y)}px) ` +
|
||||
`scale(${scale})`;
|
||||
const panelStyle = this.#window.getComputedStyle(panel);
|
||||
const late = (values, property) => {
|
||||
const hidden = property === "boxShadow" ? "none" : "transparent";
|
||||
return opening
|
||||
? [
|
||||
{ [property]: hidden, offset: 0 },
|
||||
{ [property]: hidden, offset: HANDOVER },
|
||||
{ [property]: values, offset: 1 },
|
||||
]
|
||||
: [
|
||||
{ [property]: values, offset: 0 },
|
||||
{ [property]: hidden, offset: CHROME_OUT },
|
||||
{ [property]: hidden, offset: 1 },
|
||||
];
|
||||
};
|
||||
panel.animate(late(panelStyle.backgroundColor, "backgroundColor"), chrome);
|
||||
panel.animate(late(panelStyle.boxShadow, "boxShadow"), chrome);
|
||||
if (stage) {
|
||||
const stageStyle = this.#window.getComputedStyle(stage);
|
||||
stage.animate(
|
||||
late(stageStyle.backgroundColor, "backgroundColor"),
|
||||
chrome
|
||||
);
|
||||
stage.animate(late(stageStyle.borderTopColor, "borderTopColor"), chrome);
|
||||
}
|
||||
header.animate(
|
||||
fadeOut([
|
||||
{ opacity: 0, offset: 0 },
|
||||
{ opacity: 0, offset: HANDOVER },
|
||||
{ opacity: 1, offset: 1 },
|
||||
]),
|
||||
chrome
|
||||
);
|
||||
if (media && shown?.width) {
|
||||
media.animate(
|
||||
steps([
|
||||
{ borderRadius: `${Math.round(MEDIA_RADIUS / scale)}px` },
|
||||
{ borderRadius: `${MEDIA_RADIUS}px` },
|
||||
]),
|
||||
base
|
||||
);
|
||||
}
|
||||
return panel.animate(
|
||||
steps([{ transform: atCard }, { transform: "none" }]),
|
||||
base
|
||||
);
|
||||
}
|
||||
|
||||
#step(delta) {
|
||||
const next = this.#index + delta;
|
||||
if (next < 0 || next >= this.#items.length) {
|
||||
return;
|
||||
}
|
||||
this.#index = next;
|
||||
this.#origin = this.#thumbFor(this.#item);
|
||||
this.#show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} item - The media being shown
|
||||
* @returns {Element|null} Its card in the grid, so closing lands back on it
|
||||
*/
|
||||
#thumbFor(item) {
|
||||
const section = this.#document.querySelector("zen-library-media-section");
|
||||
const cards = [
|
||||
...(section?.querySelectorAll(".zen-library-media-item") ?? []),
|
||||
];
|
||||
return (
|
||||
cards
|
||||
.find(card => card.title === item?.name)
|
||||
?.querySelector(".zen-library-media-frame") ?? this.#origin
|
||||
);
|
||||
}
|
||||
|
||||
#handleKey(event) {
|
||||
if (!this.isOpen) {
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case "Escape":
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.stopImmediatePropagation();
|
||||
this.close();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
this.#step(-1);
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.#step(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Contents
|
||||
|
||||
#show() {
|
||||
const item = this.#item;
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const { stage, nameLabel, date, openText, prev, next } = this.#parts;
|
||||
const doc = this.#document;
|
||||
stage.replaceChildren();
|
||||
const media = doc.createElementNS(
|
||||
HTML_NS,
|
||||
item.kind === "video" ? "video" : "img"
|
||||
);
|
||||
media.className = "zen-library-media-preview-media";
|
||||
media.src = item.url;
|
||||
if (item.kind !== "video") {
|
||||
media.draggable = true;
|
||||
media.addEventListener("dragstart", event =>
|
||||
this.#onDragStart(event, item)
|
||||
);
|
||||
}
|
||||
if (item.kind === "video") {
|
||||
media.controls = true;
|
||||
media.autoplay = true;
|
||||
media.preload = "auto";
|
||||
} else {
|
||||
media.alt = "";
|
||||
}
|
||||
stage.appendChild(media);
|
||||
|
||||
nameLabel.textContent = item.name;
|
||||
date.textContent = this.#relativeDate(item.modified);
|
||||
doc.l10n
|
||||
.formatValue("library-media-open-in", { app: this.#openerName(item) })
|
||||
.then(text => {
|
||||
openText.textContent = text;
|
||||
});
|
||||
prev.disabled = this.#index <= 0;
|
||||
next.disabled = this.#index >= this.#items.length - 1;
|
||||
}
|
||||
|
||||
#relativeDate(modified) {
|
||||
const days = Math.round((modified - Date.now()) / MS_PER_DAY);
|
||||
if (Math.abs(days) >= 7) {
|
||||
return lazy.relativeDayFormat.format(Math.round(days / 7), "week");
|
||||
}
|
||||
return lazy.relativeDayFormat.format(days, "day");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} item - The media being shown
|
||||
* @returns {string} What the system opens this kind of file with
|
||||
*/
|
||||
#openerName(item) {
|
||||
try {
|
||||
const mime = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
|
||||
const info = mime.getFromTypeAndExtension(null, item.extension);
|
||||
return info.defaultDescription || info.description || "";
|
||||
} catch (error) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
#onDragStart(event, item) {
|
||||
const file = new lazy.FileUtils.File(item.path);
|
||||
if (!file.exists()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
event.dataTransfer.mozSetDataAt("application/x-moz-file", file, 0);
|
||||
event.dataTransfer.effectAllowed = "copyMove";
|
||||
event.dataTransfer.setData("text/uri-list", item.url);
|
||||
}
|
||||
|
||||
#launch() {
|
||||
const item = this.#item;
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new lazy.FileUtils.File(item.path).launch();
|
||||
} catch (error) {
|
||||
this.#window.openTrustedLinkIn(item.url, "tab");
|
||||
}
|
||||
}
|
||||
|
||||
#reveal() {
|
||||
const item = this.#item;
|
||||
if (item) {
|
||||
new lazy.FileUtils.File(item.path).reveal();
|
||||
}
|
||||
}
|
||||
|
||||
#copy() {
|
||||
const item = this.#item;
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
const transferable = Cc[
|
||||
"@mozilla.org/widget/transferable;1"
|
||||
].createInstance(Ci.nsITransferable);
|
||||
transferable.init(this.#window.docShell.QueryInterface(Ci.nsILoadContext));
|
||||
transferable.addDataFlavor("application/x-moz-file");
|
||||
transferable.setTransferData(
|
||||
"application/x-moz-file",
|
||||
new lazy.FileUtils.File(item.path)
|
||||
);
|
||||
Services.clipboard.setData(
|
||||
transferable,
|
||||
null,
|
||||
Services.clipboard.kGlobalClipboard
|
||||
);
|
||||
}
|
||||
|
||||
#openMenu(event) {
|
||||
if (!this.#menu) {
|
||||
this.#menu = this.#window.MozXULElement.parseXULToFragment(`
|
||||
<menupopup class="zen-library-media-menu">
|
||||
<menuitem data-action="open" data-l10n-id="library-media-menu-open"/>
|
||||
<menuitem data-action="show" data-l10n-id="downloads-cmd-show-menuitem-2"/>
|
||||
<menuseparator/>
|
||||
<menuitem data-action="copy" data-l10n-id="library-media-menu-copy"/>
|
||||
</menupopup>
|
||||
`).firstElementChild;
|
||||
this.#menu.addEventListener("command", menuEvent => {
|
||||
switch (menuEvent.target.dataset.action) {
|
||||
case "open":
|
||||
this.#launch();
|
||||
break;
|
||||
case "show":
|
||||
this.#reveal();
|
||||
break;
|
||||
case "copy":
|
||||
this.#copy();
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.#document.getElementById("mainPopupSet").appendChild(this.#menu);
|
||||
}
|
||||
this.#menu.openPopup(event.currentTarget, "after_end", 0, 0, false, false);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
DownloadsCommon:
|
||||
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
@@ -290,9 +291,10 @@ class ZenLibraryDownloadStack {
|
||||
badge.parentElement.toggleAttribute("downloading", pending);
|
||||
}
|
||||
if (download) {
|
||||
const preview = this.#previewUrl(download);
|
||||
badge.style.setProperty(
|
||||
"--download-image",
|
||||
`url('${this.#iconUrl(download)}')`
|
||||
`${preview ? `url('${preview}'), ` : ""}url('${this.#iconUrl(download)}')`
|
||||
);
|
||||
}
|
||||
badge
|
||||
@@ -398,6 +400,18 @@ class ZenLibraryDownloadStack {
|
||||
: download.source.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} download - The download an entry stands for
|
||||
* @returns {string|null} The finished file itself, when it is a picture
|
||||
*/
|
||||
#previewUrl(download) {
|
||||
const path = download.succeeded && download.target.path;
|
||||
if (!path || !lazy.canDrawThumbnail(path)) {
|
||||
return null;
|
||||
}
|
||||
return PathUtils.toFileURI(path);
|
||||
}
|
||||
|
||||
#isPending(download) {
|
||||
return (
|
||||
!!download &&
|
||||
|
||||
@@ -12,5 +12,7 @@ MOZ_SRC_FILES += [
|
||||
|
||||
"ZenLibrary.mjs",
|
||||
"ZenLibraryDragAndDrop.mjs",
|
||||
"ZenLibraryFileTypes.sys.mjs",
|
||||
"ZenLibraryMediaPreview.mjs",
|
||||
"ZenLibraryWidget.sys.mjs",
|
||||
]
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
FILE_GROUPS: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
fileGroupOf: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
|
||||
DownloadsCommon:
|
||||
@@ -25,22 +28,6 @@ ChromeUtils.defineESModuleGetters(lazy, {
|
||||
const FILE_MIME = "application/x-moz-file";
|
||||
const OPENING_FEEDBACK_MS = 1500;
|
||||
|
||||
const FILE_TYPES = {
|
||||
images: "png jpg jpeg gif webp svg bmp tif tiff heic heif avif ico",
|
||||
video: "mp4 mkv mov avi webm m4v wmv flv mpg mpeg",
|
||||
audio: "mp3 wav flac aac ogg oga m4a opus wma aiff",
|
||||
documents:
|
||||
"pdf doc docx xls xlsx ppt pptx txt md rtf odt ods odp csv epub pages numbers key",
|
||||
archives: "zip rar 7z tar gz bz2 xz tgz zst",
|
||||
apps: "dmg pkg exe msi app deb rpm appimage apk jar",
|
||||
};
|
||||
const EXTENSION_TYPES = new Map();
|
||||
for (const [type, extensions] of Object.entries(FILE_TYPES)) {
|
||||
for (const extension of extensions.split(" ")) {
|
||||
EXTENSION_TYPES.set(extension, type);
|
||||
}
|
||||
}
|
||||
|
||||
export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||
static id = "downloads";
|
||||
static label = "library-downloads-section-title";
|
||||
@@ -103,7 +90,7 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||
{
|
||||
id: "type",
|
||||
titleL10nId: "library-downloads-filter-type",
|
||||
options: Object.keys(FILE_TYPES).map(id => ({
|
||||
options: lazy.FILE_GROUPS.map(id => ({
|
||||
id,
|
||||
l10nId: `library-downloads-type-${id}`,
|
||||
})),
|
||||
@@ -214,14 +201,11 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||
}
|
||||
|
||||
#fileType(download) {
|
||||
const extension = this.#fileName(download).match(/\.([^.]+)$/)?.[1];
|
||||
return extension ? EXTENSION_TYPES.get(extension.toLowerCase()) : undefined;
|
||||
return lazy.fileGroupOf(this.#fileName(download)) ?? undefined;
|
||||
}
|
||||
|
||||
#activeTypes() {
|
||||
return Object.keys(FILE_TYPES).filter(id =>
|
||||
this.isFilterActive("type", id)
|
||||
);
|
||||
return lazy.FILE_GROUPS.filter(id => this.isFilterActive("type", id));
|
||||
}
|
||||
|
||||
#whenCutoff() {
|
||||
@@ -260,6 +244,19 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} download - The download a row stands for
|
||||
* @returns {string|null} The finished file itself, when it is a picture
|
||||
* worth showing in place of a file icon
|
||||
*/
|
||||
#previewUrl(download) {
|
||||
const path = download.succeeded && download.target.path;
|
||||
if (!path || !lazy.canDrawThumbnail(path)) {
|
||||
return null;
|
||||
}
|
||||
return PathUtils.toFileURI(path);
|
||||
}
|
||||
|
||||
#isPending(download) {
|
||||
return !download.stopped || (download.canceled && download.hasPartialData);
|
||||
}
|
||||
@@ -597,7 +594,12 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
|
||||
>
|
||||
<img
|
||||
class="zen-library-row-icon"
|
||||
src=${this.#iconUrl(download)}
|
||||
?preview=${!!this.#previewUrl(download)}
|
||||
src=${this.#previewUrl(download) ?? this.#iconUrl(download)}
|
||||
@error=${event => {
|
||||
event.target.removeAttribute("preview");
|
||||
event.target.src = this.#iconUrl(download);
|
||||
}}
|
||||
alt=""
|
||||
/>
|
||||
<div class="zen-library-row-text">
|
||||
|
||||
@@ -3,12 +3,73 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { html } from "chrome://global/content/vendor/lit.all.mjs";
|
||||
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
import { ZenLibrarySearchSection } from "moz-src:///zen/library/sections/ZenLibrarySearchSection.mjs";
|
||||
import { ZenLibraryMediaPreview } from "moz-src:///zen/library/ZenLibraryMediaPreview.mjs";
|
||||
|
||||
export class ZenLibraryMediaSection extends MozLitElement {
|
||||
let lazy = {};
|
||||
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
|
||||
canDrawThumbnail: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
mediaKindOf: "moz-src:///zen/library/ZenLibraryFileTypes.sys.mjs",
|
||||
});
|
||||
|
||||
const FILE_MIME = "application/x-moz-file";
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const FOLDERS_PREF = "zen.library.media.folders";
|
||||
const ENABLED_PREF = "zen.library.media.enabled";
|
||||
const SKELETON_CARDS = 10;
|
||||
|
||||
const FOLDERS = {
|
||||
downloads: { key: "DfltDwnld", home: "Downloads" },
|
||||
documents: { key: "Docs", home: "Documents" },
|
||||
desktop: { key: "Desk", home: "Desktop" },
|
||||
pictures: { home: "Pictures" },
|
||||
music: { home: "Music" },
|
||||
videos: { home: AppConstants.platform === "macosx" ? "Movies" : "Videos" },
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} fileName - A file found while looking through a folder
|
||||
* @returns {string|null} How the grid shows it
|
||||
*/
|
||||
function kindFor(fileName) {
|
||||
const kind = lazy.mediaKindOf(fileName);
|
||||
if (kind === "video") {
|
||||
return "video";
|
||||
}
|
||||
if (kind === "image") {
|
||||
return lazy.canDrawThumbnail(fileName) ? "image" : "file";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const MAX_DEPTH = 2;
|
||||
const MAX_FOLDERS = 60;
|
||||
const MAX_ITEMS = 300;
|
||||
|
||||
export class ZenLibraryMediaSection extends ZenLibrarySearchSection {
|
||||
static id = "media";
|
||||
static label = "library-media-section-title";
|
||||
|
||||
static properties = {
|
||||
items: { state: true },
|
||||
loading: { state: true },
|
||||
};
|
||||
|
||||
#preview = null;
|
||||
#scanId = 0;
|
||||
#scanned = [];
|
||||
#scanHandle = null;
|
||||
#thumbObserver = null;
|
||||
#cardItems = new WeakMap();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.items = [];
|
||||
this.loading = true;
|
||||
}
|
||||
|
||||
static render(library) {
|
||||
return html`
|
||||
<zen-library-media-section
|
||||
@@ -19,9 +80,391 @@ export class ZenLibraryMediaSection extends MozLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
get searchPlaceholderL10nId() {
|
||||
return "library-media-search-placeholder";
|
||||
}
|
||||
|
||||
get filterTitleL10nId() {
|
||||
return "library-media-filter-title";
|
||||
}
|
||||
|
||||
get filterGroups() {
|
||||
if (!this.#enabled || this.#scanned.length < 2) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "folder",
|
||||
titleL10nId: "library-media-filter-folders",
|
||||
options: this.#scanned.map(path => ({
|
||||
id: path,
|
||||
label: PathUtils.filename(path),
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
onFiltersChanged() {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!this.#enabled) {
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
this.#scanHandle = window.requestIdleCallback(() => this.#scan(), {
|
||||
timeout: 600,
|
||||
});
|
||||
}
|
||||
|
||||
/** @returns {boolean} Whether looking through the folders was agreed to */
|
||||
get #enabled() {
|
||||
return Services.prefs.getBoolPref(ENABLED_PREF, false);
|
||||
}
|
||||
|
||||
#enable() {
|
||||
Services.prefs.setBoolPref(ENABLED_PREF, true);
|
||||
this.loading = true;
|
||||
this.requestUpdate();
|
||||
this.#scan();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.#scanId++;
|
||||
if (this.#scanHandle) {
|
||||
window.cancelIdleCallback(this.#scanHandle);
|
||||
this.#scanHandle = null;
|
||||
}
|
||||
this.#thumbObserver?.disconnect();
|
||||
this.#thumbObserver = null;
|
||||
this.#preview?.destroy();
|
||||
this.#preview = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]} Absolute paths of the folders to look through. The
|
||||
* pref takes named locations such as "downloads" and absolute paths
|
||||
* alike, and anything that cannot be found is passed over.
|
||||
*/
|
||||
#folders() {
|
||||
const home = Services.dirsvc.get("Home", Ci.nsIFile).path;
|
||||
const paths = [];
|
||||
for (const raw of Services.prefs
|
||||
.getStringPref(FOLDERS_PREF, "")
|
||||
.split(",")) {
|
||||
const entry = raw.trim();
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
if (PathUtils.isAbsolute(entry)) {
|
||||
paths.push(entry);
|
||||
continue;
|
||||
}
|
||||
const folder = FOLDERS[entry.toLowerCase()];
|
||||
if (!folder) {
|
||||
continue;
|
||||
}
|
||||
let path = null;
|
||||
if (folder.key) {
|
||||
try {
|
||||
path = Services.dirsvc.get(folder.key, Ci.nsIFile).path;
|
||||
} catch (error) {
|
||||
path = null;
|
||||
}
|
||||
}
|
||||
paths.push(path ?? PathUtils.join(home, folder.home));
|
||||
}
|
||||
return [...new Set(paths)];
|
||||
}
|
||||
|
||||
async #scan() {
|
||||
const scanId = ++this.#scanId;
|
||||
this.#scanHandle = null;
|
||||
this.loading = true;
|
||||
const found = [];
|
||||
const budget = { folders: MAX_FOLDERS };
|
||||
this.#scanned = this.#folders();
|
||||
for (const folder of this.#scanned) {
|
||||
budget.root = folder;
|
||||
await this.#walk(folder, 0, found, budget);
|
||||
if (scanId !== this.#scanId) {
|
||||
return;
|
||||
}
|
||||
// Let the window breathe between folders.
|
||||
await new Promise(resolve =>
|
||||
window.requestIdleCallback(resolve, { timeout: 100 })
|
||||
);
|
||||
}
|
||||
found.sort((a, b) => b.modified - a.modified);
|
||||
this.items = found.slice(0, MAX_ITEMS);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path - Folder to look through
|
||||
* @param {number} depth - How far below the named folder this is
|
||||
* @param {object[]} found - Collects the media
|
||||
* @param {object} budget - Caps how many folders one scan opens
|
||||
*/
|
||||
async #walk(path, depth, found, budget) {
|
||||
if (depth > MAX_DEPTH || budget.folders <= 0) {
|
||||
return;
|
||||
}
|
||||
budget.folders--;
|
||||
let children = [];
|
||||
try {
|
||||
children = await IOUtils.getChildren(path);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
const candidates = [];
|
||||
for (const child of children) {
|
||||
const fileName = PathUtils.filename(child);
|
||||
if (fileName.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
const dot = fileName.lastIndexOf(".");
|
||||
const extension = dot > 0 ? fileName.slice(dot + 1).toLowerCase() : "";
|
||||
const kind = kindFor(fileName);
|
||||
// A name with a suffix that is not media is a file to pass over, or a
|
||||
// bundle on macOS. Either way there is nothing to ask the disk about.
|
||||
if (!kind && extension) {
|
||||
continue;
|
||||
}
|
||||
candidates.push({ child, fileName, extension, kind });
|
||||
}
|
||||
const stats = await Promise.all(
|
||||
candidates.map(candidate =>
|
||||
IOUtils.stat(candidate.child).catch(() => null)
|
||||
)
|
||||
);
|
||||
const folders = [];
|
||||
for (const [index, candidate] of candidates.entries()) {
|
||||
const info = stats[index];
|
||||
if (!info) {
|
||||
continue;
|
||||
}
|
||||
if (info.type === "directory") {
|
||||
if (!candidate.extension) {
|
||||
folders.push(candidate.child);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!candidate.kind) {
|
||||
continue;
|
||||
}
|
||||
found.push({
|
||||
folder: budget.root,
|
||||
path: candidate.child,
|
||||
name: candidate.fileName,
|
||||
extension: candidate.extension,
|
||||
kind: candidate.kind,
|
||||
modified: info.lastModified,
|
||||
size: info.size,
|
||||
url: PathUtils.toFileURI(candidate.child),
|
||||
});
|
||||
}
|
||||
for (const folder of folders) {
|
||||
await this.#walk(folder, depth + 1, found, budget);
|
||||
}
|
||||
}
|
||||
|
||||
// Interaction
|
||||
|
||||
#onDragStart(event, item) {
|
||||
const file = new lazy.FileUtils.File(item.path);
|
||||
if (!file.exists()) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const { dataTransfer } = event;
|
||||
dataTransfer.mozSetDataAt(FILE_MIME, file, 0);
|
||||
dataTransfer.effectAllowed = "copyMove";
|
||||
dataTransfer.setData("text/uri-list", item.url);
|
||||
dataTransfer.addElement(event.currentTarget);
|
||||
}
|
||||
|
||||
#onItemClick(event, item) {
|
||||
this.#preview ??= new ZenLibraryMediaPreview(window);
|
||||
const shown = this.#shown;
|
||||
this.#preview.open({
|
||||
items: shown,
|
||||
index: shown.indexOf(item),
|
||||
from: event.currentTarget.querySelector(".zen-library-media-frame"),
|
||||
});
|
||||
}
|
||||
|
||||
// Rendering
|
||||
|
||||
/**
|
||||
* Builds one card, empty. Its picture is fetched only once the card comes
|
||||
* into view, so opening the tab does not read every file at once.
|
||||
*
|
||||
* @param {object} item - The media the card stands for
|
||||
* @returns {Element} Its card
|
||||
*/
|
||||
#buildCard(item) {
|
||||
const card = document.createElementNS(HTML_NS, "div");
|
||||
card.className = "zen-library-media-item";
|
||||
card.draggable = true;
|
||||
card.title = item.name;
|
||||
card.addEventListener("dragstart", event => this.#onDragStart(event, item));
|
||||
card.addEventListener("click", event => this.#onItemClick(event, item));
|
||||
|
||||
const frame = document.createElementNS(HTML_NS, "div");
|
||||
frame.className = "zen-library-media-frame";
|
||||
frame.dataset.kind = item.kind;
|
||||
card.appendChild(frame);
|
||||
|
||||
this.#cardItems.set(card, item);
|
||||
return card;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} card - A card that has come into view
|
||||
*/
|
||||
#loadThumb(card) {
|
||||
const item = this.#cardItems.get(card);
|
||||
const frame = card.firstElementChild;
|
||||
if (!item || !frame || frame.childElementCount) {
|
||||
return;
|
||||
}
|
||||
const make = tag => {
|
||||
const element = document.createElementNS(HTML_NS, tag);
|
||||
frame.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
if (item.kind === "video") {
|
||||
const video = make("video");
|
||||
video.className = "zen-library-media-thumb";
|
||||
video.src = `${item.url}#t=0.1`;
|
||||
video.preload = "metadata";
|
||||
video.muted = true;
|
||||
video.tabIndex = -1;
|
||||
const badge = make("img");
|
||||
badge.className = "zen-library-media-play no-squircles";
|
||||
badge.src = "chrome://browser/skin/zen-icons/media-play.svg";
|
||||
badge.alt = "";
|
||||
return;
|
||||
}
|
||||
const image = make("img");
|
||||
image.className =
|
||||
item.kind === "image"
|
||||
? "zen-library-media-thumb"
|
||||
: "zen-library-media-thumb zen-library-media-file";
|
||||
image.src =
|
||||
item.kind === "image" ? item.url : `moz-icon://${item.name}?size=128`;
|
||||
image.alt = "";
|
||||
}
|
||||
|
||||
#fillGrid() {
|
||||
const grid = this.querySelector(".zen-library-media-grid");
|
||||
if (!grid || !this.#enabled) {
|
||||
return;
|
||||
}
|
||||
const shown = this.#shown;
|
||||
const signature = shown.map(item => item.path).join("\n");
|
||||
if (grid.dataset.signature === signature) {
|
||||
return;
|
||||
}
|
||||
grid.dataset.signature = signature;
|
||||
this.#thumbObserver?.disconnect();
|
||||
this.#thumbObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
this.#loadThumb(entry.target);
|
||||
this.#thumbObserver.unobserve(entry.target);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
root: this.querySelector(".zen-library-search-results"),
|
||||
rootMargin: "300px",
|
||||
}
|
||||
);
|
||||
const fragment = document.createDocumentFragment();
|
||||
const cards = shown.map(item => this.#buildCard(item));
|
||||
for (const card of cards) {
|
||||
fragment.appendChild(card);
|
||||
}
|
||||
grid.replaceChildren(fragment);
|
||||
for (const card of cards) {
|
||||
this.#thumbObserver.observe(card);
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {object[]} What the grid shows, once the search has its say */
|
||||
get #shown() {
|
||||
const folders = this.#scanned.filter(path =>
|
||||
this.isFilterActive("folder", path)
|
||||
);
|
||||
const query = this.searchQuery.trim().toLowerCase();
|
||||
return this.items.filter(
|
||||
item =>
|
||||
(!folders.length || folders.includes(item.folder)) &&
|
||||
(!query || item.name.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
|
||||
renderItems() {
|
||||
if (!this.#enabled) {
|
||||
return this.#renderOptIn();
|
||||
}
|
||||
if (this.loading) {
|
||||
return html`
|
||||
<div
|
||||
class="zen-library-empty"
|
||||
data-l10n-id="library-media-loading"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
const shown = this.#shown;
|
||||
if (!shown.length) {
|
||||
return html`
|
||||
<div class="zen-library-empty" data-l10n-id="library-media-empty"></div>
|
||||
`;
|
||||
}
|
||||
return html`<div class="zen-library-media-grid"></div>`;
|
||||
}
|
||||
|
||||
updated(changedProperties) {
|
||||
super.updated?.(changedProperties);
|
||||
this.toggleAttribute("needs-opt-in", !this.#enabled);
|
||||
this.#fillGrid();
|
||||
}
|
||||
|
||||
/**
|
||||
* An empty grid behind a note saying what turning this on would do. The
|
||||
* cards are only shapes: nothing has been looked at yet.
|
||||
*/
|
||||
#renderOptIn() {
|
||||
return html`
|
||||
<div class="zen-library-empty" data-l10n-id="library-media-empty"></div>
|
||||
<div class="zen-library-media-grid zen-library-media-skeleton" behind>
|
||||
${Array.from(
|
||||
{ length: SKELETON_CARDS },
|
||||
() => html`<div class="zen-library-media-item"></div>`
|
||||
)}
|
||||
</div>
|
||||
<div class="zen-library-media-opt-in">
|
||||
<div class="zen-library-media-opt-in-icon">
|
||||
<div class="empty-state-icon-image"></div>
|
||||
</div>
|
||||
<h3 data-l10n-id="library-media-opt-in-title"></h3>
|
||||
<p data-l10n-id="library-media-opt-in-body"></p>
|
||||
<button
|
||||
class="zen-big-accent-button primary zen-library-media-opt-in-button"
|
||||
data-l10n-id="library-media-opt-in-button"
|
||||
@click=${() => this.#enable()}
|
||||
></button>
|
||||
<div class="zen-library-media-opt-in-note">
|
||||
<img src="chrome://global/skin/icons/security.svg" alt="" />
|
||||
<span data-l10n-id="library-media-opt-in-note"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
||||
import { ZenLibraryDragAndDrop } from "moz-src:///zen/library/ZenLibraryDragAndDrop.mjs";
|
||||
|
||||
const GRADIENT_TOPIC = "zen-space-gradient-update";
|
||||
const SIZING_FALLBACK_MS = 600;
|
||||
const SCROLL_EDGE_PX = 48;
|
||||
const SCROLL_STEP_PX = 12;
|
||||
|
||||
@@ -69,8 +70,6 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
#observer = { observe: () => this.requestUpdate() };
|
||||
#onDataChanged = () => this.requestUpdate();
|
||||
#onStripEvent = event => {
|
||||
// The copies raise the same events as they are built; those are not
|
||||
// changes to the real strips.
|
||||
if (this.contains(event.target)) {
|
||||
return;
|
||||
}
|
||||
@@ -100,15 +99,13 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
this.#dnd.clearDragOverVisuals();
|
||||
this.#refreshStrips();
|
||||
};
|
||||
// A drag from a copy ran on the strip's drag and drop, whose own drag end
|
||||
// only hears drags from the strip; it gets to finish this one too. The
|
||||
// event comes to the copy itself, which the drop's rebuild has taken out
|
||||
// of the document by then, so it never reaches the window.
|
||||
#onCopyDragEnd = event => {
|
||||
gBrowser.tabContainer.tabDragAndDrop.handle_dragend(event);
|
||||
this.#onDragEnd();
|
||||
};
|
||||
#landing = false;
|
||||
#sizingTimer = null;
|
||||
#pendingGroups = [];
|
||||
/** @type {WeakMap<Element, Element>} copied tab or group to the real one */
|
||||
#realElements = new WeakMap();
|
||||
#dnd = new ZenLibraryDragAndDrop(this);
|
||||
@@ -162,6 +159,8 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
}
|
||||
clearTimeout(this.#refreshTimer);
|
||||
this.#refreshTimer = null;
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
this.library?.removeAttribute("sizing");
|
||||
this.library?.style.removeProperty("--zen-library-content-width");
|
||||
}
|
||||
|
||||
@@ -179,17 +178,55 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
}
|
||||
}
|
||||
|
||||
onShown() {
|
||||
this.#updateLibraryWidth();
|
||||
}
|
||||
|
||||
onHidden() {
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
this.library?.removeAttribute("sizing");
|
||||
this.library?.style.removeProperty("--zen-library-content-width");
|
||||
}
|
||||
|
||||
#updateLibraryWidth() {
|
||||
const side = this.library?.querySelector("#zen-library-side");
|
||||
const list = this.querySelector(".zen-library-spaces");
|
||||
if (!side || !list) {
|
||||
if (!side || !list || this.hidden) {
|
||||
return;
|
||||
}
|
||||
const sideWidth = window.windowUtils.getBoundsWithoutFlushing(side).width;
|
||||
this.library.style.setProperty(
|
||||
"--zen-library-content-width",
|
||||
`${sideWidth + this.#cardsWidth(list)}px`
|
||||
);
|
||||
const width = `${sideWidth + this.#cardsWidth(list)}px`;
|
||||
if (
|
||||
this.library.style.getPropertyValue("--zen-library-content-width") ===
|
||||
width
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.library.style.setProperty("--zen-library-content-width", width);
|
||||
this.#markResizing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the cards' scrollbars back while the library grows or shrinks
|
||||
* around them, where they would otherwise flash into view for the length
|
||||
* of the animation.
|
||||
*/
|
||||
#markResizing() {
|
||||
const library = this.library;
|
||||
library.setAttribute("sizing", "true");
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
const done = () => {
|
||||
window.clearTimeout(this.#sizingTimer);
|
||||
library.removeAttribute("sizing");
|
||||
library.removeEventListener("transitionend", onEnd);
|
||||
};
|
||||
const onEnd = event => {
|
||||
if (event.target === library && event.propertyName === "width") {
|
||||
done();
|
||||
}
|
||||
};
|
||||
library.addEventListener("transitionend", onEnd);
|
||||
this.#sizingTimer = window.setTimeout(done, SIZING_FALLBACK_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,14 +322,20 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
continue;
|
||||
}
|
||||
const before = rebuild ? this.#rowPositions(strip) : null;
|
||||
strip.textContent = "";
|
||||
const space = gZenWorkspaces.workspaceElement(card.dataset.uuid);
|
||||
if (!space) {
|
||||
strip.textContent = "";
|
||||
continue;
|
||||
}
|
||||
// Built away from the document, so a card's rows land in one go
|
||||
// rather than one reflow at a time.
|
||||
const rows = document.createDocumentFragment();
|
||||
this.#pendingGroups = [];
|
||||
for (const section of [space.pinnedTabsContainer, space.tabsContainer]) {
|
||||
this.#appendCopy(strip, section);
|
||||
this.#appendCopy(rows, section);
|
||||
}
|
||||
strip.replaceChildren(rows);
|
||||
this.#fillGroups();
|
||||
// Rows slide to their new places, unless the drag image is landing on
|
||||
// the moved one, which is motion enough.
|
||||
if (before && !this.#landing) {
|
||||
@@ -301,6 +344,22 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills every copied group now that the strip is in the document. A group
|
||||
* nested inside another is connected as soon as its parent takes it, so
|
||||
* this list drains in order.
|
||||
*/
|
||||
#fillGroups() {
|
||||
while (this.#pendingGroups.length) {
|
||||
const { copy, inner } = this.#pendingGroups.shift();
|
||||
for (const child of inner.children) {
|
||||
if (!child.classList.contains("zen-tab-group-start")) {
|
||||
this.#appendCopy(copy, child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The top-left of every row in a strip, keyed by the copy's stable id, so a
|
||||
* row can be matched across a rebuild.
|
||||
@@ -405,12 +464,12 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
...[...realIcon.children].map(child => child.cloneNode(true))
|
||||
);
|
||||
}
|
||||
const inner = node.querySelector(":scope > .tab-group-container") ?? node;
|
||||
for (const child of inner.children) {
|
||||
if (!child.classList.contains("zen-tab-group-start")) {
|
||||
this.#appendCopy(copy, child);
|
||||
}
|
||||
}
|
||||
// A group empties itself and builds its own scaffolding the moment it
|
||||
// joins the document, so its rows are put in afterwards.
|
||||
this.#pendingGroups.push({
|
||||
copy,
|
||||
inner: node.querySelector(":scope > .tab-group-container") ?? node,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!node.querySelector("tab, tab-group, zen-folder")) {
|
||||
@@ -451,6 +510,7 @@ export class ZenLibrarySpacesSection extends MozLitElement {
|
||||
} else {
|
||||
gZenFolders.animateExpand(copy);
|
||||
}
|
||||
gZenFolders.relayoutCollapsedFolder(group);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,32 +4,6 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
#zen-appcontent-wrapper {
|
||||
--library-progress: 0;
|
||||
--library-wrapper-target-px: 0px;
|
||||
transform: translateX(calc(var(--library-progress) * var(--library-wrapper-target-px)));
|
||||
}
|
||||
|
||||
#navigator-toolbox {
|
||||
--toolbox-progress: min(1, calc(var(--library-progress) * 1.5));
|
||||
|
||||
transform: scale(calc(1 - var(--toolbox-progress) * 0.04));
|
||||
opacity: calc(1 - var(--toolbox-progress)) !important;
|
||||
}
|
||||
|
||||
#zen-main-app-wrapper[zen-compact-mode="true"] #navigator-toolbox {
|
||||
@media -moz-pref('zen.view.compact.hide-tabbar') or -moz-pref('zen.view.use-single-toolbar') {
|
||||
transform: translateX(calc(var(--toolbox-progress) * -100%));
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
:root[zen-right-side="true"] #zen-main-app-wrapper[zen-compact-mode="true"] #navigator-toolbox {
|
||||
@media -moz-pref('zen.view.compact.hide-tabbar') or -moz-pref('zen.view.use-single-toolbar') {
|
||||
transform: translateX(calc(var(--toolbox-progress) * 100%));
|
||||
}
|
||||
}
|
||||
|
||||
:root:not([zen-sidebar-expanded="true"]) .zen-library-window-buttons-clone {
|
||||
display: none;
|
||||
}
|
||||
@@ -54,7 +28,6 @@ zen-library {
|
||||
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transform: translateX(calc(-100% * (1 - var(--library-progress, 0))));
|
||||
|
||||
& #zen-library-panel {
|
||||
flex-direction: row;
|
||||
@@ -106,14 +79,14 @@ zen-library[open] {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
zen-library[transitioning] {
|
||||
& #zen-library-content {
|
||||
pointer-events: none;
|
||||
}
|
||||
zen-library[transitioning] #zen-library-content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
& :is(.zen-library-search-results, .zen-library-spaces, .zen-library-space-body) {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
zen-library:is([transitioning], [sizing])
|
||||
:is(.zen-library-search-results, .zen-library-spaces, .zen-library-space-body) {
|
||||
scrollbar-width: none;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#zen-library-panel {
|
||||
@@ -310,6 +283,187 @@ zen-library[zen-library-downloading] .zen-library-tab[data-section="downloads"]:
|
||||
background-image: url("chrome://browser/skin/zen-icons/library/library-media-sprite.svg");
|
||||
}
|
||||
|
||||
.zen-library-media-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
|
||||
gap: 12px;
|
||||
padding: 6px;
|
||||
align-content: start;
|
||||
-moz-window-dragging: no-drag;
|
||||
}
|
||||
|
||||
.zen-library-media-item {
|
||||
border-radius: 16px;
|
||||
background: var(--zen-library-pill-bg);
|
||||
padding: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 1;
|
||||
transition:
|
||||
background 0.12s ease,
|
||||
scale 0.14s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--zen-library-pill-hover-bg);
|
||||
scale: 1.04;
|
||||
}
|
||||
|
||||
&:hover:active {
|
||||
scale: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-media-frame {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.zen-library-media-thumb {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.zen-library-media-file {
|
||||
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 {
|
||||
pointer-events: none;
|
||||
|
||||
& .zen-library-media-item {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zen-library-media-wait {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
|
||||
zen-library-media-section[needs-opt-in] {
|
||||
& .zen-library-search-top {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .zen-library-search-results {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-media-opt-in {
|
||||
position: absolute;
|
||||
inset-inline: 6px;
|
||||
top: 50%;
|
||||
translate: 0 -50%;
|
||||
padding: 22px 22px 18px;
|
||||
border-radius: 22px;
|
||||
background: light-dark(rgb(250, 250, 252), rgb(28, 29, 33));
|
||||
box-shadow:
|
||||
0 18px 50px rgba(0, 0, 0, 0.45),
|
||||
0 0 0 1px light-dark(rgba(0, 0, 0, 0.06), rgba(255, 255, 255, 0.06));
|
||||
text-align: center;
|
||||
|
||||
& h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
& p {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-media-opt-in-icon {
|
||||
--zen-library-sprite-size: 40px;
|
||||
|
||||
width: var(--zen-library-sprite-size);
|
||||
height: var(--zen-library-sprite-size);
|
||||
margin: 2px auto 12px;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.zen-library-media-opt-in-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.zen-library-media-opt-in-note {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 14px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
opacity: 0.6;
|
||||
text-align: start;
|
||||
|
||||
& img {
|
||||
flex: none;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.zen-library-media-play {
|
||||
position: absolute;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 9px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 100%;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
-moz-context-properties: fill;
|
||||
fill: white;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes zen-library-sprite-play {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
@@ -538,6 +692,17 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&[preview] {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
zen-library-downloads-section &[preview] {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
margin-inline: -4px;
|
||||
}
|
||||
margin-inline: 2px 1px;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
@@ -662,7 +827,7 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
border: 3px solid rgba(255, 255, 255, .6);
|
||||
background: rgba(237, 237, 237, 0.6);
|
||||
box-shadow: 0 0 6px 1px rgba(0, 0, 0, 0.1);
|
||||
@@ -701,7 +866,7 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
|
||||
left: 50%;
|
||||
width: 135%;
|
||||
height: 4px;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
background: light-dark(rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.5));
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
pointer-events: none;
|
||||
|
||||
@@ -43,15 +43,21 @@ export class ZenSpacesSwipe {
|
||||
);
|
||||
}
|
||||
|
||||
#readySwipeLibrary = null;
|
||||
#readySwipeOpenLibrary() {
|
||||
if (this.#readySwipeLibrary) {
|
||||
return this.#readySwipeLibrary;
|
||||
}
|
||||
|
||||
const spaces = gZenWorkspaces.getWorkspaces();
|
||||
const current = gZenWorkspaces.getActiveWorkspaceFromCache();
|
||||
const libraryEnabled = Services.prefs.getBoolPref("zen.library.enabled");
|
||||
const libraryOnRight = lazy.ZenLibrary.libraryOnRight;
|
||||
return (
|
||||
|
||||
this.#readySwipeLibrary =
|
||||
spaces.indexOf(current) === (libraryOnRight ? spaces.length - 1 : 0) &&
|
||||
libraryEnabled
|
||||
);
|
||||
libraryEnabled;
|
||||
return this.#readySwipeLibrary;
|
||||
}
|
||||
|
||||
attachWorkspaceSwipeGestures(element) {
|
||||
@@ -154,6 +160,19 @@ export class ZenSpacesSwipe {
|
||||
}
|
||||
}
|
||||
|
||||
#toggleSwipeGestureAttr(enable) {
|
||||
const elements = [
|
||||
"zen-workspace",
|
||||
"#tabbrowser-arrowscrollbox",
|
||||
".zen-browser-grain",
|
||||
];
|
||||
elements.forEach(el =>
|
||||
document
|
||||
.querySelectorAll(el)
|
||||
.forEach(node => node?.toggleAttribute("swipe-gesture", enable))
|
||||
);
|
||||
}
|
||||
|
||||
_handleSwipeStart(event) {
|
||||
const ws = gZenWorkspaces;
|
||||
|
||||
@@ -163,7 +182,7 @@ export class ZenSpacesSwipe {
|
||||
|
||||
gZenFolders.cancelPopupTimer();
|
||||
|
||||
document.documentElement.setAttribute("swipe-gesture", "true");
|
||||
this.#toggleSwipeGestureAttr(true);
|
||||
document.addEventListener("popupshown", this._popupOpenHandler, {
|
||||
once: true,
|
||||
});
|
||||
@@ -286,6 +305,7 @@ export class ZenSpacesSwipe {
|
||||
|
||||
if (this._swipeState.isSwipingLibrary) {
|
||||
lazy.ZenLibrary.stopSwipe(rawDirection * direction);
|
||||
this.#readySwipeLibrary = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -297,6 +317,7 @@ export class ZenSpacesSwipe {
|
||||
|
||||
if (this._swipeState.isSwipingLibrary) {
|
||||
lazy.ZenLibrary.stopSwipe(null);
|
||||
this.#readySwipeLibrary = null;
|
||||
}
|
||||
|
||||
// Reset swipe state
|
||||
@@ -309,7 +330,7 @@ export class ZenSpacesSwipe {
|
||||
};
|
||||
|
||||
Services.prefs.setBoolPref("zen.swipe.is-fast-swipe", false);
|
||||
document.documentElement.removeAttribute("swipe-gesture");
|
||||
this.#toggleSwipeGestureAttr(false);
|
||||
gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
|
||||
[lazy.browserBackgroundElement, lazy.toolbarBackgroundElement].forEach(
|
||||
element => {
|
||||
|
||||
@@ -43,20 +43,30 @@
|
||||
fill: currentColor;
|
||||
scroll-margin: 0 20px;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
height: 32px;
|
||||
max-width: 32px;
|
||||
}
|
||||
|
||||
& .zen-workspace-icon {
|
||||
pointer-events: none;
|
||||
line-height: 0;
|
||||
position: absolute;
|
||||
font-size: 12px;
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:is(img) {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
@media (-moz-platform: macos) {
|
||||
font-size: 15px;
|
||||
|
||||
&:is(img) {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&[no-icon='true'] {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@@ -356,7 +366,7 @@ zen-workspace {
|
||||
overscroll-behavior: auto;
|
||||
}
|
||||
|
||||
:root[swipe-gesture] &::part(scrollbox) {
|
||||
&[swipe-gesture]::part(scrollbox) {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@@ -404,7 +414,7 @@ zen-workspace {
|
||||
}
|
||||
}
|
||||
|
||||
:root:not(:is([animating-background], [swipe-gesture])) #navigator-toolbox:not([movingtab]) &:not([active]) {
|
||||
&:not([swipe-gesture]) :root:not(:is([animating-background])) #navigator-toolbox:not([movingtab]) &:not([active]) {
|
||||
-moz-subtree-hidden-only-visually: 1;
|
||||
content-visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -890,7 +890,7 @@ zen-library {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root[zen-library-open="true"] #zen-sidebar-splitter {
|
||||
#zen-sidebar-splitter[zen-library-open="true"] {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user