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) {
26 this.download = services.downloadManager.getDownload(id);
33 ["tr", { highlight: "Download", key: "row" },
34 ["td", { highlight: "DownloadTitle" },
35 ["span", { highlight: "Link" },
36 ["a", { key: "launch", href: this.target.spec, path: this.targetFile.path },
38 ["span", { highlight: "LinkInfo" },
39 this.targetFile.path]]],
40 ["td", { highlight: "DownloadState", key: "state" }],
41 ["td", { highlight: "DownloadButtons Buttons" },
42 ["a", { highlight: "Button", href: "javascript:0", key: "pause" }, _("download.action.Pause")],
43 ["a", { highlight: "Button", href: "javascript:0", key: "remove" }, _("download.action.Remove")],
44 ["a", { highlight: "Button", href: "javascript:0", key: "resume" }, _("download.action.Resume")],
45 ["a", { highlight: "Button", href: "javascript:0", key: "retry" }, _("download.action.Retry")],
46 ["a", { highlight: "Button", href: "javascript:0", key: "cancel" }, _("download.action.Cancel")],
47 ["a", { highlight: "Button", href: "javascript:0", key: "delete" }, _("download.action.Delete")]],
48 ["td", { highlight: "DownloadProgress", key: "progress" },
49 ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
51 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],,
52 ["td", { highlight: "DownloadPercent", key: "percent" }],
53 ["td", { highlight: "DownloadSpeed", key: "speed" }],
54 ["td", { highlight: "DownloadTime", key: "time" }],
56 ["a", { highlight: "DownloadSource", key: "source", href: this.source.spec },
58 this.list.document, this.nodes);
60 this.nodes.launch.addEventListener("click", (event) => {
61 if (event.button == 0) {
62 event.preventDefault();
63 this.command("launch");
71 get status() states[this.state],
73 inState: function inState(states) states.indexOf(this.status) >= 0,
75 get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
77 allowedCommands: Class.Memoize(function () let (self = this) ({
78 get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
79 get delete() !this.cancel && self.targetFile.exists(),
80 get launch() self.targetFile.exists() && self.inState(["finished"]),
81 get pause() self.inState(["downloading"]),
82 get remove() self.inState(["blocked_parental", "blocked_policy",
83 "canceled", "dirty", "failed", "finished"]),
84 get resume() self.resumable && self.inState(["paused"]),
85 get retry() self.inState(["canceled", "failed"])
88 command: function command(name) {
89 util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
90 util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
92 if (Set.has(this.commands, name))
93 this.commands[name].call(this);
95 services.downloadManager[name + "Download"](this.id);
99 delete: function delete_() {
100 this.targetFile.remove(false);
103 launch: function launch() {
104 // Behavior mimics that of the builtin Download Manager.
107 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
108 this.MIMEInfo.launchWithFile(file.file);
113 services.externalProtocol.loadUrl(this.target);
117 let file = io.File(this.targetFile);
118 if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
119 this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
121 if (/^a(lways)$/i.test(resp)) {
122 prefs.set("browser.download.manager.alertOnEXEOpen", false);
125 if (/^y(es)?$/i.test(resp))
134 active: function (a, b) a.alive - b.alive,
135 complete: function (a, b) a.percentComplete - b.percentComplete,
136 date: function (a, b) a.startTime - b.startTime,
137 filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
138 size: function (a, b) a.size - b.size,
139 speed: function (a, b) a.speed - b.speed,
140 time: function (a, b) a.timeRemaining - b.timeRemaining,
141 url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
144 compare: function compare(other) values(this.list.sortOrder).map(function (order) {
145 let val = this._compare[order.substr(1)](this, other);
147 return (order[0] == "-") ? -val : val;
148 }, this).nth(util.identity, 0) || 0,
150 timeRemaining: Infinity,
152 updateProgress: function updateProgress() {
153 let self = this.__proto__;
155 if (this.amountTransferred === this.size) {
156 this.nodes.speed.textContent = "";
157 this.nodes.time.textContent = "";
160 this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
162 if (this.speed == 0 || this.size == 0)
163 this.nodes.time.textContent = _("download.unknown");
165 let seconds = (this.size - this.amountTransferred) / this.speed;
166 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
167 if (this.timeRemaining)
168 this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
170 this.nodes.time.textContent = _("download.almostDone");
174 let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
175 : _("download.unknown");
176 let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
177 this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
179 this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
182 updateStatus: function updateStatus() {
184 this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
186 this.nodes.row.setAttribute("status", this.status);
187 this.nodes.state.textContent = util.capitalize(this.status);
189 for (let node in values(this.nodes))
193 this.updateProgress();
196 Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
197 if (!(key in Download.prototype))
198 Object.defineProperty(Download.prototype, key, {
199 get: function get() this.download[key],
200 set: function set(val) this.download[key] = val,
205 var DownloadList = Class("DownloadList",
206 XPCOM([Ci.nsIDownloadProgressListener,
208 Ci.nsISupportsWeakReference]), {
209 init: function init(modules, filter, sort) {
210 this.sortOrder = sort;
211 this.modules = modules;
212 this.filter = filter && filter.toLowerCase();
219 cleanup: function cleanup() {
220 this.observe.unregister();
221 services.downloadManager.removeListener(this);
224 message: Class.Memoize(function () {
226 DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
227 ["tr", { highlight: "DownloadHead", key: "head" },
228 ["span", {}, _("title.Title")],
229 ["span", {}, _("title.Status")],
231 ["span", {}, _("title.Progress")],
233 ["span", {}, _("title.Speed")],
234 ["span", {}, _("title.Time remaining")],
235 ["span", {}, _("title.Source")]],
236 ["tr", { highlight: "Download" },
238 ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
239 ["tr", { highlight: "Download", key: "totals", active: "true" },
241 ["span", { highlight: "Title" },
242 _("title.Totals") + ":"],
244 ["span", { key: "total" }]],
246 ["td", { highlight: "DownloadButtons" },
247 ["a", { highlight: "Button", href: "javascript:0", key: "clear" }, _("download.action.Clear")]],
248 ["td", { highlight: "DownloadProgress", key: "progress" },
249 ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
251 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
252 ["td", { highlight: "DownloadPercent", key: "percent" }],
253 ["td", { highlight: "DownloadSpeed", key: "speed" }],
254 ["td", { highlight: "DownloadTime", key: "time" }],
256 this.document, this.nodes);
258 this.index = Array.indexOf(this.nodes.list.childNodes,
261 let start = Date.now();
262 for (let row in iter(services.downloadManager.DBConnection
263 .createStatement("SELECT id FROM moz_downloads"))) {
264 if (Date.now() - start > MAX_LOAD_TIME) {
265 util.dactyl.warn(_("download.givingUpAfter", (Date.now() - start) / 1000));
268 this.addDownload(row.id);
272 util.addObserver(this);
273 services.downloadManager.addListener(this);
274 return this.nodes.list;
277 addDownload: function addDownload(id) {
278 if (!(id in this.downloads)) {
279 let download = Download(id, this);
280 if (this.filter && download.displayName.indexOf(this.filter) === -1)
283 this.downloads[id] = download;
284 let index = values(this.downloads).sort(function (a, b) a.compare(b))
287 this.nodes.list.insertBefore(download.nodes.row,
288 this.nodes.list.childNodes[index + this.index + 1]);
291 removeDownload: function removeDownload(id) {
292 if (id in this.downloads) {
293 this.nodes.list.removeChild(this.downloads[id].nodes.row);
294 delete this.downloads[id];
298 leave: function leave(stack) {
303 allowedCommands: Class.Memoize(function () let (self = this) ({
304 get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
309 services.downloadManager.cleanUp();
313 sort: function sort() {
314 let list = values(this.downloads).sort(function (a, b) a.compare(b));
316 for (let [i, download] in iter(list))
317 if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
318 this.nodes.list.insertBefore(download.nodes.row,
319 this.nodes.list.childNodes[i + 1]);
322 shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
324 update: function update() {
325 for (let node in values(this.nodes))
326 if (node.update && node.update != update)
328 this.updateProgress();
330 let event = this.document.createEvent("Events");
331 event.initEvent("dactyl-commandupdate", true, false);
332 this.document.dispatchEvent(event);
335 timeRemaining: Infinity,
337 updateProgress: function updateProgress() {
338 let downloads = values(this.downloads).toArray();
339 let active = downloads.filter(function (d) d.alive);
341 let self = Object.create(this);
342 for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
343 this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
345 Download.prototype.updateProgress.call(self);
347 this.nActive = active.length;
349 this.nodes.total.textContent = _("download.nActive", active.length);
350 else for (let key in values(["total", "percent", "speed", "time"]))
351 this.nodes[key].textContent = "";
353 if (this.shouldSort("complete", "size", "speed", "time"))
358 "download-manager-remove-download": function (id) {
360 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
362 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
364 Array.concat(id).map(this.closure.removeDownload);
369 onDownloadStateChange: function (state, download) {
371 if (download.id in this.downloads)
372 this.downloads[download.id].updateStatus();
374 this.addDownload(download.id);
376 this.modules.mow.resize(false);
377 this.nodes.list.scrollIntoView(false);
381 if (this.shouldSort("active"))
389 onProgressChange: function (webProgress, request,
390 curProgress, maxProgress,
391 curTotalProgress, maxTotalProgress,
394 if (download.id in this.downloads)
395 this.downloads[download.id].updateProgress();
396 this.updateProgress();
404 var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
406 services.downloadManager.addListener(this);
409 cleanup: function destroy() {
410 services.downloadManager.removeListener(this);
413 onDownloadStateChange: function (state, download) {
414 if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
415 let url = download.source.spec;
416 let title = download.displayName;
417 let file = download.targetFile.path;
418 let size = download.size;
420 overlay.modules.forEach(function (modules) {
421 modules.dactyl.echomsg({ domains: [util.getHost(url)], message: _("io.downloadFinished", title, file) },
422 1, modules.commandline.ACTIVE_WINDOW);
423 modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
429 commands: function initCommands(dactyl, modules, window) {
430 const { commands, CommandOption } = modules;
432 commands.add(["downl[oads]", "dl"],
433 "Display the downloads list",
435 let downloads = DownloadList(modules, args[0], args["-sort"]);
436 modules.commandline.echo(downloads);
442 names: ["-sort", "-s"],
443 description: "Sort order (see 'downloadsort')",
444 type: CommandOption.LIST,
445 get default() modules.options["downloadsort"],
446 completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
447 validator: function (value) modules.options.get("downloadsort").validator(value)
452 commands.add(["dlc[lear]"],
453 "Clear completed downloads",
454 function (args) { services.downloadManager.cleanUp(); });
456 options: function initOptions(dactyl, modules, window) {
457 const { options } = modules;
460 options.add(["downloadcolumns", "dlc"],
461 "The columns to show in the download manager",
462 "stringlist", "filename,state,buttons,progress,percent,time,url",
465 buttons: "Control buttons",
466 filename: "Target filename",
467 percent: "Percent complete",
469 speed: "Download speed",
470 state: "The download's state",
471 time: "Time remaining",
476 options.add(["downloadsort", "dlsort", "dls"],
477 ":downloads sort order",
478 "stringlist", "-active,+filename",
481 active: "Whether download is active",
482 complete: "Percent complete",
483 date: "Date and time the download began",
484 filename: "Target filename",
486 speed: "Download speed",
487 time: "Time remaining",
491 completer: function (context, extra) {
492 let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
494 context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
495 .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
496 ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
500 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
502 validator: function (value) {
504 return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
505 && !Set.add(seen, val.substr(1)),
506 this) && value.length;
514 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
516 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: