// Copyright (c) 2011 by Kris Maglione // // This work is licensed for reuse under an MIT license. Details are // given in the LICENSE.txt file included with this file. "use strict"; Components.utils.import("resource://dactyl/bootstrap.jsm"); defineModule("downloads", { exports: ["Download", "Downloads", "downloads"], use: ["io", "messages", "prefs", "services", "util"] }, this); Cu.import("resource://gre/modules/DownloadUtils.jsm", this); let prefix = "DOWNLOAD_"; var states = iter([v, k.slice(prefix.length).toLowerCase()] for ([k, v] in Iterator(Ci.nsIDownloadManager)) if (k.indexOf(prefix) == 0)) .toObject(); var Download = Class("Download", { init: function init(id, list) { let self = XPCSafeJSObjectWrapper(services.downloadManager.getDownload(id)); self.__proto__ = this; this.instance = this; this.list = list; this.nodes = { commandTarget: self }; util.xmlToDom( {self.displayName} {self.targetFile.path} {_("download.action.Pause")} {_("download.action.Remove")} {_("download.action.Resume")} {_("download.action.Retry")} {_("download.action.Cancel")} {_("download.action.Delete")} / {self.source.spec} , this.list.document, this.nodes); this.nodes.launch.addEventListener("click", function (event) { if (event.button == 0) { event.preventDefault(); self.command("launch"); } }, false); self.updateStatus(); return self; }, get status() states[this.state], inState: function inState(states) states.indexOf(this.status) >= 0, get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]), allowedCommands: Class.memoize(function () let (self = this) ({ get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]), get delete() !this.cancel && self.targetFile.exists(), get launch() self.targetFile.exists() && self.inState(["finished"]), get pause() self.inState(["downloading"]), get remove() self.inState(["blocked_parental", "blocked_policy", "canceled", "dirty", "failed", "finished"]), get resume() self.resumable && self.inState(["paused"]), get retry() self.inState(["canceled", "failed"]) })), command: function command(name) { util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand")); util.assert(this.allowedCommands[name], _("download.commandNotAllowed")); if (Set.has(this.commands, name)) this.commands[name].call(this); else services.downloadManager[name + "Download"](this.id); }, commands: { delete: function delete_() { this.targetFile.remove(false); this.updateStatus(); }, launch: function launch() { let self = this; // Behavior mimics that of the builtin Download Manager. function action() { try { if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp) this.MIMEInfo.launchWithFile(file); else file.launch(); } catch (e) { services.externalProtocol.loadUrl(this.target); } } let file = io.File(this.targetFile); if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true)) this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ", function (resp) { if (/^a(lways)$/i.test(resp)) { prefs.set("browser.download.manager.alertOnEXEOpen", false); resp = "yes"; } if (/^y(es)?$/i.test(resp)) action.call(self); }); else action.call(this); } }, _compare: { active: function (a, b) a.alive - b.alive, complete: function (a, b) a.percentComplete - b.percentComplete, date: function (a, b) a.startTime - b.startTime, filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName), size: function (a, b) a.size - b.size, speed: function (a, b) a.speed - b.speed, time: function (a, b) a.timeRemaining - b.timeRemaining, url: function (a, b) String.localeCompare(a.source.spec, b.source.spec) }, compare: function compare(other) values(this.list.sortOrder).map(function (order) { let val = this._compare[order.substr(1)](this, other); return (order[0] == "-") ? -val : val; }, this).nth(util.identity, 0) || 0, timeRemaining: Infinity, updateProgress: function updateProgress() { let self = this.__proto__; if (this.amountTransferred === this.size) { this.nodes.speed.textContent = ""; this.nodes.time.textContent = ""; } else { this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s"; if (this.speed == 0 || this.size == 0) this.nodes.time.textContent = _("download.unknown"); else { let seconds = (this.size - this.amountTransferred) / this.speed; [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining); if (this.timeRemaining) this.nodes.time.textContent = util.formatSeconds(this.timeRemaining); else this.nodes.time.textContent = _("download.almostDone"); } } let total = this.nodes.progressTotal.textContent = this.size ? util.formatBytes(this.size, 1, true) : _("download.unknown"); let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$"); this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, ""); this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : ""; }, updateStatus: function updateStatus() { this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true"); this.nodes.row.setAttribute("status", this.status); this.nodes.state.textContent = util.capitalize(this.status); for (let node in values(this.nodes)) if (node.update) node.update(); this.updateProgress(); } }); var DownloadList = Class("DownloadList", XPCOM([Ci.nsIDownloadProgressListener, Ci.nsIObserver, Ci.nsISupportsWeakReference]), { init: function init(modules, filter, sort) { this.sortOrder = sort; this.modules = modules; this.filter = filter && filter.toLowerCase(); this.nodes = { commandTarget: this }; this.downloads = {}; }, cleanup: function cleanup() { this.observe.unregister(); services.downloadManager.removeListener(this); }, message: Class.memoize(function () { util.xmlToDom({_("title.Title")}{_("title.Status")} {_("title.Progress")} {_("title.Speed")} {_("title.Time remaining")} {_("title.Source")}
{_("title.Totals")}:  {_("download.action.Clear")} /
, this.document, this.nodes); for (let row in iter(services.downloadManager.DBConnection .createStatement("SELECT id FROM moz_downloads"))) this.addDownload(row.id); this.update(); util.addObserver(this); services.downloadManager.addListener(this); return this.nodes.list; }), addDownload: function addDownload(id) { if (!(id in this.downloads)) { let download = Download(id, this); if (this.filter && download.displayName.indexOf(this.filter) === -1) return; this.downloads[id] = download; let index = values(this.downloads).sort(function (a, b) a.compare(b)) .indexOf(download); this.nodes.list.insertBefore(download.nodes.row, this.nodes.list.childNodes[index + 1]); } }, removeDownload: function removeDownload(id) { if (id in this.downloads) { this.nodes.list.removeChild(this.downloads[id].nodes.row); delete this.downloads[id]; } }, leave: function leave(stack) { if (stack.pop) this.cleanup(); }, allowedCommands: Class.memoize(function () let (self = this) ({ get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove) })), commands: { clear: function () { services.downloadManager.cleanUp(); } }, sort: function sort() { let list = values(this.downloads).sort(function (a, b) a.compare(b)); for (let [i, download] in iter(list)) if (this.nodes.list.childNodes[i + 1] != download.nodes.row) this.nodes.list.insertBefore(download.nodes.row, this.nodes.list.childNodes[i + 1]); }, shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this), update: function update() { for (let node in values(this.nodes)) if (node.update && node.update != update) node.update(); this.updateProgress(); let event = this.document.createEvent("Events"); event.initEvent("dactyl-commandupdate", true, false); this.document.dispatchEvent(event); }, timeRemaining: Infinity, updateProgress: function updateProgress() { let downloads = values(this.downloads).toArray(); let self = Object.create(this); for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"])) this[prop] = downloads.reduce(function (acc, dl) dl[prop] + acc, 0); Download.prototype.updateProgress.call(self); let active = downloads.filter(function (dl) dl.alive).length; if (active) this.nodes.total.textContent = _("download.nActive", active); else for (let key in values(["total", "percent", "speed", "time"])) this.nodes[key].textContent = ""; if (this.shouldSort("complete", "size", "speed", "time")) this.sort(); }, observers: { "download-manager-remove-download": function (id) { if (id == null) id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)]; else id = [id.QueryInterface(Ci.nsISupportsPRUint32).data]; Array.concat(id).map(this.closure.removeDownload); this.update(); } }, onDownloadStateChange: function (state, download) { try { if (download.id in this.downloads) this.downloads[download.id].updateStatus(); else { this.addDownload(download.id); this.modules.mow.resize(false); this.nodes.list.scrollIntoView(false); } this.update(); if (this.shouldSort("active")) this.sort(); } catch (e) { util.reportError(e); } }, onProgressChange: function (webProgress, request, curProgress, maxProgress, curTotalProgress, maxTotalProgress, download) { try { if (download.id in this.downloads) this.downloads[download.id].updateProgress(); this.updateProgress(); } catch (e) { util.reportError(e); } } }); var Downloads = Module("downloads", { }, { }, { commands: function initCommands(dactyl, modules, window) { const { commands, CommandOption } = modules; commands.add(["downl[oads]", "dl"], "Display the downloads list", function (args) { let downloads = DownloadList(modules, args[0], args["-sort"]); modules.commandline.echo(downloads); }, { argCount: "?", options: [ { names: ["-sort", "-s"], description: "Sort order (see 'downloadsort')", type: CommandOption.LIST, get default() modules.options["downloadsort"], completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }), validator: function (value) modules.options.get("downloadsort").validator(value) } ] }); commands.add(["dlc[lear]"], "Clear completed downloads", function (args) { services.downloadManager.cleanUp(); }); }, options: function initOptions(dactyl, modules, window) { const { options } = modules; if (false) options.add(["downloadcolumns", "dlc"], "The columns to show in the download manager", "stringlist", "filename,state,buttons,progress,percent,time,url", { values: { buttons: "Control buttons", filename: "Target filename", percent: "Percent complete", size: "File size", speed: "Download speed", state: "The download's state", time: "Time remaining", url: "Source URL" } }); options.add(["downloadsort", "dlsort", "dls"], ":downloads sort order", "stringlist", "-active,+filename", { values: { active: "Whether download is active", complete: "Percent complete", date: "Date and time the download began", filename: "Target filename", size: "File size", speed: "Download speed", time: "Time remaining", url: "Source URL" }, completer: function (context, extra) { let seen = Set.has(Set(extra.values.map(function (val) val.substr(1)))); context.completions = iter(this.values).filter(function ([k, v]) !seen(k)) .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")], ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]]) .flatten().array; }, has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)), validator: function (value) { let seen = {}; return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1)) && !Set.add(seen, val.substr(1)), this) && value.length; } }); } }); endModule(); // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); } // vim: set fdm=marker sw=4 ts=4 et ft=javascript: