1 // Copyright (c) 2011 by Kris Maglione <maglione.k@gmail.com>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("downloads", {
9 exports: ["Download", "Downloads", "downloads"]
12 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
14 let prefix = "DOWNLOAD_";
15 var states = iter([v, k.slice(prefix.length).toLowerCase()]
16 for ([k, v] in Iterator(Ci.nsIDownloadManager))
17 if (k.indexOf(prefix) == 0))
20 var Download = Class("Download", {
21 init: function init(id, list) {
23 this.download = services.downloadManager.getDownload(id);
30 <tr highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
31 <td highlight="DownloadTitle">
32 <span highlight="Link">
34 href={self.target.spec} path={self.targetFile.path}>{self.displayName}</a>
35 <span highlight="LinkInfo">{self.targetFile.path}</span>
38 <td highlight="DownloadState" key="state"/>
39 <td highlight="DownloadButtons Buttons">
40 <a highlight="Button" href="javascript:0" key="pause">{_("download.action.Pause")}</a>
41 <a highlight="Button" href="javascript:0" key="remove">{_("download.action.Remove")}</a>
42 <a highlight="Button" href="javascript:0" key="resume">{_("download.action.Resume")}</a>
43 <a highlight="Button" href="javascript:0" key="retry">{_("download.action.Retry")}</a>
44 <a highlight="Button" href="javascript:0" key="cancel">{_("download.action.Cancel")}</a>
45 <a highlight="Button" href="javascript:0" key="delete">{_("download.action.Delete")}</a>
47 <td highlight="DownloadProgress" key="progress">
48 <span highlight="DownloadProgressHave" key="progressHave"
49 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
51 <td highlight="DownloadPercent" key="percent"/>
52 <td highlight="DownloadSpeed" key="speed"/>
53 <td highlight="DownloadTime" key="time"/>
54 <td><a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a></td>
56 this.list.document, this.nodes);
58 this.nodes.launch.addEventListener("click", function (event) {
59 if (event.button == 0) {
60 event.preventDefault();
61 self.command("launch");
69 get status() states[this.state],
71 inState: function inState(states) states.indexOf(this.status) >= 0,
73 get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
75 allowedCommands: Class.Memoize(function () let (self = this) ({
76 get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
77 get delete() !this.cancel && self.targetFile.exists(),
78 get launch() self.targetFile.exists() && self.inState(["finished"]),
79 get pause() self.inState(["downloading"]),
80 get remove() self.inState(["blocked_parental", "blocked_policy",
81 "canceled", "dirty", "failed", "finished"]),
82 get resume() self.resumable && self.inState(["paused"]),
83 get retry() self.inState(["canceled", "failed"])
86 command: function command(name) {
87 util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
88 util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
90 if (Set.has(this.commands, name))
91 this.commands[name].call(this);
93 services.downloadManager[name + "Download"](this.id);
97 delete: function delete_() {
98 this.targetFile.remove(false);
101 launch: function launch() {
103 // Behavior mimics that of the builtin Download Manager.
106 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
107 this.MIMEInfo.launchWithFile(file);
112 services.externalProtocol.loadUrl(this.target);
116 let file = io.File(this.targetFile);
117 if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
118 this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
120 if (/^a(lways)$/i.test(resp)) {
121 prefs.set("browser.download.manager.alertOnEXEOpen", false);
124 if (/^y(es)?$/i.test(resp))
133 active: function (a, b) a.alive - b.alive,
134 complete: function (a, b) a.percentComplete - b.percentComplete,
135 date: function (a, b) a.startTime - b.startTime,
136 filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
137 size: function (a, b) a.size - b.size,
138 speed: function (a, b) a.speed - b.speed,
139 time: function (a, b) a.timeRemaining - b.timeRemaining,
140 url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
143 compare: function compare(other) values(this.list.sortOrder).map(function (order) {
144 let val = this._compare[order.substr(1)](this, other);
146 return (order[0] == "-") ? -val : val;
147 }, this).nth(util.identity, 0) || 0,
149 timeRemaining: Infinity,
151 updateProgress: function updateProgress() {
152 let self = this.__proto__;
154 if (this.amountTransferred === this.size) {
155 this.nodes.speed.textContent = "";
156 this.nodes.time.textContent = "";
159 this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
161 if (this.speed == 0 || this.size == 0)
162 this.nodes.time.textContent = _("download.unknown");
164 let seconds = (this.size - this.amountTransferred) / this.speed;
165 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
166 if (this.timeRemaining)
167 this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
169 this.nodes.time.textContent = _("download.almostDone");
173 let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
174 : _("download.unknown");
175 let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
176 this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
178 this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
181 updateStatus: function updateStatus() {
183 this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
185 this.nodes.row.setAttribute("status", this.status);
186 this.nodes.state.textContent = util.capitalize(this.status);
188 for (let node in values(this.nodes))
192 this.updateProgress();
195 Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
196 if (!(key in Download.prototype))
197 Object.defineProperty(Download.prototype, key, {
198 get: function get() this.download[key],
199 set: function set(val) this.download[key] = val,
204 var DownloadList = Class("DownloadList",
205 XPCOM([Ci.nsIDownloadProgressListener,
207 Ci.nsISupportsWeakReference]), {
208 init: function init(modules, filter, sort) {
209 this.sortOrder = sort;
210 this.modules = modules;
211 this.filter = filter && filter.toLowerCase();
218 cleanup: function cleanup() {
219 this.observe.unregister();
220 services.downloadManager.removeListener(this);
223 message: Class.Memoize(function () {
225 util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
226 <tr highlight="DownloadHead">
227 <span>{_("title.Title")}</span>
228 <span>{_("title.Status")}</span>
230 <span>{_("title.Progress")}</span>
232 <span>{_("title.Speed")}</span>
233 <span>{_("title.Time remaining")}</span>
234 <span>{_("title.Source")}</span>
236 <tr highlight="Download"><span><div style="min-height: 1ex; /* FIXME */"/></span></tr>
237 <tr highlight="Download" key="totals" active="true">
238 <td><span highlight="Title">{_("title.Totals")}:</span> <span key="total"/></td>
240 <td highlight="DownloadButtons">
241 <a highlight="Button" href="javascript:0" key="clear">{_("download.action.Clear")}</a>
243 <td highlight="DownloadProgress" key="progress">
244 <span highlight="DownloadProgressHave" key="progressHave"
245 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
247 <td highlight="DownloadPercent" key="percent"/>
248 <td highlight="DownloadSpeed" key="speed"/>
249 <td highlight="DownloadTime" key="time"/>
252 </table>, this.document, this.nodes);
254 for (let row in iter(services.downloadManager.DBConnection
255 .createStatement("SELECT id FROM moz_downloads")))
256 this.addDownload(row.id);
259 util.addObserver(this);
260 services.downloadManager.addListener(this);
261 return this.nodes.list;
264 addDownload: function addDownload(id) {
265 if (!(id in this.downloads)) {
266 let download = Download(id, this);
267 if (this.filter && download.displayName.indexOf(this.filter) === -1)
270 this.downloads[id] = download;
271 let index = values(this.downloads).sort(function (a, b) a.compare(b))
274 this.nodes.list.insertBefore(download.nodes.row,
275 this.nodes.list.childNodes[index + 1]);
278 removeDownload: function removeDownload(id) {
279 if (id in this.downloads) {
280 this.nodes.list.removeChild(this.downloads[id].nodes.row);
281 delete this.downloads[id];
285 leave: function leave(stack) {
290 allowedCommands: Class.Memoize(function () let (self = this) ({
291 get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
296 services.downloadManager.cleanUp();
300 sort: function sort() {
301 let list = values(this.downloads).sort(function (a, b) a.compare(b));
303 for (let [i, download] in iter(list))
304 if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
305 this.nodes.list.insertBefore(download.nodes.row,
306 this.nodes.list.childNodes[i + 1]);
309 shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
311 update: function update() {
312 for (let node in values(this.nodes))
313 if (node.update && node.update != update)
315 this.updateProgress();
317 let event = this.document.createEvent("Events");
318 event.initEvent("dactyl-commandupdate", true, false);
319 this.document.dispatchEvent(event);
322 timeRemaining: Infinity,
324 updateProgress: function updateProgress() {
325 let downloads = values(this.downloads).toArray();
326 let active = downloads.filter(function (d) d.alive);
328 let self = Object.create(this);
329 for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
330 this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
332 Download.prototype.updateProgress.call(self);
334 this.nActive = active.length;
336 this.nodes.total.textContent = _("download.nActive", active.length);
337 else for (let key in values(["total", "percent", "speed", "time"]))
338 this.nodes[key].textContent = "";
340 if (this.shouldSort("complete", "size", "speed", "time"))
345 "download-manager-remove-download": function (id) {
347 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
349 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
351 Array.concat(id).map(this.closure.removeDownload);
356 onDownloadStateChange: function (state, download) {
358 if (download.id in this.downloads)
359 this.downloads[download.id].updateStatus();
361 this.addDownload(download.id);
363 this.modules.mow.resize(false);
364 this.nodes.list.scrollIntoView(false);
368 if (this.shouldSort("active"))
376 onProgressChange: function (webProgress, request,
377 curProgress, maxProgress,
378 curTotalProgress, maxTotalProgress,
381 if (download.id in this.downloads)
382 this.downloads[download.id].updateProgress();
383 this.updateProgress();
391 var Downloads = Module("downloads", {
394 commands: function initCommands(dactyl, modules, window) {
395 const { commands, CommandOption } = modules;
397 commands.add(["downl[oads]", "dl"],
398 "Display the downloads list",
400 let downloads = DownloadList(modules, args[0], args["-sort"]);
401 modules.commandline.echo(downloads);
407 names: ["-sort", "-s"],
408 description: "Sort order (see 'downloadsort')",
409 type: CommandOption.LIST,
410 get default() modules.options["downloadsort"],
411 completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
412 validator: function (value) modules.options.get("downloadsort").validator(value)
417 commands.add(["dlc[lear]"],
418 "Clear completed downloads",
419 function (args) { services.downloadManager.cleanUp(); });
421 options: function initOptions(dactyl, modules, window) {
422 const { options } = modules;
425 options.add(["downloadcolumns", "dlc"],
426 "The columns to show in the download manager",
427 "stringlist", "filename,state,buttons,progress,percent,time,url",
430 buttons: "Control buttons",
431 filename: "Target filename",
432 percent: "Percent complete",
434 speed: "Download speed",
435 state: "The download's state",
436 time: "Time remaining",
441 options.add(["downloadsort", "dlsort", "dls"],
442 ":downloads sort order",
443 "stringlist", "-active,+filename",
446 active: "Whether download is active",
447 complete: "Percent complete",
448 date: "Date and time the download began",
449 filename: "Target filename",
451 speed: "Download speed",
452 time: "Time remaining",
456 completer: function (context, extra) {
457 let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
459 context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
460 .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
461 ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
465 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
467 validator: function (value) {
469 return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
470 && !Set.add(seen, val.substr(1)),
471 this) && value.length;
479 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
481 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: