1 // Copyright (c) 2011-2012 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 defineModule("downloads", {
8 exports: ["Download", "Downloads", "downloads"],
12 lazyRequire("overlay", ["overlay"]);
14 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
16 var MAX_LOAD_TIME = 10 * 1000;
18 let prefix = "DOWNLOAD_";
19 var states = iter([v, k.slice(prefix.length).toLowerCase()]
20 for ([k, v] in Iterator(Ci.nsIDownloadManager))
21 if (k.indexOf(prefix) == 0))
24 var Download = Class("Download", {
25 init: function init(id, list) {
27 this.download = services.downloadManager.getDownload(id);
34 ["tr", { highlight: "Download", key: "row" },
35 ["td", { highlight: "DownloadTitle" },
36 ["span", { highlight: "Link" },
37 ["a", { key: "launch", href: self.target.spec, path: self.targetFile.path },
39 ["span", { highlight: "LinkInfo" },
40 self.targetFile.path]]],
41 ["td", { highlight: "DownloadState", key: "state" }],
42 ["td", { highlight: "DownloadButtons Buttons" },
43 ["a", { highlight: "Button", href: "javascript:0", key: "pause" }, _("download.action.Pause")],
44 ["a", { highlight: "Button", href: "javascript:0", key: "remove" }, _("download.action.Remove")],
45 ["a", { highlight: "Button", href: "javascript:0", key: "resume" }, _("download.action.Resume")],
46 ["a", { highlight: "Button", href: "javascript:0", key: "retry" }, _("download.action.Retry")],
47 ["a", { highlight: "Button", href: "javascript:0", key: "cancel" }, _("download.action.Cancel")],
48 ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("download.action.Delete")]],
49 ["td", { highlight: "DownloadProgress", key: "progress" },
50 ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
52 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],,
53 ["td", { highlight: "DownloadPercent", key: "percent" }],
54 ["td", { highlight: "DownloadSpeed", key: "speed" }],
55 ["td", { highlight: "DownloadTime", key: "time" }],
57 ["a", { highlight: "DownloadSource", key: "source", href: self.source.spec },
59 this.list.document, this.nodes);
61 this.nodes.launch.addEventListener("click", function (event) {
62 if (event.button == 0) {
63 event.preventDefault();
64 self.command("launch");
72 get status() states[this.state],
74 inState: function inState(states) states.indexOf(this.status) >= 0,
76 get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
78 allowedCommands: Class.Memoize(function () let (self = this) ({
79 get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
80 get delete() !this.cancel && self.targetFile.exists(),
81 get launch() self.targetFile.exists() && self.inState(["finished"]),
82 get pause() self.inState(["downloading"]),
83 get remove() self.inState(["blocked_parental", "blocked_policy",
84 "canceled", "dirty", "failed", "finished"]),
85 get resume() self.resumable && self.inState(["paused"]),
86 get retry() self.inState(["canceled", "failed"])
89 command: function command(name) {
90 util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
91 util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
93 if (Set.has(this.commands, name))
94 this.commands[name].call(this);
96 services.downloadManager[name + "Download"](this.id);
100 delete: function delete_() {
101 this.targetFile.remove(false);
104 launch: function launch() {
106 // Behavior mimics that of the builtin Download Manager.
109 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
110 this.MIMEInfo.launchWithFile(file.file);
115 services.externalProtocol.loadUrl(this.target);
119 let file = io.File(this.targetFile);
120 if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
121 this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
123 if (/^a(lways)$/i.test(resp)) {
124 prefs.set("browser.download.manager.alertOnEXEOpen", false);
127 if (/^y(es)?$/i.test(resp))
136 active: function (a, b) a.alive - b.alive,
137 complete: function (a, b) a.percentComplete - b.percentComplete,
138 date: function (a, b) a.startTime - b.startTime,
139 filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
140 size: function (a, b) a.size - b.size,
141 speed: function (a, b) a.speed - b.speed,
142 time: function (a, b) a.timeRemaining - b.timeRemaining,
143 url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
146 compare: function compare(other) values(this.list.sortOrder).map(function (order) {
147 let val = this._compare[order.substr(1)](this, other);
149 return (order[0] == "-") ? -val : val;
150 }, this).nth(util.identity, 0) || 0,
152 timeRemaining: Infinity,
154 updateProgress: function updateProgress() {
155 let self = this.__proto__;
157 if (this.amountTransferred === this.size) {
158 this.nodes.speed.textContent = "";
159 this.nodes.time.textContent = "";
162 this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
164 if (this.speed == 0 || this.size == 0)
165 this.nodes.time.textContent = _("download.unknown");
167 let seconds = (this.size - this.amountTransferred) / this.speed;
168 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
169 if (this.timeRemaining)
170 this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
172 this.nodes.time.textContent = _("download.almostDone");
176 let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
177 : _("download.unknown");
178 let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
179 this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
181 this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
184 updateStatus: function updateStatus() {
186 this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
188 this.nodes.row.setAttribute("status", this.status);
189 this.nodes.state.textContent = util.capitalize(this.status);
191 for (let node in values(this.nodes))
195 this.updateProgress();
198 Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
199 if (!(key in Download.prototype))
200 Object.defineProperty(Download.prototype, key, {
201 get: function get() this.download[key],
202 set: function set(val) this.download[key] = val,
207 var DownloadList = Class("DownloadList",
208 XPCOM([Ci.nsIDownloadProgressListener,
210 Ci.nsISupportsWeakReference]), {
211 init: function init(modules, filter, sort) {
212 this.sortOrder = sort;
213 this.modules = modules;
214 this.filter = filter && filter.toLowerCase();
221 cleanup: function cleanup() {
222 this.observe.unregister();
223 services.downloadManager.removeListener(this);
226 message: Class.Memoize(function () {
228 DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
229 ["tr", { highlight: "DownloadHead", key: "head" },
230 ["span", {}, _("title.Title")],
231 ["span", {}, _("title.Status")],
233 ["span", {}, _("title.Progress")],
235 ["span", {}, _("title.Speed")],
236 ["span", {}, _("title.Time remaining")],
237 ["span", {}, _("title.Source")]],
238 ["tr", { highlight: "Download" },
240 ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
241 ["tr", { highlight: "Download", key: "totals", active: "true" },
243 ["span", { highlight: "Title" },
244 _("title.Totals") + ":"],
246 ["span", { key: "total" }]],
248 ["td", { highlight: "DownloadButtons" },
249 ["a", { highlight: "Button", href: "javascript:0", key: "clear" }, _("download.action.Clear")]],
250 ["td", { highlight: "DownloadProgress", key: "progress" },
251 ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
253 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
254 ["td", { highlight: "DownloadPercent", key: "percent" }],
255 ["td", { highlight: "DownloadSpeed", key: "speed" }],
256 ["td", { highlight: "DownloadTime", key: "time" }],
258 this.document, this.nodes);
260 this.index = Array.indexOf(this.nodes.list.childNodes,
263 let start = Date.now();
264 for (let row in iter(services.downloadManager.DBConnection
265 .createStatement("SELECT id FROM moz_downloads"))) {
266 if (Date.now() - start > MAX_LOAD_TIME) {
267 util.dactyl.warn(_("download.givingUpAfter", (Date.now() - start) / 1000));
270 this.addDownload(row.id);
274 util.addObserver(this);
275 services.downloadManager.addListener(this);
276 return this.nodes.list;
279 addDownload: function addDownload(id) {
280 if (!(id in this.downloads)) {
281 let download = Download(id, this);
282 if (this.filter && download.displayName.indexOf(this.filter) === -1)
285 this.downloads[id] = download;
286 let index = values(this.downloads).sort(function (a, b) a.compare(b))
289 this.nodes.list.insertBefore(download.nodes.row,
290 this.nodes.list.childNodes[index + this.index + 1]);
293 removeDownload: function removeDownload(id) {
294 if (id in this.downloads) {
295 this.nodes.list.removeChild(this.downloads[id].nodes.row);
296 delete this.downloads[id];
300 leave: function leave(stack) {
305 allowedCommands: Class.Memoize(function () let (self = this) ({
306 get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
311 services.downloadManager.cleanUp();
315 sort: function sort() {
316 let list = values(this.downloads).sort(function (a, b) a.compare(b));
318 for (let [i, download] in iter(list))
319 if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
320 this.nodes.list.insertBefore(download.nodes.row,
321 this.nodes.list.childNodes[i + 1]);
324 shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
326 update: function update() {
327 for (let node in values(this.nodes))
328 if (node.update && node.update != update)
330 this.updateProgress();
332 let event = this.document.createEvent("Events");
333 event.initEvent("dactyl-commandupdate", true, false);
334 this.document.dispatchEvent(event);
337 timeRemaining: Infinity,
339 updateProgress: function updateProgress() {
340 let downloads = values(this.downloads).toArray();
341 let active = downloads.filter(function (d) d.alive);
343 let self = Object.create(this);
344 for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
345 this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
347 Download.prototype.updateProgress.call(self);
349 this.nActive = active.length;
351 this.nodes.total.textContent = _("download.nActive", active.length);
352 else for (let key in values(["total", "percent", "speed", "time"]))
353 this.nodes[key].textContent = "";
355 if (this.shouldSort("complete", "size", "speed", "time"))
360 "download-manager-remove-download": function (id) {
362 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
364 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
366 Array.concat(id).map(this.closure.removeDownload);
371 onDownloadStateChange: function (state, download) {
373 if (download.id in this.downloads)
374 this.downloads[download.id].updateStatus();
376 this.addDownload(download.id);
378 this.modules.mow.resize(false);
379 this.nodes.list.scrollIntoView(false);
383 if (this.shouldSort("active"))
391 onProgressChange: function (webProgress, request,
392 curProgress, maxProgress,
393 curTotalProgress, maxTotalProgress,
396 if (download.id in this.downloads)
397 this.downloads[download.id].updateProgress();
398 this.updateProgress();
406 var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
408 services.downloadManager.addListener(this);
411 cleanup: function destroy() {
412 services.downloadManager.removeListener(this);
415 onDownloadStateChange: function (state, download) {
416 if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
417 let url = download.source.spec;
418 let title = download.displayName;
419 let file = download.targetFile.path;
420 let size = download.size;
423 overlay.modules.forEach(function (modules) {
424 modules.dactyl.echomsg({ domains: [util.getHost(url)], message: _("io.downloadFinished", title, file) },
425 1, modules.commandline.ACTIVE_WINDOW);
426 modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
432 commands: function initCommands(dactyl, modules, window) {
433 const { commands, CommandOption } = modules;
435 commands.add(["downl[oads]", "dl"],
436 "Display the downloads list",
438 let downloads = DownloadList(modules, args[0], args["-sort"]);
439 modules.commandline.echo(downloads);
445 names: ["-sort", "-s"],
446 description: "Sort order (see 'downloadsort')",
447 type: CommandOption.LIST,
448 get default() modules.options["downloadsort"],
449 completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
450 validator: function (value) modules.options.get("downloadsort").validator(value)
455 commands.add(["dlc[lear]"],
456 "Clear completed downloads",
457 function (args) { services.downloadManager.cleanUp(); });
459 options: function initOptions(dactyl, modules, window) {
460 const { options } = modules;
463 options.add(["downloadcolumns", "dlc"],
464 "The columns to show in the download manager",
465 "stringlist", "filename,state,buttons,progress,percent,time,url",
468 buttons: "Control buttons",
469 filename: "Target filename",
470 percent: "Percent complete",
472 speed: "Download speed",
473 state: "The download's state",
474 time: "Time remaining",
479 options.add(["downloadsort", "dlsort", "dls"],
480 ":downloads sort order",
481 "stringlist", "-active,+filename",
484 active: "Whether download is active",
485 complete: "Percent complete",
486 date: "Date and time the download began",
487 filename: "Target filename",
489 speed: "Download speed",
490 time: "Time remaining",
494 completer: function (context, extra) {
495 let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
497 context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
498 .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
499 ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
503 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
505 validator: function (value) {
507 return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
508 && !Set.add(seen, val.substr(1)),
509 this) && value.length;
517 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
519 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: