From 22604abe11be241dda943fe71f69ea575261917c Mon Sep 17 00:00:00 2001
From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com>
Date: Wed, 23 Sep 2026 23:28:40 +0200
Subject: [PATCH] gh-15533: Toggle 'open when done' for library downloads
(gh-15534)
---
locales/en-US/browser/browser/zen-library.ftl | 1 +
src/zen/library/ZenLibraryWidget.sys.mjs | 51 +++++++++++++++---
.../sections/ZenLibraryDownloadsSection.mjs | 53 +++++++++++++++----
src/zen/library/zen-library.css | 5 ++
4 files changed, 95 insertions(+), 15 deletions(-)
diff --git a/locales/en-US/browser/browser/zen-library.ftl b/locales/en-US/browser/browser/zen-library.ftl
index 906055768..8dca1d8ce 100644
--- a/locales/en-US/browser/browser/zen-library.ftl
+++ b/locales/en-US/browser/browser/zen-library.ftl
@@ -78,6 +78,7 @@ library-downloads-opening-in = { PLATFORM() ->
[windows] Opening in File Explorer…
*[other] Opening in file manager…
}
+library-downloads-open-when-done = Opens when finished
library-downloads-cancel-button =
.title = Cancel download
library-downloads-filter-title = Filter Downloads…
diff --git a/src/zen/library/ZenLibraryWidget.sys.mjs b/src/zen/library/ZenLibraryWidget.sys.mjs
index 90a08ba53..d5406c7c0 100644
--- a/src/zen/library/ZenLibraryWidget.sys.mjs
+++ b/src/zen/library/ZenLibraryWidget.sys.mjs
@@ -235,7 +235,31 @@ class ZenLibraryDownloadStack {
.finally(() => download.target.refresh());
}
+ /**
+ * @param {object} download
+ * @returns {boolean} Whether it will be opened as soon as it is here
+ */
+ #opensWhenDone(download) {
+ return !download.stopped && !!download.launchWhenSucceeded;
+ }
+
+ /**
+ * Clicking a download that is still coming in asks for it to be opened as
+ * soon as it is here, the way the downloads panel does.
+ *
+ * @param {object} download
+ */
+ #toggleOpenWhenDone(download) {
+ download.launchWhenSucceeded = !download.launchWhenSucceeded;
+ download._launchedFromPanel = download.launchWhenSucceeded;
+ this.#updateList();
+ }
+
#openDownload(download) {
+ if (!download.stopped) {
+ this.#toggleOpenWhenDone(download);
+ return;
+ }
if (download.succeeded) {
lazy.DownloadsCommon.openDownload(download).catch(console.error);
} else if (download.source?.url) {
@@ -274,14 +298,29 @@ class ZenLibraryDownloadStack {
);
entry.querySelector(".zen-library-download-list-title").textContent =
this.#fileName(download);
- entry.querySelector(".zen-library-download-list-subtitle").textContent =
- this.#statusText(download);
+ const subtitle = entry.querySelector(
+ ".zen-library-download-list-subtitle"
+ );
+ if (this.#opensWhenDone(download)) {
+ this.#window.document.l10n.setAttributes(
+ subtitle,
+ "library-downloads-open-when-done"
+ );
+ } else {
+ subtitle.removeAttribute("data-l10n-id");
+ subtitle.textContent = this.#statusText(download);
+ }
entry.toggleAttribute("downloading", !download.stopped);
+ entry.toggleAttribute("open-when-done", this.#opensWhenDone(download));
});
- this.#tabs?.style.setProperty(
- "--zen-library-stack-height",
- `${this.#list.getBoundingClientRect().height}px`
- );
+ this.#window
+ .promiseDocumentFlushed(() => this.#list.getBoundingClientRect().height)
+ .then(height => {
+ this.#tabs?.style.setProperty(
+ "--zen-library-stack-height",
+ `${height}px`
+ );
+ });
}
#updateBadge(badge, download) {
diff --git a/src/zen/library/sections/ZenLibraryDownloadsSection.mjs b/src/zen/library/sections/ZenLibraryDownloadsSection.mjs
index 65cb1a1fe..18bd55084 100644
--- a/src/zen/library/sections/ZenLibraryDownloadsSection.mjs
+++ b/src/zen/library/sections/ZenLibraryDownloadsSection.mjs
@@ -326,7 +326,34 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
lazy.DownloadsCommon.showDownloadedFile(this.#file(download));
}
+ /**
+ * Whether a download that is still coming in will be opened the moment it
+ * finishes.
+ *
+ * @param {object} download
+ * @returns {boolean}
+ */
+ #opensWhenDone(download) {
+ return !download.stopped && !!download.launchWhenSucceeded;
+ }
+
+ /**
+ * Clicking a download that is still coming in asks for it to be opened as
+ * soon as it is here, the way the downloads panel does.
+ *
+ * @param {object} download
+ */
+ #toggleOpenWhenDone(download) {
+ download.launchWhenSucceeded = !download.launchWhenSucceeded;
+ download._launchedFromPanel = download.launchWhenSucceeded;
+ this.requestUpdate();
+ }
+
#onRowClick(download) {
+ if (!download.stopped) {
+ this.#toggleOpenWhenDone(download);
+ return;
+ }
if (!this.#hasFile(download)) {
return;
}
@@ -516,15 +543,22 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
// Rendering
#renderSubtitle(download) {
- const statusNode =
- download === this.#openingDownload
- ? html``
- : html`${this.#statusText(download)}`;
+ let statusNode;
+ if (download === this.#openingDownload) {
+ statusNode = html``;
+ } else if (this.#opensWhenDone(download)) {
+ statusNode = html``;
+ } else {
+ statusNode = html`${this.#statusText(download)}`;
+ }
return html`
${statusNode}
@@ -584,6 +618,7 @@ export class ZenLibraryDownloadsSection extends ZenLibrarySearchSection {
?pending=${pending}
?indeterminate=${pending && !download.hasProgress}
?paused=${pending && download.stopped}
+ ?open-when-done=${this.#opensWhenDone(download)}
?opening=${download === this.#openingDownload}
@click=${() => this.#onRowClick(download)}
@contextmenu=${event => {
diff --git a/src/zen/library/zen-library.css b/src/zen/library/zen-library.css
index b5c10d76c..6189d26c7 100644
--- a/src/zen/library/zen-library.css
+++ b/src/zen/library/zen-library.css
@@ -882,6 +882,11 @@ zen-library:not([open]) :is(.zen-library-search-header, .zen-library-filter-head
display: none;
}
+.zen-library-row[pending][open-when-done]:not(:hover)
+ .zen-library-download-status {
+ display: inline;
+}
+
.zen-library-row[pending]:not(:hover) .zen-library-row-subtitle::before {
content: "";
display: block;