backout b56da878caed and 8487d4305be3 (bug 901360) from Aurora for regressions a=mfinkle

This commit is contained in:
Wes Johnston 2014-05-16 12:13:50 -05:00
parent f28b31d005
commit f82b34e73f
10 changed files with 906 additions and 615 deletions

View File

@ -1,354 +1,617 @@
/* 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/. */
* 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";
let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/DownloadUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils", "resource://gre/modules/DownloadUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "strings",
() => Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties"));
let gStrings = Services.strings.createBundle("chrome://browser/locale/aboutDownloads.properties");
function deleteDownload(download) {
download.finalize(true).then(null, Cu.reportError);
OS.File.remove(download.target.path).then(null, ex => {
if (!(ex instanceof OS.File.Error && ex.becauseNoSuchFile)) {
Cu.reportError(ex);
}
});
}
let downloadTemplate =
"<li downloadGUID='{guid}' class='list-item' role='button' state='{state}' contextmenu='downloadmenu'>" +
"<img class='icon' src='{icon}'/>" +
"<div class='details'>" +
"<div class='row'>" +
// This is a hack so that we can crop this label in its center
"<xul:label class='title' crop='center' value='{target}'/>" +
"<div class='date'>{date}</div>" +
"</div>" +
"<div class='size'>{size}</div>" +
"<div class='domain'>{domain}</div>" +
"<div class='displayState'>{displayState}</div>" +
"</div>" +
"</li>";
let contextMenu = {
_items: [],
_targetDownload: null,
XPCOMUtils.defineLazyGetter(window, "gChromeWin", function ()
window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow));
init: function () {
let element = document.getElementById("downloadmenu");
element.addEventListener("click",
event => event.download = this._targetDownload,
true);
this._items = [
new ContextMenuItem("open",
download => download.succeeded,
download => download.launch().then(null, Cu.reportError)),
new ContextMenuItem("retry",
download => download.error ||
(download.canceled && !download.hasPartialData),
download => download.start().then(null, Cu.reportError)),
new ContextMenuItem("remove",
download => download.stopped,
download => {
Downloads.getList(Downloads.ALL)
.then(list => list.remove(download))
.then(null, Cu.reportError);
deleteDownload(download);
}),
new ContextMenuItem("pause",
download => !download.stopped,
download => download.cancel().then(null, Cu.reportError)),
new ContextMenuItem("resume",
download => download.canceled && download.hasPartialData,
download => download.start().then(null, Cu.reportError)),
new ContextMenuItem("cancel",
download => !download.stopped ||
(download.canceled && download.hasPartialData),
download => {
download.cancel().then(null, Cu.reportError);
download.removePartialData().then(null, Cu.reportError);
}),
// following menu item is a global action
new ContextMenuItem("removeall",
() => downloadLists.finished.length > 0,
() => downloadLists.removeFinished())
var ContextMenus = {
target: null,
init: function() {
document.addEventListener("contextmenu", this, false);
document.getElementById("contextmenu-open").addEventListener("click", this.open.bind(this), false);
document.getElementById("contextmenu-retry").addEventListener("click", this.retry.bind(this), false);
document.getElementById("contextmenu-remove").addEventListener("click", this.remove.bind(this), false);
document.getElementById("contextmenu-pause").addEventListener("click", this.pause.bind(this), false);
document.getElementById("contextmenu-resume").addEventListener("click", this.resume.bind(this), false);
document.getElementById("contextmenu-cancel").addEventListener("click", this.cancel.bind(this), false);
document.getElementById("contextmenu-removeall").addEventListener("click", this.removeAll.bind(this), false);
this.items = [
{ name: "open", states: [Downloads._dlmgr.DOWNLOAD_FINISHED] },
{ name: "retry", states: [Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
{ name: "remove", states: [Downloads._dlmgr.DOWNLOAD_FINISHED,Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
{ name: "removeall", states: [Downloads._dlmgr.DOWNLOAD_FINISHED,Downloads._dlmgr.DOWNLOAD_FAILED, Downloads._dlmgr.DOWNLOAD_CANCELED] },
{ name: "pause", states: [Downloads._dlmgr.DOWNLOAD_DOWNLOADING] },
{ name: "resume", states: [Downloads._dlmgr.DOWNLOAD_PAUSED] },
{ name: "cancel", states: [Downloads._dlmgr.DOWNLOAD_DOWNLOADING, Downloads._dlmgr.DOWNLOAD_NOTSTARTED, Downloads._dlmgr.DOWNLOAD_QUEUED, Downloads._dlmgr.DOWNLOAD_PAUSED] },
];
},
addContextMenuEventListener: function (element) {
element.addEventListener("contextmenu", this.onContextMenu.bind(this));
handleEvent: function(event) {
// store the target of context menu events so that we know which app to act on
this.target = event.target;
while (!this.target.hasAttribute("contextmenu")) {
this.target = this.target.parentNode;
}
if (!this.target)
return;
let state = parseInt(this.target.getAttribute("state"));
for (let i = 0; i < this.items.length; i++) {
var item = this.items[i];
let enabled = (item.states.indexOf(state) > -1);
if (enabled)
document.getElementById("contextmenu-" + item.name).removeAttribute("hidden");
else
document.getElementById("contextmenu-" + item.name).setAttribute("hidden", "true");
}
},
onContextMenu: function (event) {
let target = event.target;
while (target && !target.download) {
target = target.parentNode;
}
if (!target) {
Cu.reportError("No download found for context menu target");
event.preventDefault();
return;
}
// Open shown only for downloads that completed successfully
open: function(event) {
Downloads.openDownload(this.target);
this.target = null;
},
// capture the target download for menu items to use in a click event
this._targetDownload = target.download;
for (let item of this._items) {
item.updateVisibility(target.download);
}
// Retry shown when its failed, canceled, blocked(covered in failed, see _getState())
retry: function (event) {
Downloads.retryDownload(this.target);
this.target = null;
},
// Remove shown when its canceled, finished, failed(failed includes blocked and dirty, see _getState())
remove: function (event) {
Downloads.removeDownload(this.target);
this.target = null;
},
// Pause shown when item is currently downloading
pause: function (event) {
Downloads.pauseDownload(this.target);
this.target = null;
},
// Resume shown for paused items only
resume: function (event) {
Downloads.resumeDownload(this.target);
this.target = null;
},
// Cancel shown when its downloading, notstarted, queued or paused
cancel: function (event) {
Downloads.cancelDownload(this.target);
this.target = null;
},
removeAll: function(event) {
Downloads.removeAll();
this.target = null;
}
};
function ContextMenuItem(name, isVisible, action) {
this.element = document.getElementById("contextmenu-" + name);
this.isVisible = isVisible;
this.element.addEventListener("click", event => action(event.download));
}
ContextMenuItem.prototype = {
updateVisibility: function (download) {
this.element.hidden = !this.isVisible(download);
let Downloads = {
init: function dl_init() {
function onClick(evt) {
let target = evt.target;
while (target.nodeName != "li") {
target = target.parentNode;
if (!target)
return;
}
Downloads.openDownload(target);
}
this._normalList = document.getElementById("normal-downloads-list");
this._privateList = document.getElementById("private-downloads-list");
this._normalList.addEventListener("click", onClick, false);
this._privateList.addEventListener("click", onClick, false);
this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
this._dlmgr.addPrivacyAwareListener(this);
Services.obs.addObserver(this, "last-pb-context-exited", false);
Services.obs.addObserver(this, "download-manager-remove-download-guid", false);
// If we have private downloads, show them all immediately. If we were to
// add them asynchronously, there's a small chance we could get a
// "last-pb-context-exited" notification before downloads are added to the
// list, meaning we'd show private downloads without any private tabs open.
let privateEntries = this.getDownloads({ isPrivate: true });
this._stepAddEntries(privateEntries, this._privateList, privateEntries.length);
// Add non-private downloads
let normalEntries = this.getDownloads({ isPrivate: false });
this._stepAddEntries(normalEntries, this._normalList, 1, this._scrollToSelectedDownload.bind(this));
ContextMenus.init();
},
uninit: function dl_uninit() {
let contextmenus = gChromeWin.NativeWindow.contextmenus;
contextmenus.remove(this.openMenuItem);
contextmenus.remove(this.removeMenuItem);
contextmenus.remove(this.pauseMenuItem);
contextmenus.remove(this.resumeMenuItem);
contextmenus.remove(this.retryMenuItem);
contextmenus.remove(this.cancelMenuItem);
contextmenus.remove(this.deleteAllMenuItem);
this._dlmgr.removeListener(this);
Services.obs.removeObserver(this, "last-pb-context-exited");
Services.obs.removeObserver(this, "download-manager-remove-download-guid");
},
onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress,
aCurTotalProgress, aMaxTotalProgress, aDownload) { },
onDownloadStateChange: function(aState, aDownload) {
switch (aDownload.state) {
case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
// For all "completed" states, move them after active downloads
this._moveDownloadAfterActive(this._getElementForDownload(aDownload.guid));
// Fall-through the rest
case Ci.nsIDownloadManager.DOWNLOAD_SCANNING:
case Ci.nsIDownloadManager.DOWNLOAD_QUEUED:
case Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
let item = this._getElementForDownload(aDownload.guid);
if (item)
this._updateDownloadRow(item, aDownload);
else
this._insertDownloadRow(aDownload);
break;
}
},
onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "last-pb-context-exited":
this._privateList.innerHTML = "";
break;
case "download-manager-remove-download-guid": {
let guid = aSubject.QueryInterface(Ci.nsISupportsCString).data;
this._removeItem(this._getElementForDownload(guid));
break;
}
}
},
_moveDownloadAfterActive: function dl_moveDownloadAfterActive(aItem) {
// Move downloads that just reached a "completed" state below any active
try {
// Iterate down until we find a non-active download
let next = aItem.nextElementSibling;
while (next && this._inProgress(next.getAttribute("state")))
next = next.nextElementSibling;
// Move the item
aItem.parentNode.insertBefore(aItem, next);
} catch (ex) {
this.logError("_moveDownloadAfterActive() " + ex);
}
},
_inProgress: function dl_inProgress(aState) {
return [
this._dlmgr.DOWNLOAD_NOTSTARTED,
this._dlmgr.DOWNLOAD_QUEUED,
this._dlmgr.DOWNLOAD_DOWNLOADING,
this._dlmgr.DOWNLOAD_PAUSED,
this._dlmgr.DOWNLOAD_SCANNING,
].indexOf(parseInt(aState)) != -1;
},
_insertDownloadRow: function dl_insertDownloadRow(aDownload) {
let updatedState = this._getState(aDownload.state);
let item = this._createItem(downloadTemplate, {
guid: aDownload.guid,
target: aDownload.displayName,
icon: "moz-icon://" + aDownload.displayName + "?size=64",
date: DownloadUtils.getReadableDates(new Date())[0],
domain: DownloadUtils.getURIHost(aDownload.source.spec)[0],
size: this._getDownloadSize(aDownload.size),
displayState: this._getStateString(updatedState),
state: updatedState
});
list = aDownload.isPrivate ? this._privateList : this._normalList;
list.insertAdjacentHTML("afterbegin", item);
},
_getDownloadSize: function dl_getDownloadSize(aSize) {
if (aSize > 0) {
let displaySize = DownloadUtils.convertByteUnits(aSize);
return displaySize.join(""); // [0] is size, [1] is units
}
return gStrings.GetStringFromName("downloadState.unknownSize");
},
// Not all states are displayed as-is on mobile, some are translated to a generic state
_getState: function dl_getState(aState) {
let str;
switch (aState) {
// Downloading and Scanning states show up as "Downloading"
case this._dlmgr.DOWNLOAD_DOWNLOADING:
case this._dlmgr.DOWNLOAD_SCANNING:
str = this._dlmgr.DOWNLOAD_DOWNLOADING;
break;
// Failed, Dirty and Blocked states show up as "Failed"
case this._dlmgr.DOWNLOAD_FAILED:
case this._dlmgr.DOWNLOAD_DIRTY:
case this._dlmgr.DOWNLOAD_BLOCKED_POLICY:
case this._dlmgr.DOWNLOAD_BLOCKED_PARENTAL:
str = this._dlmgr.DOWNLOAD_FAILED;
break;
/* QUEUED and NOTSTARTED are not translated as they
dont fall under a common state but we still need
to display a common "status" on the UI */
default:
str = aState;
}
return str;
},
// Note: This doesn't cover all states as some of the states are translated in _getState()
_getStateString: function dl_getStateString(aState) {
let str;
switch (aState) {
case this._dlmgr.DOWNLOAD_DOWNLOADING:
str = "downloadState.downloading";
break;
case this._dlmgr.DOWNLOAD_CANCELED:
str = "downloadState.canceled";
break;
case this._dlmgr.DOWNLOAD_FAILED:
str = "downloadState.failed";
break;
case this._dlmgr.DOWNLOAD_PAUSED:
str = "downloadState.paused";
break;
// Queued and Notstarted show up as "Starting..."
case this._dlmgr.DOWNLOAD_QUEUED:
case this._dlmgr.DOWNLOAD_NOTSTARTED:
str = "downloadState.starting";
break;
default:
return "";
}
return gStrings.GetStringFromName(str);
},
_updateItem: function dl_updateItem(aItem, aValues) {
for (let i in aValues) {
aItem.querySelector("." + i).textContent = aValues[i];
}
},
_initStatement: function dv__initStatement(aIsPrivate) {
let dbConn = aIsPrivate ? this._dlmgr.privateDBConnection : this._dlmgr.DBConnection;
return dbConn.createStatement(
"SELECT guid, name, source, state, startTime, endTime, referrer, " +
"currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
"FROM moz_downloads " +
"ORDER BY isActive DESC, endTime DESC, startTime DESC");
},
_createItem: function _createItem(aTemplate, aValues) {
function htmlEscape(s) {
s = s.replace(/&/g, "&amp;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/</g, "&lt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&apos;");
return s;
}
let t = aTemplate;
for (let key in aValues) {
if (aValues.hasOwnProperty(key)) {
let regEx = new RegExp("{" + key + "}", "g");
let value = htmlEscape(aValues[key].toString());
t = t.replace(regEx, value);
}
}
return t;
},
_getEntry: function dv__getEntry(aStmt) {
try {
if (!aStmt.executeStep()) {
return null;
}
let updatedState = this._getState(aStmt.row.state);
// Try to get the attribute values from the statement
return {
guid: aStmt.row.guid,
target: aStmt.row.name,
icon: "moz-icon://" + aStmt.row.name + "?size=64",
date: DownloadUtils.getReadableDates(new Date(aStmt.row.endTime / 1000))[0],
domain: DownloadUtils.getURIHost(aStmt.row.source)[0],
size: this._getDownloadSize(aStmt.row.maxBytes),
displayState: this._getStateString(updatedState),
state: updatedState
};
} catch (e) {
// Something went wrong when stepping or getting values, so clear and quit
this.logError("_getEntry() " + e);
aStmt.reset();
return null;
}
},
_stepAddEntries: function dv__stepAddEntries(aEntries, aList, aNumItems, aCallback) {
if (aEntries.length == 0){
if (aCallback)
aCallback();
return;
}
let attrs = aEntries.shift();
let item = this._createItem(downloadTemplate, attrs);
aList.insertAdjacentHTML("beforeend", item);
// Add another item to the list if we should; otherwise, let the UI update
// and continue later
if (aNumItems > 1) {
this._stepAddEntries(aEntries, aList, aNumItems - 1, aCallback);
} else {
// Use a shorter delay for earlier downloads to display them faster
let delay = Math.min(aList.itemCount * 10, 300);
setTimeout(function () {
this._stepAddEntries(aEntries, aList, 5, aCallback);
}.bind(this), delay);
}
},
getDownloads: function dl_getDownloads(aParams) {
aParams = aParams || {};
let stmt = this._initStatement(aParams.isPrivate);
stmt.reset();
stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED);
stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING);
stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED);
stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED);
stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING);
let entries = [];
while (entry = this._getEntry(stmt)) {
entries.push(entry);
}
stmt.finalize();
return entries;
},
_getElementForDownload: function dl_getElementForDownload(aKey) {
return document.body.querySelector("li[downloadGUID='" + aKey + "']");
},
_getDownloadForElement: function dl_getDownloadForElement(aElement, aCallback) {
let guid = aElement.getAttribute("downloadGUID");
this._dlmgr.getDownloadByGUID(guid, function(status, download) {
if (!Components.isSuccessCode(status)) {
return;
}
aCallback(download);
});
},
_removeItem: function dl_removeItem(aItem) {
// Make sure we have an item to remove
if (!aItem)
return;
aItem.parentNode.removeChild(aItem);
},
openDownload: function dl_openDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
if (aDownload.state !== Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
// Do not open unfinished downloads.
return;
}
try {
let f = aDownload.targetFile;
if (f) f.launch();
} catch (ex) {
this.logError("openDownload() " + ex, aDownload);
}
}.bind(this));
},
removeDownload: function dl_removeDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
if (aDownload.targetFile) {
OS.File.remove(aDownload.targetFile.path).then(null, function onError(reason) {
if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
this.logError("removeDownload() " + reason, aDownload);
}
}.bind(this));
}
aDownload.remove();
}.bind(this));
},
removeAll: function dl_removeAll() {
let title = gStrings.GetStringFromName("downloadAction.deleteAll");
let messageForm = gStrings.GetStringFromName("downloadMessage.deleteAll");
let elements = document.body.querySelectorAll("li[state='" + this._dlmgr.DOWNLOAD_FINISHED + "']," +
"li[state='" + this._dlmgr.DOWNLOAD_CANCELED + "']," +
"li[state='" + this._dlmgr.DOWNLOAD_FAILED + "']");
let message = PluralForm.get(elements.length, messageForm)
.replace("#1", elements.length);
let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_OK +
Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL;
let choice = Services.prompt.confirmEx(null, title, message, flags,
null, null, null, null, {});
if (choice == 0) {
for (let i = 0; i < elements.length; i++) {
this.removeDownload(elements[i]);
}
}
},
pauseDownload: function dl_pauseDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
try {
aDownload.pause();
this._updateDownloadRow(aItem, aDownload);
} catch (ex) {
this.logError("Error: pauseDownload() " + ex, aDownload);
}
}.bind(this));
},
resumeDownload: function dl_resumeDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
try {
aDownload.resume();
this._updateDownloadRow(aItem, aDownload);
} catch (ex) {
this.logError("resumeDownload() " + ex, aDownload);
}
}.bind(this));
},
retryDownload: function dl_retryDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
try {
this._removeItem(aItem);
aDownload.retry();
} catch (ex) {
this.logError("retryDownload() " + ex, aDownload);
}
}.bind(this));
},
cancelDownload: function dl_cancelDownload(aItem) {
this._getDownloadForElement(aItem, function(aDownload) {
OS.File.remove(aDownload.targetFile.path).then(null, function onError(reason) {
if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
this.logError("cancelDownload() " + reason, aDownload);
}
}.bind(this));
aDownload.cancel();
this._updateDownloadRow(aItem, aDownload);
}.bind(this));
},
_updateDownloadRow: function dl_updateDownloadRow(aItem, aDownload) {
try {
let updatedState = this._getState(aDownload.state);
aItem.setAttribute("state", updatedState);
this._updateItem(aItem, {
size: this._getDownloadSize(aDownload.size),
displayState: this._getStateString(updatedState),
date: DownloadUtils.getReadableDates(new Date())[0]
});
} catch (ex) {
this.logError("_updateDownloadRow() " + ex, aDownload);
}
},
/**
* In case a specific downloadId was passed while opening, scrolls the list to
* the given elemenet
*/
_scrollToSelectedDownload : function dl_scrollToSelected() {
let spec = document.location.href;
let pos = spec.indexOf("?");
let query = "";
if (pos >= 0)
query = spec.substring(pos + 1);
// Just assume the query is "id=<id>"
let id = query.substring(3);
if (!id) {
return;
}
downloadElement = this._getElementForDownload(id);
if (!downloadElement) {
return;
}
downloadElement.scrollIntoView();
},
/**
* Logs the error to the console.
*
* @param aMessage error message to log
* @param aDownload (optional) if given, and if the download is private, the
* log message is suppressed
*/
logError: function dl_logError(aMessage, aDownload) {
if (!aDownload || !aDownload.isPrivate) {
console.log("Error: " + aMessage);
}
},
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsIDownloadProgressListener) &&
!aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
function DownloadListView(type, listElementId) {
this.listElement = document.getElementById(listElementId);
contextMenu.addContextMenuEventListener(this.listElement);
this.items = new Map();
Downloads.getList(type)
.then(list => list.addView(this))
.then(null, Cu.reportError);
window.addEventListener("unload", event => {
Downloads.getList(type)
.then(list => list.removeView(this))
.then(null, Cu.reportError);
});
}
DownloadListView.prototype = {
get finished() {
let finished = [];
for (let download of this.items.keys()) {
if (download.stopped && (!download.hasPartialData || download.error)) {
finished.push(download);
}
}
document.addEventListener("DOMContentLoaded", Downloads.init.bind(Downloads), true);
window.addEventListener("unload", Downloads.uninit.bind(Downloads), false);
return finished;
},
insertOrMoveItem: function (item) {
var compare = (a, b) => {
// active downloads always before stopped downloads
if (a.stopped != b.stopped) {
return b.stopped ? -1 : 1
}
// most recent downloads first
return b.startTime - a.startTime;
};
let insertLocation = this.listElement.firstChild;
while (insertLocation && compare(item.download, insertLocation.download) > 0) {
insertLocation = insertLocation.nextElementSibling;
}
this.listElement.insertBefore(item.element, insertLocation);
},
onDownloadAdded: function (download) {
let item = new DownloadItem(download);
this.items.set(download, item);
this.insertOrMoveItem(item);
},
onDownloadChanged: function (download) {
let item = this.items.get(download);
if (!item) {
Cu.reportError("No DownloadItem found for download");
return;
}
if (item.stateChanged) {
this.insertOrMoveItem(item);
}
item.onDownloadChanged();
},
onDownloadRemoved: function (download) {
let item = this.items.get(download);
if (!item) {
Cu.reportError("No DownloadItem found for download");
return;
}
this.items.delete(download);
this.listElement.removeChild(item.element);
}
};
let downloadLists = {
init: function () {
this.publicDownloads = new DownloadListView(Downloads.PUBLIC, "public-downloads-list");
this.privateDownloads = new DownloadListView(Downloads.PRIVATE, "private-downloads-list");
},
get finished() {
return this.publicDownloads.finished.concat(this.privateDownloads.finished);
},
removeFinished: function () {
let finished = this.finished;
if (finished.length == 0) {
return;
}
let title = strings.GetStringFromName("downloadAction.deleteAll");
let messageForm = strings.GetStringFromName("downloadMessage.deleteAll");
let message = PluralForm.get(finished.length, messageForm).replace("#1", finished.length);
if (Services.prompt.confirm(null, title, message)) {
Downloads.getList(Downloads.ALL)
.then(list => {
for (let download of finished) {
list.remove(download).then(null, Cu.reportError);
deleteDownload(download);
}
}, Cu.reportError);
}
}
};
function DownloadItem(download) {
this._download = download;
this._updateFromDownload();
this._domain = DownloadUtils.getURIHost(download.source.url)[0];
this._fileName = this._htmlEscape(OS.Path.basename(download.target.path));
this._iconUrl = "moz-icon://" + this._fileName + "?size=64";
this._startDate = this._htmlEscape(DownloadUtils.getReadableDates(download.startTime)[0]);
this._element = this.createElement();
}
const kDownloadStatePropertyNames = [
"stopped",
"succeeded",
"canceled",
"error",
"startTime"
];
DownloadItem.prototype = {
_htmlEscape : function (s) {
s = s.replace(/&/g, "&amp;");
s = s.replace(/>/g, "&gt;");
s = s.replace(/</g, "&lt;");
s = s.replace(/"/g, "&quot;");
s = s.replace(/'/g, "&apos;");
return s;
},
_updateFromDownload: function () {
this._state = {};
kDownloadStatePropertyNames.forEach(
name => this._state[name] = this._download[name],
this);
},
get stateChanged() {
return kDownloadStatePropertyNames.some(
name => this._state[name] != this._download[name],
this);
},
get download() this._download,
get element() this._element,
createElement: function() {
let template = document.getElementById("download-item");
// TODO: use this once <template> is working
// let element = document.importNode(template.content, true);
// simulate a <template> node...
let element = template.cloneNode(true);
element.removeAttribute("id");
element.removeAttribute("style");
// launch the download if clicked
element.addEventListener("click", this.onClick.bind(this));
// set download as an expando property for the context menu
element.download = this.download;
// fill in template placeholders
this.updateElement(element);
return element;
},
updateElement: function (element) {
element.querySelector(".date").textContent = this.startDate;
element.querySelector(".domain").textContent = this.domain;
element.querySelector(".icon").src = this.iconUrl;
element.querySelector(".size").textContent = this.size;
element.querySelector(".state").textContent = this.stateDescription;
element.querySelector(".title").setAttribute("value", this.fileName);
},
onClick: function (event) {
if (this.download.succeeded) {
this.download.launch().then(null, Cu.reportError);
}
},
onDownloadChanged: function () {
this._updateFromDownload();
this.updateElement(this.element);
},
// template properties below
get domain() this._domain,
get fileName() this._fileName,
get id() this._id,
get iconUrl() this._iconUrl,
get size() {
if (this.download.hasProgress) {
return DownloadUtils.convertByteUnits(this.download.totalBytes).join("");
}
return strings.GetStringFromName("downloadState.unknownSize");
},
get startDate() {
return this._startDate;
},
get stateDescription() {
let name;
if (this.download.error) {
name = "downloadState.failed";
} else if (this.download.canceled) {
if (this.download.hasPartialData) {
name = "downloadState.paused";
} else {
name = "downloadState.canceled";
}
} else if (!this.download.stopped) {
if (this.download.currentBytes > 0) {
name = "downloadState.downloading";
} else {
name = "downloadState.starting";
}
}
if (name) {
return strings.GetStringFromName(name);
}
return "";
}
};
window.addEventListener("DOMContentLoaded", event => {
contextMenu.init();
downloadLists.init()
});

View File

@ -35,27 +35,11 @@
<menuitem id="contextmenu-removeall" label="&aboutDownloads.removeAll;"></menuitem>
</menu>
<!--template id="download-item"-->
<li id="download-item" class="list-item" role="button" contextmenu="downloadmenu" style="display: none">
<img class="icon" src=""/>
<div class="details">
<div class="row">
<!-- This is a hack so that we can crop this label in its center -->
<xul:label class="title" crop="center" value=""/>
<div class="date"></div>
</div>
<div class="size"></div>
<div class="domain"></div>
<div class="state"></div>
</div>
</li>
<!--/template-->
<div class="header">
<div>&aboutDownloads.header;</div>
</div>
<ul id="private-downloads-list" class="list"></ul>
<ul id="public-downloads-list" class="list"></ul>
<ul id="normal-downloads-list" class="list"></ul>
<span id="no-downloads-indicator">&aboutDownloads.empty;</span>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutDownloads.js"/>
</body>

View File

@ -13,7 +13,6 @@ let Cr = Components.results;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/DownloadNotifications.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/JNI.jsm");
Cu.import('resource://gre/modules/Payment.jsm');
@ -370,7 +369,7 @@ var BrowserApp = {
NativeWindow.init();
LightWeightThemeWebInstaller.init();
DownloadNotifications.init();
Downloads.init();
FormAssistant.init();
IndexedDB.init();
HealthReportStatusListener.init();
@ -658,7 +657,7 @@ var BrowserApp = {
function(aTarget) {
aTarget.muted = true;
});
NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
NativeWindow.contextmenus.mediaContext("media-muted"),
function(aTarget) {
@ -751,7 +750,6 @@ var BrowserApp = {
shutdown: function shutdown() {
NativeWindow.uninit();
DownloadNotifications.uninit();
LightWeightThemeWebInstaller.uninit();
FormAssistant.uninit();
IndexedDB.uninit();
@ -1825,7 +1823,7 @@ var NativeWindow = {
return;
sendMessageToJava({
type: "Menu:Update",
type: "Menu:Update",
id: aId,
options: aOptions
});
@ -1851,7 +1849,7 @@ var NativeWindow = {
* automatically dismiss before this time.
* checkbox: A string to appear next to a checkbox under the notification
* message. The button callback functions will be called with
* the checked state as an argument.
* the checked state as an argument.
*/
show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
if (aButtons == null) {
@ -2253,7 +2251,7 @@ var NativeWindow = {
mode: SelectionHandler.SELECT_AT_POINT,
x: x,
y: y
})) {
})) {
SelectionHandler.attachCaret(target);
}
}
@ -3147,7 +3145,7 @@ Tab.prototype = {
viewportWidth - 15);
},
/**
/**
* Reloads the tab with the desktop mode setting.
*/
reloadWithMode: function (aDesktopMode) {
@ -3782,7 +3780,7 @@ Tab.prototype = {
if (sizes == "any") {
// Since Java expects an integer, use -1 to represent icons with sizes="any"
maxSize = -1;
maxSize = -1;
} else {
let tokens = sizes.split(" ");
tokens.forEach(function(token) {
@ -6663,7 +6661,7 @@ var IdentityHandler = {
.QueryInterface(Components.interfaces.nsISSLStatusProvider)
.SSLStatus;
// Don't pass in the actual location object, since it can cause us to
// Don't pass in the actual location object, since it can cause us to
// hold on to the window object too long. Just pass in the fields we
// care about. (bug 424829)
let locationObj = {};
@ -6713,7 +6711,7 @@ var IdentityHandler = {
return result;
}
// Otherwise, we don't know the cert owner
result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
@ -7325,7 +7323,7 @@ var WebappsUI = {
favicon.src = WebappsUI.DEFAULT_ICON;
}
};
favicon.src = aIconURL;
},
@ -8525,21 +8523,3 @@ HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
}
},
});
/**
* CID of Downloads.jsm's implementation of nsITransfer.
*/
const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
/**
* Contract ID of the service implementing nsITransfer.
*/
const kTransferContractId = "@mozilla.org/transfer;1";
// Override Toolkit's nsITransfer implementation with the one from the
// JavaScript API for downloads. This will eventually be removed when
// nsIDownloadManager will not be available anymore (bug 851471). The
// old code in this module will be removed in bug 899110.
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(kTransferCid, "",
kTransferContractId, null);

View File

@ -11,6 +11,7 @@
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="chrome://browser/content/browser.js"/>
<script type="application/javascript" src="chrome://browser/content/downloads.js"/>
<deck id="browsers" flex="1"/>

View File

@ -0,0 +1,298 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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";
let Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
function dump(a) {
Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).logStringMessage(a);
}
XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
"resource://gre/modules/Notifications.jsm");
const URI_GENERIC_ICON_DOWNLOAD = "drawable://alert_download";
const URI_PAUSE_ICON = "drawable://pause";
const URI_CANCEL_ICON = "drawable://close";
const URI_RESUME_ICON = "drawable://play";
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
var Downloads = {
_initialized: false,
_dlmgr: null,
_progressAlert: null,
_privateDownloads: [],
_showingPrompt: false,
_downloadsIdMap: {},
_getLocalFile: function dl__getLocalFile(aFileURI) {
// if this is a URL, get the file from that
// XXX it's possible that using a null char-set here is bad
const fileUrl = Services.io.newURI(aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
},
init: function dl_init() {
if (this._initialized)
return;
this._initialized = true;
// Monitor downloads and display alerts
this._dlmgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
this._progressAlert = new AlertDownloadProgressListener();
this._dlmgr.addPrivacyAwareListener(this._progressAlert);
Services.obs.addObserver(this, "last-pb-context-exited", true);
},
openDownload: function dl_openDownload(aDownload) {
let fileUri = aDownload.target.spec;
let guid = aDownload.guid;
let f = this._getLocalFile(fileUri);
try {
f.launch();
} catch (ex) {
// in case we are not able to open the file (i.e. there is no app able to handle it)
// we just open the browser tab showing it
BrowserApp.addTab("about:downloads?id=" + guid);
}
},
cancelDownload: function dl_cancelDownload(aDownload) {
aDownload.cancel();
let fileURI = aDownload.target.spec;
let f = this._getLocalFile(fileURI);
OS.File.remove(f.path);
},
showCancelConfirmPrompt: function dl_showCancelConfirmPrompt(aDownload) {
if (this._showingPrompt)
return;
this._showingPrompt = true;
// Open a prompt that offers a choice to cancel the download
let title = Strings.browser.GetStringFromName("downloadCancelPromptTitle");
let message = Strings.browser.GetStringFromName("downloadCancelPromptMessage");
let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_YES +
Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_NO;
let choice = Services.prompt.confirmEx(null, title, message, flags,
null, null, null, null, {});
if (choice == 0)
this.cancelDownload(aDownload);
this._showingPrompt = false;
},
handleClickEvent: function dl_handleClickEvent(aDownload) {
// Only open the downloaded file if the download is complete
if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED)
this.openDownload(aDownload);
else if (aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING ||
aDownload.state == Ci.nsIDownloadManager.DOWNLOAD_PAUSED)
this.showCancelConfirmPrompt(aDownload);
},
clickCallback: function dl_clickCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
this.handleClickEvent(download);
}).bind(this));
},
pauseClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
download.pause();
}).bind(this));
},
resumeClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
download.resume();
}).bind(this));
},
cancelClickCallback: function dl_buttonPauseCallback(aDownloadId) {
this._dlmgr.getDownloadByGUID(aDownloadId, (function(status, download) {
if (Components.isSuccessCode(status))
this.cancelDownload(download);
}).bind(this));
},
notificationCanceledCallback: function dl_notifCancelCallback(aId, aDownloadId) {
let notificationId = this._downloadsIdMap[aDownloadId];
if (notificationId && notificationId == aId)
delete this._downloadsIdMap[aDownloadId];
},
createNotification: function dl_createNotif(aDownload, aOptions) {
let notificationId = Notifications.create(aOptions);
this._downloadsIdMap[aDownload.guid] = notificationId;
},
updateNotification: function dl_updateNotif(aDownload, aOptions) {
let notificationId = this._downloadsIdMap[aDownload.guid];
if (notificationId)
Notifications.update(notificationId, aOptions);
},
cancelNotification: function dl_cleanNotif(aDownload) {
Notifications.cancel(this._downloadsIdMap[aDownload.guid]);
delete this._downloadsIdMap[aDownload.guid];
},
// observer for last-pb-context-exited
observe: function dl_observe(aSubject, aTopic, aData) {
let download;
while ((download = this._privateDownloads.pop())) {
try {
let notificationId = aDownload.guid;
Notifications.clear(notificationId);
Downloads.removeNotification(download);
} catch (e) {
dump("Error removing private download: " + e);
}
}
},
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsISupports) &&
!aIID.equals(Ci.nsIObserver) &&
!aIID.equals(Ci.nsISupportsWeakReference))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};
const PAUSE_BUTTON = {
buttonId: "pause",
title : Strings.browser.GetStringFromName("alertDownloadsPause"),
icon : URI_PAUSE_ICON,
onClicked: function (aId, aCookie) {
Downloads.pauseClickCallback(aCookie);
}
};
const CANCEL_BUTTON = {
buttonId: "cancel",
title : Strings.browser.GetStringFromName("alertDownloadsCancel"),
icon : URI_CANCEL_ICON,
onClicked: function (aId, aCookie) {
Downloads.cancelClickCallback(aCookie);
}
};
const RESUME_BUTTON = {
buttonId: "resume",
title : Strings.browser.GetStringFromName("alertDownloadsResume"),
icon: URI_RESUME_ICON,
onClicked: function (aId, aCookie) {
Downloads.resumeClickCallback(aCookie);
}
};
function DownloadNotifOptions (aDownload, aTitle, aMessage) {
this.icon = URI_GENERIC_ICON_DOWNLOAD;
this.onCancel = function (aId, aCookie) {
Downloads.notificationCanceledCallback(aId, aCookie);
}
this.onClick = function (aId, aCookie) {
Downloads.clickCallback(aCookie);
}
this.title = aTitle;
this.message = aMessage;
this.buttons = null;
this.cookie = aDownload.guid;
this.persistent = true;
}
function DownloadProgressNotifOptions (aDownload, aButtons) {
DownloadNotifOptions.apply(this, [aDownload, aDownload.displayName, aDownload.percentComplete + "%"]);
this.ongoing = true;
this.progress = aDownload.percentComplete;
this.buttons = aButtons;
}
// AlertDownloadProgressListener is used to display progress in the alert notifications.
function AlertDownloadProgressListener() { }
AlertDownloadProgressListener.prototype = {
//////////////////////////////////////////////////////////////////////////////
//// nsIDownloadProgressListener
onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) {
let strings = Strings.browser;
let availableSpace = -1;
try {
// diskSpaceAvailable is not implemented on all systems
let availableSpace = aDownload.targetFile.diskSpaceAvailable;
} catch(ex) { }
let contentLength = aDownload.size;
if (availableSpace > 0 && contentLength > 0 && contentLength > availableSpace) {
Downloads.updateNotification(aDownload, new DownloadNotifOptions(aDownload,
strings.GetStringFromName("alertDownloadsNoSpace"),
strings.GetStringFromName("alertDownloadsSize")));
aDownload.cancel();
}
if (aDownload.percentComplete == -1) {
// Undetermined progress is not supported yet
return;
}
Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [PAUSE_BUTTON, CANCEL_BUTTON]));
},
onDownloadStateChange: function(aState, aDownload) {
let state = aDownload.state;
switch (state) {
case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: {
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertDownloadsToast"), "long");
Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
Strings.browser.GetStringFromName("alertDownloadsStart2"),
aDownload.displayName));
break;
}
case Ci.nsIDownloadManager.DOWNLOAD_PAUSED: {
Downloads.updateNotification(aDownload, new DownloadProgressNotifOptions(aDownload, [RESUME_BUTTON, CANCEL_BUTTON]));
break;
}
case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
case Ci.nsIDownloadManager.DOWNLOAD_FINISHED: {
Downloads.cancelNotification(aDownload);
if (aDownload.isPrivate) {
let index = Downloads._privateDownloads.indexOf(aDownload);
if (index != -1) {
Downloads._privateDownloads.splice(index, 1);
}
}
if (state == Ci.nsIDownloadManager.DOWNLOAD_FINISHED) {
Downloads.createNotification(aDownload, new DownloadNotifOptions(aDownload,
Strings.browser.GetStringFromName("alertDownloadsDone2"),
aDownload.displayName));
}
break;
}
}
},
onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
//////////////////////////////////////////////////////////////////////////////
//// nsISupports
QueryInterface: function (aIID) {
if (!aIID.equals(Ci.nsIDownloadProgressListener) &&
!aIID.equals(Ci.nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
}
};

View File

@ -34,6 +34,7 @@ chrome.jar:
* content/browser.js (content/browser.js)
content/bindings/checkbox.xml (content/bindings/checkbox.xml)
content/bindings/settings.xml (content/bindings/settings.xml)
content/downloads.js (content/downloads.js)
content/netError.xhtml (content/netError.xhtml)
content/SelectHelper.js (content/SelectHelper.js)
content/SelectionHandler.js (content/SelectionHandler.js)

View File

@ -442,9 +442,6 @@
@BINPATH@/components/TestInterfaceJS.manifest
#endif
@BINPATH@/components/Downloads.manifest
@BINPATH@/components/DownloadLegacy.js
; Modules
@BINPATH@/modules/*

View File

@ -1,232 +0,0 @@
/* 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";
this.EXPORTED_SYMBOLS = ["DownloadNotifications"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyGetter(this, "strings",
() => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
Object.defineProperty(this, "nativeWindow",
{ get: () => Services.wm.getMostRecentWindow("navigator:browser").NativeWindow });
const kButtons = {
PAUSE: new DownloadNotificationButton("pause",
"drawable://pause",
"alertDownloadsPause",
notification => notification.pauseDownload()),
RESUME: new DownloadNotificationButton("resume",
"drawable://play",
"alertDownloadsResume",
notification => notification.resumeDownload()),
CANCEL: new DownloadNotificationButton("cancel",
"drawable://close",
"alertDownloadsCancel",
notification => notification.cancelDownload())
};
let notifications = new Map();
var DownloadNotifications = {
init: function () {
if (!this._viewAdded) {
Downloads.getList(Downloads.ALL)
.then(list => list.addView(this))
.then(null, Cu.reportError);
this._viewAdded = true;
}
},
uninit: function () {
if (this._viewAdded) {
Downloads.getList(Downloads.ALL)
.then(list => list.removeView(this))
.then(null, Cu.reportError);
for (let notification of notifications.values()) {
notification.hide();
}
this._viewAdded = false;
}
},
onDownloadAdded: function (download) {
let notification = new DownloadNotification(download);
notifications.set(download, notification);
notification.showOrUpdate();
if (download.currentBytes == 0) {
nativeWindow.toast.show(strings.GetStringFromName("alertDownloadsToast"), "long");
}
},
onDownloadChanged: function (download) {
let notification = notifications.get(download);
if (!notification) {
Cu.reportError("Download doesn't have a notification.");
return;
}
notification.showOrUpdate();
},
onDownloadRemoved: function (download) {
let notification = notifications.get(download);
if (!notification) {
Cu.reportError("Download doesn't have a notification.");
return;
}
notification.hide();
notifications.delete(download);
}
};
function DownloadNotification(download) {
this.download = download;
this._fileName = OS.Path.basename(download.target.path);
this.id = null;
}
DownloadNotification.prototype = {
_updateFromDownload: function () {
this._downloading = !this.download.stopped;
this._paused = this.download.canceled && this.download.hasPartialData;
this._succeeded = this.download.succeeded;
this._show = this._downloading || this._paused || this._succeeded;
},
get options() {
if (!this._show) {
return null;
}
let options = {
icon : "drawable://alert_download",
onClick : (id, cookie) => this.onClick(),
onCancel : (id, cookie) => this._notificationId = null,
cookie : this.download
};
if (this._downloading) {
if (this.download.currentBytes == 0) {
this._updateOptionsForStatic(options, "alertDownloadsStart2");
} else {
this._updateOptionsForOngoing(options, [kButtons.PAUSE, kButtons.CANCEL]);
}
} else if (this._paused) {
this._updateOptionsForOngoing(options, [kButtons.RESUME, kButtons.CANCEL]);
} else if (this._succeeded) {
options.persistent = false;
this._updateOptionsForStatic(options, "alertDownloadsDone2");
}
return options;
},
_updateOptionsForStatic : function (options, titleName) {
options.title = strings.GetStringFromName(titleName);
options.message = this._fileName;
},
_updateOptionsForOngoing: function (options, buttons) {
options.title = this._fileName;
options.message = this.download.progress + "%";
options.buttons = buttons;
options.ongoing = true;
options.progress = this.download.progress;
options.persistent = true;
},
showOrUpdate: function () {
this._updateFromDownload();
if (this._show) {
if (!this.id) {
this.id = Notifications.create(this.options);
} else {
Notifications.update(this.id, this.options);
}
} else {
this.hide();
}
},
hide: function () {
if (this.id) {
Notifications.cancel(this.id);
this.id = null;
}
},
onClick: function () {
if (this.download.succeeded) {
this.download.launch().then(null, Cu.reportError);
} else {
ConfirmCancelPrompt.show(this);
}
},
pauseDownload: function () {
this.download.cancel().then(null, Cu.reportError);
},
resumeDownload: function () {
this.download.start().then(null, Cu.reportError);
},
cancelDownload: function () {
this.hide();
this.download.cancel().then(null, Cu.reportError);
this.download.removePartialData().then(null, Cu.reportError);
}
};
var ConfirmCancelPrompt = {
showing: false,
show: function (downloadNotification) {
if (this.showing) {
return;
}
this.showing = true;
// Open a prompt that offers a choice to cancel the download
let title = strings.GetStringFromName("downloadCancelPromptTitle");
let message = strings.GetStringFromName("downloadCancelPromptMessage");
if (Services.prompt.confirm(null, title, message)) {
downloadNotification.cancelDownload();
}
this.showing = false;
}
};
function DownloadNotificationButton(buttonId, iconUrl, titleStringName, onClicked) {
this.buttonId = buttonId;
this.title = strings.GetStringFromName(titleStringName);
this.icon = iconUrl;
this.onClicked = (id, download) => {
let notification = notifications.get(download);
if (!notification) {
Cu.reportError("No DownloadNotification for button");
return;
}
onClicked(notification);
}
}

View File

@ -8,7 +8,6 @@ EXTRA_JS_MODULES += [
'Accounts.jsm',
'AndroidLog.jsm',
'ContactService.jsm',
'DownloadNotifications.jsm',
'HelperApps.jsm',
'Home.jsm',
'HomeProvider.jsm',

View File

@ -51,7 +51,7 @@ li:active div.details,
display: inline;
}
.state {
.displayState {
color: gray;
margin-bottom: -3px; /* Prevent overflow that hides bottom border */
}
@ -65,7 +65,7 @@ li:active div.details,
display: none;
}
#private-downloads-list:empty + #public-downloads-list:empty + #no-downloads-indicator {
#private-downloads-list:empty + #normal-downloads-list:empty + #no-downloads-indicator {
display: block;
text-align: center;
padding-top: 3.9em;