--- a/browser/components/downloads/DownloadsSubview.jsm
+++ b/browser/components/downloads/DownloadsSubview.jsm
@@ -20,16 +20,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource:///modules/DownloadsCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsViewUI",
"resource:///modules/DownloadsViewUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
let gPanelViewInstances = new WeakMap();
const kEvents = ["ViewShowing", "ViewHiding", "click", "command"];
+const kRefreshBatchSize = 10;
+const kMaxWaitForIdleMs = 200;
XPCOMUtils.defineLazyGetter(this, "kButtonLabels", () => {
return {
show: DownloadsCommon.strings[AppConstants.platform == "macosx" ? "showMacLabel" : "showLabel"],
open: DownloadsCommon.strings.openFileLabel,
retry: DownloadsCommon.strings.retryLabel,
};
});
@@ -73,16 +75,17 @@ class DownloadsSubview extends Downloads
this._downloadsData.addView(this);
}
destructor(event) {
this.panelview.removeEventListener("click", DownloadsSubview.onClick);
this.panelview.removeEventListener("ViewHiding", DownloadsSubview.onViewHiding);
this._downloadsData.removeView(this);
gPanelViewInstances.delete(this);
+ this.destroyed = true;
}
/**
* DataView handler; invoked when a batch of downloads is being passed in -
* usually when this instance is added as a view in the constructor.
*/
onDownloadBatchStarting() {
this.batchFragment = this.document.createDocumentFragment();
@@ -99,18 +102,20 @@ class DownloadsSubview extends Downloads
let waitForMs = 200;
if (this.batchFragment.childElementCount) {
// Prepend the batch fragment.
this.container.insertBefore(this.batchFragment, this.container.firstChild || null);
waitForMs = 0;
}
// Wait a wee bit to dispatch the event, because another batch may start
// right away.
- this._batchTimeout = window.setTimeout(() =>
- this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded")), waitForMs);
+ this._batchTimeout = window.setTimeout(() => {
+ this._updateStatsFromDisk();
+ this.panelview.dispatchEvent(new window.CustomEvent("DownloadsLoaded"));
+ }, waitForMs);
this.batchFragment = null;
}
/**
* DataView handler; invoked when a new download is added to the list.
*
* @param {Download} download
* @param {DOMNode} [options.insertBefore]
@@ -145,16 +150,56 @@ class DownloadsSubview extends Downloads
* DataView handler; invoked when a download is removed.
*
* @param {Download} download
*/
onDownloadRemoved(download) {
this._viewItemsForDownloads.get(download).element.remove();
}
+ /**
+ * Schedule a refresh of the downloads that were added, which is mainly about
+ * checking whether the target file still exists.
+ * We're doing this during idle time and in chunks.
+ */
+ async _updateStatsFromDisk() {
+ if (this._updatingStats)
+ return;
+
+ this._updatingStats = true;
+
+ try {
+ let idleOptions = { timeout: kMaxWaitForIdleMs };
+ // Start with getting an idle moment to (maybe) refresh the list of downloads.
+ await new Promise(resolve => this.window.requestIdleCallback(resolve), idleOptions);
+ // In the meantime, this instance could have been destroyed, so take note.
+ if (this.destroyed)
+ return;
+
+ let count = 0;
+ for (let button of this.container.childNodes) {
+ if (this.destroyed)
+ return;
+ if (!button._shell)
+ continue;
+
+ await button._shell.refresh();
+
+ // Make sure to request a new idle moment every `kRefreshBatchSize` buttons.
+ if (++count % kRefreshBatchSize === 0) {
+ await new Promise(resolve => this.window.requestIdleCallback(resolve, idleOptions));
+ }
+ }
+ } catch (ex) {
+ Cu.reportError(ex);
+ } finally {
+ this._updatingStats = false;
+ }
+ }
+
// ----- Static methods. -----
/**
* Perform all tasks necessary to be able to show a Downloads Subview.
*
* @param {DOMWindow} window Global window object.
* @return {Promise} Will resolve when all tasks are done.
*/
@@ -339,31 +384,43 @@ DownloadsSubview.Button = class extends
this.element.classList.add("subviewbutton", "subviewbutton-iconic", "download",
"download-state");
}
get browserWindow() {
return this.element.ownerGlobal;
}
+ async refresh() {
+ if (this._targetFileChecked)
+ return;
+
+ try {
+ await this.download.refresh();
+ } catch (ex) {
+ Cu.reportError(ex);
+ } finally {
+ this._targetFileChecked = true;
+ }
+ }
+
/**
* Handle state changes of a download.
*/
onStateChanged() {
// Since the state changed, we may need to check the target file again.
this._targetFileChecked = false;
this._updateState();
}
/**
* Handler method; invoked when any state attribute of a download changed.
*/
onChanged() {
- // TODO: implement "file moved or missing" check - bug 1395615.
let newState = DownloadsCommon.stateOfDownload(this.download);
if (this._downloadState !== newState) {
this._downloadState = newState;
this.onStateChanged();
} else {
this._updateState();
}
@@ -380,18 +437,25 @@ DownloadsSubview.Button = class extends
_updateState() {
super._updateState();
this.element.setAttribute("label", this.element.getAttribute("displayName"));
this.element.setAttribute("tooltiptext", this.element.getAttribute("fullStatus"));
if (this.isCommandEnabled("downloadsCmd_show")) {
this.element.setAttribute("openLabel", kButtonLabels.open);
this.element.setAttribute("showLabel", kButtonLabels.show);
+ this.element.removeAttribute("retryLabel");
} else if (this.isCommandEnabled("downloadsCmd_retry")) {
this.element.setAttribute("retryLabel", kButtonLabels.retry);
+ this.element.removeAttribute("openLabel");
+ this.element.removeAttribute("showLabel");
+ } else {
+ this.element.removeAttribute("openLabel");
+ this.element.removeAttribute("retryLabel");
+ this.element.removeAttribute("showLabel");
}
this._updateVisibility();
}
_updateVisibility() {
let state = this.element.getAttribute("state");
// This view only show completed and failed downloads.