/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; var { interfaces: Ci, utils: Cu, classes: Cc } = Components; const nsIDM = Ci.nsIDownloadManager; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); var gStr = {}; var DownloadItem = function(aID, aDownload) { this.id = aID; this._download = aDownload; this.file = aDownload.target.path; this.target = OS.Path.basename(aDownload.target.path); this.uri = aDownload.source.url; this.endTime = new Date(); this.updateState(); this.updateData(); this.createItem(); }; DownloadItem.prototype = { /** * The XUL element corresponding to the associated richlistbox item. */ element: null, /** * The inner XUL element for the progress bar, or null if not available. */ _progressElement: null, _lastEstimatedSecondsLeft: Infinity, updateState: function() { // Collapse state using the correct priority. if (this._download.succeeded) { this.state = nsIDM.DOWNLOAD_FINISHED; } else if (this._download.error && this._download.error.becauseBlockedByParentalControls) { this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL; } else if (this._download.error && this._download.error.becauseBlockedByReputationCheck) { this.state = nsIDM.DOWNLOAD_DIRTY; } else if (this._download.error) { this.state = nsIDM.DOWNLOAD_FAILED; } else if (this._download.canceled && this._download.hasPartialData) { this.state = nsIDM.DOWNLOAD_PAUSED; } else if (this._download.canceled) { this.state = nsIDM.DOWNLOAD_CANCELED; } else if (this._download.stopped) { this.state = nsIDM.DOWNLOAD_NOTSTARTED; } else { this.state = nsIDM.DOWNLOAD_DOWNLOADING; } }, updateData: function() { let wasInProgress = this.inProgress; this.updateState(); if (wasInProgress && !this.inProgress) { this.endTime = new Date(); } this.referrer = this._download.source.referrer; this.startTime = this._download.startTime; this.currBytes = this._download.currentBytes; this.resumable = this._download.hasPartialData; this.speed = this._download.speed; if (this._download.succeeded) { // If the download succeeded, show the final size if available, otherwise // use the last known number of bytes transferred. The final size on disk // will be available when bug 941063 is resolved. this.maxBytes = this._download.hasProgress ? this._download.totalBytes : this._download.currentBytes; this.percentComplete = 100; } else if (this._download.hasProgress) { // If the final size and progress are known, use them. this.maxBytes = this._download.totalBytes; this.percentComplete = this._download.progress; } else { // The download final size and progress percentage is unknown. this.maxBytes = -1; this.percentComplete = -1; } }, /** * Indicates whether the download is proceeding normally, and not finished * yet. This includes paused downloads. When this property is true, the * "progress" property represents the current progress of the download. */ get inProgress() { return [ nsIDM.DOWNLOAD_NOTSTARTED, nsIDM.DOWNLOAD_QUEUED, nsIDM.DOWNLOAD_DOWNLOADING, nsIDM.DOWNLOAD_PAUSED, nsIDM.DOWNLOAD_SCANNING, ].indexOf(this.state) != -1; }, /** * This is true during the initial phases of a download, before the actual * download of data bytes starts. */ get starting() { return this.state == nsIDM.DOWNLOAD_NOTSTARTED || this.state == nsIDM.DOWNLOAD_QUEUED; }, /** * Indicates whether the download is paused. */ get paused() { return this.state == nsIDM.DOWNLOAD_PAUSED; }, /** * Indicates whether the download is in a final state, either because it * completed successfully or because it was blocked. */ get done() { return [ nsIDM.DOWNLOAD_FINISHED, nsIDM.DOWNLOAD_BLOCKED_PARENTAL, nsIDM.DOWNLOAD_BLOCKED_POLICY, nsIDM.DOWNLOAD_DIRTY, ].indexOf(this.state) != -1; }, /** * Indicates whether the download can be removed. */ get removable() { return [ nsIDM.DOWNLOAD_FINISHED, nsIDM.DOWNLOAD_CANCELED, nsIDM.DOWNLOAD_BLOCKED_PARENTAL, nsIDM.DOWNLOAD_BLOCKED_POLICY, nsIDM.DOWNLOAD_DIRTY, nsIDM.DOWNLOAD_FAILED, ].indexOf(this.state) != -1; }, /** * Indicates whether the download is finished and can be opened. */ get openable() { return this.state == nsIDM.DOWNLOAD_FINISHED; }, /** * Indicates whether the download stopped because of an error, and can be * resumed manually. */ get canRetry() { return this.state == nsIDM.DOWNLOAD_CANCELED || this.state == nsIDM.DOWNLOAD_FAILED; }, /** * Returns the nsILocalFile for the download target. * * @throws if the native path is not valid. This can happen if the same * profile is used on different platforms, for example if a native * Windows path is stored and then the item is accessed on a Mac. */ get localFile() { return this._getFile(this.file); }, /** * Returns the nsILocalFile for the partially downloaded target. * * @throws if the native path is not valid. This can happen if the same * profile is used on different platforms, for example if a native * Windows path is stored and then the item is accessed on a Mac. */ get partFile() { return this._getFile(this.file + ".part"); }, /** * Return a nsILocalFile for aFilename. aFilename might be a file URL or * a native path. * * @param aFilename the filename of the file to retrieve. * @return an nsILocalFile for the file. * @throws if the native path is not valid. This can happen if the same * profile is used on different platforms, for example if a native * Windows path is stored and then the item is accessed on a Mac. * @note This function makes no guarantees about the file's existence - * callers should check that the returned file exists. */ _getFile: function(aFilename) { // The download database may contain targets stored as file URLs or native // paths. This can still be true for previously stored items, even if new // items are stored using their file URL. See also bug 239948 comment 12. if (aFilename.startsWith("file:")) { // Assume the file URL we obtained from the downloads database or from the // "spec" property of the target has the UTF-8 charset. let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL); return fileUrl.file; } // The downloads database contains a native path. Try to create a local // file, though this may throw an exception if the path is invalid. let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); file.initWithPath(aFilename); return file; }, /** * Show a downloaded file in the system file manager. */ showLocalFile: function() { let file = this.localFile; try { // Show the directory containing the file and select the file. file.reveal(); } catch (ex) { // If reveal fails for some reason (e.g., it's not implemented on unix // or the file doesn't exist), try using the parent if we have it. let parent = file.parent; if (!parent) { return; } try { // Open the parent directory to show where the file should be. parent.launch(); } catch (ex) { // If launch also fails (probably because it's not implemented), let // the OS handler try to open the parent. Cc["@mozilla.org/uriloader/external-protocol-service;1"]. getService(Ci.nsIExternalProtocolService). loadUrl(NetUtil.newURI(parent)); } } }, /** * Open the target file for this download. */ openLocalFile: function() { return this._download.launch(); }, /** * Pause the download, if it is active. */ pause: function() { return this._download.cancel(); }, /** * Resume the download, if it is paused. */ resume: function() { return this._download.start(); }, /** * Attempt to retry the download. */ retry: function() { return this._download.start(); }, /** * Cancel the download. */ cancel: function() { this._download.cancel(); return this._download.removePartialData(); }, /** * Remove the download. */ remove: function() { return Downloads.getList(Downloads.ALL) .then(list => list.remove(this._download)) .then(() => this._download.finalize(true)); }, /** * Create a download richlistitem with the provided attributes. */ createItem: function() { this.element = document.createElement("richlistitem"); this.element.setAttribute("id", this.id); this.element.setAttribute("target", this.target); this.element.setAttribute("state", this.state); this.element.setAttribute("progress", this.percentComplete); this.element.setAttribute("type", "download"); this.element.setAttribute("image", "moz-icon://" + this.file + "?size=32"); }, /** * Update the status for a download item depending on its state. */ updateStatusText: function() { let status = ""; let statusTip = ""; switch (this.state) { case nsIDM.DOWNLOAD_PAUSED: let transfer = DownloadUtils.getTransferTotal(this.currBytes, this.maxBytes); status = gStr.paused.replace("#1", transfer); break; case nsIDM.DOWNLOAD_DOWNLOADING: let newLast; [status, newLast] = DownloadUtils.getDownloadStatus(this.currBytes, this.maxBytes, this.speed, this._lastEstimatedSecondsLeft); this._lastEstimatedSecondsLeft = newLast; break; case nsIDM.DOWNLOAD_FINISHED: case nsIDM.DOWNLOAD_FAILED: case nsIDM.DOWNLOAD_CANCELED: case nsIDM.DOWNLOAD_BLOCKED_PARENTAL: case nsIDM.DOWNLOAD_BLOCKED_POLICY: case nsIDM.DOWNLOAD_DIRTY: let stateSize = {}; stateSize[nsIDM.DOWNLOAD_FINISHED] = () => { // Display the file size, but show "Unknown" for negative sizes. let sizeText = gStr.doneSizeUnknown; if (this.maxBytes >= 0) { let [size, unit] = DownloadUtils.convertByteUnits(this.maxBytes); sizeText = gStr.doneSize.replace("#1", size); sizeText = sizeText.replace("#2", unit); } return sizeText; }; stateSize[nsIDM.DOWNLOAD_FAILED] = () => gStr.stateFailed; stateSize[nsIDM.DOWNLOAD_CANCELED] = () => gStr.stateCanceled; stateSize[nsIDM.DOWNLOAD_BLOCKED_PARENTAL] = () => gStr.stateBlocked; stateSize[nsIDM.DOWNLOAD_BLOCKED_POLICY] = () => gStr.stateBlockedPolicy; stateSize[nsIDM.DOWNLOAD_DIRTY] = () => gStr.stateDirty; // Insert 1 is the download size or download state. status = gStr.doneStatus.replace("#1", stateSize[this.state]()); let [displayHost, fullHost] = DownloadUtils.getURIHost(this.referrer || this.uri); // Insert 2 is the eTLD + 1 or other variations of the host. status = status.replace("#2", displayHost); // Set the tooltip to be the full host. statusTip = fullHost; break; } this.element.setAttribute("status", status); this.element.setAttribute("statusTip", statusTip || status); }, updateView: function() { this.updateData(); // Update this download's progressmeter. if (this.starting) { // Before the download starts, the progress meter has its initial value. this.element.setAttribute("progressmode", "normal"); this.element.setAttribute("progress", "0"); } else if (this.state == Ci.nsIDownloadManager.DOWNLOAD_SCANNING || this.percentComplete == -1) { // We might not know the progress of a running download, and we don't know // the remaining time during the malware scanning phase. this.element.setAttribute("progressmode", "undetermined"); } else { // This is a running download of which we know the progress. this.element.setAttribute("progressmode", "normal"); this.element.setAttribute("progress", this.percentComplete); } // Find the progress element as soon as the download binding is accessible. if (!this._progressElement) { this._progressElement = document.getAnonymousElementByAttribute(this.element, "anonid", "progressmeter"); } // Dispatch the ValueChange event for accessibility, if possible. if (this._progressElement) { let event = document.createEvent("Events"); event.initEvent("ValueChange", true, true); this._progressElement.dispatchEvent(event); } // Update the rest of the UI (bytes transferred, bytes total, download rate, // time remaining). // Update the time that gets shown for completed download items // Don't bother updating it for things that aren't finished if (!this.inProgress) { let [dateCompact, dateComplete] = DownloadUtils.getReadableDates(this.endTime); this.element.setAttribute("dateTime", dateCompact); this.element.setAttribute("dateTimeTip", dateComplete); } this.element.setAttribute("state", this.state); this.updateStatusText(); // Update the disabled state of the buttons of a download let buttons = this.element.buttons; for (let i = 0; i < buttons.length; ++i) { let cmd = buttons[i].getAttribute("cmd"); let enabled = this.isCommandEnabled(cmd); buttons[i].disabled = !enabled; if ("cmd_pause" == cmd && !enabled) { // We need to add the tooltip indicating that the download cannot be // paused now. buttons[i].setAttribute("tooltiptext", gStr.cannotPause); } } if (this.done) { // getTypeFromFile fails if it can't find a type for this file. try { // Refresh the icon, so that executable icons are shown. let mimeService = Cc["@mozilla.org/mime;1"]. getService(Ci.nsIMIMEService); let contentType = mimeService.getTypeFromFile(this.localFile); let oldImage = this.element.getAttribute("image"); // Tacking on contentType bypasses cache this.element.setAttribute("image", oldImage + "&contentType=" + contentType); } catch (e) {} } }, /** * Check if the download matches the provided search term based on the texts * shown to the user. All search terms are checked to see if each matches any * of the displayed texts. * * @return Boolean true if it matches the search; false otherwise */ matchesSearch: function(aTerms, aAttributes) { return aTerms.some(term => aAttributes.some(attr => this.element.getAttribute(attr).includes(term))); }, isCommandEnabled: function(aCommand) { switch (aCommand) { case "cmd_cancel": return this.inProgress; case "cmd_open": return this.openable && this.localFile.exists(); case "cmd_show": return this.localFile.exists(); case "cmd_pause": return this.inProgress && !this.paused && this.resumable; case "cmd_pauseResume": return (this.inProgress || this.paused) && this.resumable; case "cmd_resume": return this.paused && this.resumable; case "cmd_openReferrer": return !!this.referrer; case "cmd_removeFromList": return this.removable; case "cmd_retry": return this.canRetry; case "cmd_copyLocation": return true; } return false; }, doCommand: function(aCommand) { if (!this.isCommandEnabled(aCommand)) { return; } switch (aCommand) { case "cmd_cancel": this.cancel().catch(Cu.reportError); break; case "cmd_open": this.openLocalFile().catch(Cu.reportError); break; case "cmd_show": this.showLocalFile(); break; case "cmd_pause": this.pause().catch(Cu.reportError); break; case "cmd_pauseResume": if (this.paused) { this.resume().catch(Cu.reportError); } else { this.pause().catch(Cu.reportError); } break; case "cmd_resume": this.resume().catch(Cu.reportError); break; case "cmd_openReferrer": let uri = Services.io.newURI(this.referrer || this.uri, null, null); // Direct the URL to the default browser. Cc["@mozilla.org/uriloader/external-protocol-service;1"]. getService(Ci.nsIExternalProtocolService). getProtocolHandlerInfo(uri.scheme). launchWithURI(uri); break; case "cmd_removeFromList": this.remove().catch(Cu.reportError); break; case "cmd_retry": this.retry().catch(Cu.reportError); break; case "cmd_copyLocation": let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. getService(Ci.nsIClipboardHelper); clipboard.copyString(this.uri); break; } }, }; var gDownloadList = { downloadItemsMap: new Map(), idToDownloadItemMap: new Map(), _autoIncrementID: 0, downloadView: null, searchBox: null, searchTerms: [], searchAttributes: [ "target", "status", "dateTime", ], contextMenus: [ // DOWNLOAD_DOWNLOADING [ "menuitem_pause", "menuitem_cancel", "menuseparator", "menuitem_show", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", ], // DOWNLOAD_FINISHED [ "menuitem_open", "menuitem_show", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", "menuseparator", "menuitem_removeFromList" ], // DOWNLOAD_FAILED [ "menuitem_retry", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", "menuseparator", "menuitem_removeFromList", ], // DOWNLOAD_CANCELED [ "menuitem_retry", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", "menuseparator", "menuitem_removeFromList", ], // DOWNLOAD_PAUSED [ "menuitem_resume", "menuitem_cancel", "menuseparator", "menuitem_show", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", ], // DOWNLOAD_QUEUED [ "menuitem_cancel", "menuseparator", "menuitem_show", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", ], // DOWNLOAD_BLOCKED_PARENTAL [ "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", "menuseparator", "menuitem_removeFromList", ], // DOWNLOAD_SCANNING [ "menuitem_show", "menuseparator", "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", ], // DOWNLOAD_DIRTY [ "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", "menuseparator", "menuitem_removeFromList", ], // DOWNLOAD_BLOCKED_POLICY [ "menuitem_openReferrer", "menuitem_copyLocation", "menuseparator", "menuitem_selectAll", "menuseparator", "menuitem_removeFromList", ] ], init: function() { this.downloadView = document.getElementById("downloadView"); this.searchBox = document.getElementById("searchbox"); this.buildList(true); this.downloadView.focus(); // Clear the search box and move focus to the list on escape from the box. this.searchBox.addEventListener("keypress", (e) => { this.searchBoxKeyPressHandler(e); }, false); Downloads.getList(Downloads.ALL) .then(list => list.addView(this)) .catch(Cu.reportError); }, buildList: function(aForceBuild) { // Stringify the previous search. let prevSearch = this.searchTerms.join(" "); // Array of space-separated lower-case search terms. this.searchTerms = this.searchBox.value.trim().toLowerCase().split(/\s+/); // Unless forced, don't rebuild the download list if the search didn't change. if (!aForceBuild && this.searchTerms.join(" ") == prevSearch) { return; } // Clear the list before adding items by replacing with a shallow copy. let empty = this.downloadView.cloneNode(false); this.downloadView.parentNode.replaceChild(empty, this.downloadView); this.downloadView = empty; for (let downloadItem of this.idToDownloadItemMap.values()) { if (downloadItem.inProgress || downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) { this.downloadView.appendChild(downloadItem.element); // Because of the joys of XBL, we can't update the buttons until the // download object is in the document. downloadItem.updateView(); } } this.updateClearListButton(); }, /** * Remove the finished downloads from the shown download list. */ clearList: Task.async(function*() { let searchTerms = this.searchTerms; let list = yield Downloads.getList(Downloads.ALL); yield list.removeFinished((aDownload) => { let downloadItem = this.downloadItemsMap.get(aDownload); if (!downloadItem) { Cu.reportError("Download doesn't exist."); return; } return downloadItem.matchesSearch(searchTerms, this.searchAttributes); }); // Clear the input as if the user did it and move focus to the list. this.searchBox.value = ""; this.searchBox.doCommand(); this.downloadView.focus(); }), /** * Update the disabled state of the clear list button based on whether or not * there are items in the list that can potentially be removed. */ updateClearListButton: function() { let button = document.getElementById("clearListButton"); // The button is enabled if we have items in the list that we can clean up. for (let downloadItem of this.idToDownloadItemMap.values()) { if (!downloadItem.inProgress && downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) { button.disabled = false; return; } } button.disabled = true; }, setSearchboxFocus: function() { this.searchBox.focus(); this.searchBox.select(); }, searchBoxKeyPressHandler: function(aEvent) { if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { // Move focus to the list instead of closing the window. this.downloadView.focus(); aEvent.preventDefault(); } }, buildContextMenu: function(aEvent) { if (aEvent.target.id != "downloadContextMenu") { return false; } let popup = document.getElementById("downloadContextMenu"); while (popup.hasChildNodes()) { popup.removeChild(popup.firstChild); } if (this.downloadView.selectedItem) { let downloadItem = this._getSelectedDownloadItem(); let idx = downloadItem.state; if (idx < 0) { idx = 0; } let menus = this.contextMenus[idx]; for (let i = 0; i < menus.length; ++i) { let menuitem = document.getElementById(menus[i]).cloneNode(true); let cmd = menuitem.getAttribute("cmd"); if (cmd) { menuitem.disabled = !downloadItem.isCommandEnabled(cmd); } popup.appendChild(menuitem); } return true; } return false; }, /** * Perform the default action for the currently selected download item. */ doDefaultForSelected: function() { // Make sure we have something selected. let download = this._getSelectedDownloadItem(); if (!download) { return; } // Get the default action (first item in the menu). let menuitem = document.getElementById(this.contextMenus[download.state][0]); // Try to do the action if the command is enabled. download.doCommand(menuitem.getAttribute("cmd")); }, onDownloadDblClick: function(aEvent) { // Only do the default action for double primary clicks. if (aEvent.button == 0 && aEvent.target.selected) { this.doDefaultForSelected(); } }, /** * Helper function to do commands. * * @param aCmd * The command to be performed. * @param aItem * The richlistitem that represents the download that will have the * command performed on it. If this is null, the command is performed on * all downloads. If the item passed in is not a richlistitem that * represents a download, it will walk up the parent nodes until it finds * a DOM node that is. */ performCommand: function(aCmd, aItem) { if (!aItem) { // Convert the nodelist into an array to keep a copy of the download items. let items = Array.from(this.downloadView.selectedItems); // Do the command for each download item. for (let item of items) { this.performCommand(aCmd, item); } return; } let elm = aItem; while (elm.nodeName != "richlistitem" || elm.getAttribute("type") != "download") { elm = elm.parentNode; } let downloadItem = this._getDownloadItemForElement(elm); downloadItem.doCommand(aCmd); }, onDragStart: function(aEvent) { let downloadItem = this._getSelectedDownloadItem(); if (!downloadItem) { return; } let dl = this.downloadView.selectedItem; let f = downloadItem.localFile; if (!f.exists()) { return; } let dt = aEvent.dataTransfer; dt.mozSetDataAt("application/x-moz-file", f, 0); let url = Services.io.newFileURI(f).spec; dt.setData("text/uri-list", url); dt.setData("text/plain", url); dt.effectAllowed = "copyMove"; dt.addElement(dl); }, onDragOver: function(aEvent) { let types = aEvent.dataTransfer.types; if (types.contains("text/uri-list") || types.contains("text/x-moz-url") || types.contains("text/plain")) { aEvent.preventDefault(); } aEvent.stopPropagation(); }, onDrop: function(aEvent) { let dt = aEvent.dataTransfer; // If dragged item is from our source, do not try to // redownload already downloaded file. if (dt.mozGetDataAt("application/x-moz-file", 0)) { return; } let name; let url = dt.getData("URL"); if (!url) { url = dt.getData("text/x-moz-url") || dt.getData("text/plain"); [url, name] = url.split("\n"); } if (url) { let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document; saveURL(url, name ? name : url, null, true, true, null, sourceDoc); } }, pasteHandler: function() { let trans = Cc["@mozilla.org/widget/transferable;1"]. createInstance(Ci.nsITransferable); trans.init(null); let flavors = ["text/x-moz-url", "text/unicode"]; flavors.forEach(trans.addDataFlavor); Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); // Getting the data or creating the nsIURI might fail try { let data = {}; trans.getAnyTransferData({}, data, {}); let [url, name] = data.value.QueryInterface(Ci.nsISupportsString).data.split("\n"); if (!url) { return; } let uri = Services.io.newURI(url, null, null); saveURL(uri.spec, name || uri.spec, null, true, true, null, document); } catch (ex) {} }, selectAll: function() { this.downloadView.selectAll(); }, onUpdateProgress: function() { let numActive = 0; let totalSize = 0; let totalTransferred = 0; for (let downloadItem of this.idToDownloadItemMap.values()) { if (!downloadItem.inProgress) { continue; } numActive++; // Only add to total values if we actually know the download size. if (downloadItem.maxBytes > 0 && downloadItem.state != nsIDM.DOWNLOAD_CANCELED && downloadItem.state != nsIDM.DOWNLOAD_FAILED) { totalSize += downloadItem.maxBytes; totalTransferred += downloadItem.currBytes; } } // Use the default title and reset "last" values if there are no downloads. if (numActive == 0) { document.title = document.documentElement.getAttribute("statictitle"); return; } // Establish the mean transfer speed and amount downloaded. let mean = totalTransferred; // Calculate the percent transferred, unless we don't have a total file size. let title = gStr.downloadsTitlePercent; if (totalSize == 0) { title = gStr.downloadsTitleFiles; } else { mean = Math.floor((totalTransferred / totalSize) * 100); } // Get the correct plural form and insert number of downloads and percent. title = PluralForm.get(numActive, title); title = title.replace("#1", numActive); title = title.replace("#2", mean); // Update title of window. document.title = title; }, onDownloadAdded: function(aDownload) { let newID = this._autoIncrementID++; let downloadItem = new DownloadItem(newID, aDownload); this.downloadItemsMap.set(aDownload, downloadItem); this.idToDownloadItemMap.set(newID, downloadItem); if (downloadItem.inProgress || downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) { this.downloadView.appendChild(downloadItem.element); // Because of the joys of XBL, we can't update the buttons until the // download object is in the document. downloadItem.updateView(); // We might have added an item to an empty list, so update button. this.updateClearListButton(); } }, onDownloadChanged: function(aDownload) { let downloadItem = this.downloadItemsMap.get(aDownload); if (!downloadItem) { Cu.reportError("Download doesn't exist."); return; } downloadItem.updateView(); this.onUpdateProgress(); // The download may have changed state, so update the clear list button. this.updateClearListButton(); if (downloadItem.done) { // Move the download below active if it should stay in the list. if (downloadItem.matchesSearch(this.searchTerms, this.searchAttributes)) { // Iterate down until we find a non-active download. let next = this.element.nextSibling; while (next && next.inProgress) { next = next.nextSibling; } // Move the item. this.downloadView.insertBefore(this.element, next); } else { this.removeFromView(downloadItem); } return true; } return false; }, onDownloadRemoved: function(aDownload) { let downloadItem = this.downloadItemsMap.get(aDownload); if (!downloadItem) { Cu.reportError("Download doesn't exist."); return; } this.downloadItemsMap.delete(aDownload); this.idToDownloadItemMap.delete(downloadItem.id); this.removeFromView(downloadItem); }, removeFromView: function(aDownloadItem) { // Make sure we have an item to remove. if (!aDownloadItem.element) { return; } let index = this.downloadView.selectedIndex; this.downloadView.removeChild(aDownloadItem.element); this.downloadView.selectedIndex = Math.min(index, this.downloadView.itemCount - 1); // We might have removed the last item, so update the clear list button. this.updateClearListButton(); }, _getDownloadItemForElement(element) { return this.idToDownloadItemMap.get(element.getAttribute("id")); }, _getSelectedDownloadItem() { let dl = this.downloadView.selectedItem; return dl ? this._getDownloadItemForElement(dl) : null; }, }; function Startup() { // Convert strings to those in the string bundle. let sb = document.getElementById("downloadStrings"); let strings = ["paused", "cannotPause", "doneStatus", "doneSize", "doneSizeUnknown", "stateFailed", "stateCanceled", "stateBlocked", "stateBlockedPolicy", "stateDirty", "downloadsTitleFiles", "downloadsTitlePercent",]; for (let name of strings) { gStr[name] = sb.getString(name); } gDownloadList.init(); let DownloadTaskbarProgress = Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress; DownloadTaskbarProgress.onDownloadWindowLoad(window); } function Shutdown() { Downloads.getList(Downloads.ALL) .then(list => list.removeView(gDownloadList)) .catch(Cu.reportError); }