]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/downloads.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / downloads.jsm
1 // Copyright (c) 2011-2012 Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 defineModule("downloads", {
8     exports: ["Download", "Downloads", "downloads"],
9     require: ["util"]
10 });
11
12 lazyRequire("overlay", ["overlay"]);
13
14 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
15
16 var MAX_LOAD_TIME = 10 * 1000;
17
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))
22                 .toObject();
23
24 var Download = Class("Download", {
25     init: function init(id, list) {
26         let self = this;
27         this.download = services.downloadManager.getDownload(id);
28         this.list = list;
29
30         this.nodes = {
31             commandTarget: self
32         };
33         DOM.fromJSON(
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 },
38                             self.displayName],
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" }],
51                     "/",
52                     ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],,
53                 ["td", { highlight: "DownloadPercent", key: "percent" }],
54                 ["td", { highlight: "DownloadSpeed", key: "speed" }],
55                 ["td", { highlight: "DownloadTime", key: "time" }],
56                 ["td", {},
57                     ["a", { highlight: "DownloadSource", key: "source", href: self.source.spec },
58                         self.source.spec]]],
59             this.list.document, this.nodes);
60
61         this.nodes.launch.addEventListener("click", function (event) {
62             if (event.button == 0) {
63                 event.preventDefault();
64                 self.command("launch");
65             }
66         }, false);
67
68         self.updateStatus();
69         return self;
70     },
71
72     get status() states[this.state],
73
74     inState: function inState(states) states.indexOf(this.status) >= 0,
75
76     get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
77
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"])
87     })),
88
89     command: function command(name) {
90         util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
91         util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
92
93         if (Set.has(this.commands, name))
94             this.commands[name].call(this);
95         else
96             services.downloadManager[name + "Download"](this.id);
97     },
98
99     commands: {
100         delete: function delete_() {
101             this.targetFile.remove(false);
102             this.updateStatus();
103         },
104         launch: function launch() {
105             let self = this;
106             // Behavior mimics that of the builtin Download Manager.
107             function action() {
108                 try {
109                     if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
110                         this.MIMEInfo.launchWithFile(file.file);
111                     else
112                         file.launch();
113                 }
114                 catch (e) {
115                     services.externalProtocol.loadUrl(this.target);
116                 }
117             }
118
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") + " ",
122                     function (resp) {
123                         if (/^a(lways)$/i.test(resp)) {
124                             prefs.set("browser.download.manager.alertOnEXEOpen", false);
125                             resp = "yes";
126                         }
127                         if (/^y(es)?$/i.test(resp))
128                             action.call(self);
129                     });
130             else
131                 action.call(this);
132         }
133     },
134
135     _compare: {
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)
144     },
145
146     compare: function compare(other) values(this.list.sortOrder).map(function (order) {
147         let val = this._compare[order.substr(1)](this, other);
148
149         return (order[0] == "-") ? -val : val;
150     }, this).nth(util.identity, 0) || 0,
151
152     timeRemaining: Infinity,
153
154     updateProgress: function updateProgress() {
155         let self = this.__proto__;
156
157         if (this.amountTransferred === this.size) {
158             this.nodes.speed.textContent = "";
159             this.nodes.time.textContent = "";
160         }
161         else {
162             this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
163
164             if (this.speed == 0 || this.size == 0)
165                 this.nodes.time.textContent = _("download.unknown");
166             else {
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);
171                 else
172                     this.nodes.time.textContent = _("download.almostDone");
173             }
174         }
175
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, "");
180
181         this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
182     },
183
184     updateStatus: function updateStatus() {
185
186         this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
187
188         this.nodes.row.setAttribute("status", this.status);
189         this.nodes.state.textContent = util.capitalize(this.status);
190
191         for (let node in values(this.nodes))
192             if (node.update)
193                 node.update();
194
195         this.updateProgress();
196     }
197 });
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,
203             configurable: true
204         });
205 });
206
207 var DownloadList = Class("DownloadList",
208                          XPCOM([Ci.nsIDownloadProgressListener,
209                                 Ci.nsIObserver,
210                                 Ci.nsISupportsWeakReference]), {
211     init: function init(modules, filter, sort) {
212         this.sortOrder = sort;
213         this.modules = modules;
214         this.filter = filter && filter.toLowerCase();
215         this.nodes = {
216             commandTarget: this
217         };
218         this.downloads = {};
219     },
220
221     cleanup: function cleanup() {
222         this.observe.unregister();
223         services.downloadManager.removeListener(this);
224     },
225
226     message: Class.Memoize(function () {
227
228         DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
229                         ["tr", { highlight: "DownloadHead", key: "head" },
230                             ["span", {}, _("title.Title")],
231                             ["span", {}, _("title.Status")],
232                             ["span"],
233                             ["span", {}, _("title.Progress")],
234                             ["span"],
235                             ["span", {}, _("title.Speed")],
236                             ["span", {}, _("title.Time remaining")],
237                             ["span", {}, _("title.Source")]],
238                         ["tr", { highlight: "Download" },
239                             ["span", {},
240                                 ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
241                         ["tr", { highlight: "Download", key: "totals", active: "true" },
242                             ["td", {},
243                                 ["span", { highlight: "Title" },
244                                     _("title.Totals") + ":"],
245                                 " ",
246                                 ["span", { key: "total" }]],
247                             ["td"],
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" }],
252                                 "/",
253                                 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
254                             ["td", { highlight: "DownloadPercent", key: "percent" }],
255                             ["td", { highlight: "DownloadSpeed", key: "speed" }],
256                             ["td", { highlight: "DownloadTime", key: "time" }],
257                             ["td"]]],
258                       this.document, this.nodes);
259
260         this.index = Array.indexOf(this.nodes.list.childNodes,
261                                    this.nodes.head);
262
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));
268                 break;
269             }
270             this.addDownload(row.id);
271         }
272         this.update();
273
274         util.addObserver(this);
275         services.downloadManager.addListener(this);
276         return this.nodes.list;
277     }),
278
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)
283                 return;
284
285             this.downloads[id] = download;
286             let index = values(this.downloads).sort(function (a, b) a.compare(b))
287                                               .indexOf(download);
288
289             this.nodes.list.insertBefore(download.nodes.row,
290                                          this.nodes.list.childNodes[index + this.index + 1]);
291         }
292     },
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];
297         }
298     },
299
300     leave: function leave(stack) {
301         if (stack.pop)
302             this.cleanup();
303     },
304
305     allowedCommands: Class.Memoize(function () let (self = this) ({
306         get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
307     })),
308
309     commands: {
310         clear: function () {
311             services.downloadManager.cleanUp();
312         }
313     },
314
315     sort: function sort() {
316         let list = values(this.downloads).sort(function (a, b) a.compare(b));
317
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]);
322     },
323
324     shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
325
326     update: function update() {
327         for (let node in values(this.nodes))
328             if (node.update && node.update != update)
329                 node.update();
330         this.updateProgress();
331
332         let event = this.document.createEvent("Events");
333         event.initEvent("dactyl-commandupdate", true, false);
334         this.document.dispatchEvent(event);
335     },
336
337     timeRemaining: Infinity,
338
339     updateProgress: function updateProgress() {
340         let downloads = values(this.downloads).toArray();
341         let active    = downloads.filter(function (d) d.alive);
342
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);
346
347         Download.prototype.updateProgress.call(self);
348
349         this.nActive = active.length;
350         if (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 = "";
354
355         if (this.shouldSort("complete", "size", "speed", "time"))
356             this.sort();
357     },
358
359     observers: {
360         "download-manager-remove-download": function (id) {
361             if (id == null)
362                 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
363             else
364                 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
365
366             Array.concat(id).map(this.closure.removeDownload);
367             this.update();
368         }
369     },
370
371     onDownloadStateChange: function (state, download) {
372         try {
373             if (download.id in this.downloads)
374                 this.downloads[download.id].updateStatus();
375             else {
376                 this.addDownload(download.id);
377
378                 this.modules.mow.resize(false);
379                 this.nodes.list.scrollIntoView(false);
380             }
381             this.update();
382
383             if (this.shouldSort("active"))
384                 this.sort();
385         }
386         catch (e) {
387             util.reportError(e);
388         }
389     },
390
391     onProgressChange: function (webProgress, request,
392                                 curProgress, maxProgress,
393                                 curTotalProgress, maxTotalProgress,
394                                 download) {
395         try {
396             if (download.id in this.downloads)
397                 this.downloads[download.id].updateProgress();
398             this.updateProgress();
399         }
400         catch (e) {
401             util.reportError(e);
402         }
403     }
404 });
405
406 var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
407     init: function () {
408         services.downloadManager.addListener(this);
409     },
410
411     cleanup: function destroy() {
412         services.downloadManager.removeListener(this);
413     },
414
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;
421
422
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 });
427             });
428         }
429     }
430 }, {
431 }, {
432     commands: function initCommands(dactyl, modules, window) {
433         const { commands, CommandOption } = modules;
434
435         commands.add(["downl[oads]", "dl"],
436             "Display the downloads list",
437             function (args) {
438                 let downloads = DownloadList(modules, args[0], args["-sort"]);
439                 modules.commandline.echo(downloads);
440             },
441             {
442                 argCount: "?",
443                 options: [
444                     {
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)
451                     }
452                 ]
453             });
454
455         commands.add(["dlc[lear]"],
456             "Clear completed downloads",
457             function (args) { services.downloadManager.cleanUp(); });
458     },
459     options: function initOptions(dactyl, modules, window) {
460         const { options } = modules;
461
462         if (false)
463         options.add(["downloadcolumns", "dlc"],
464             "The columns to show in the download manager",
465             "stringlist", "filename,state,buttons,progress,percent,time,url",
466             {
467                 values: {
468                     buttons:    "Control buttons",
469                     filename:   "Target filename",
470                     percent:    "Percent complete",
471                     size:       "File size",
472                     speed:      "Download speed",
473                     state:      "The download's state",
474                     time:       "Time remaining",
475                     url:        "Source URL"
476                 }
477             });
478
479         options.add(["downloadsort", "dlsort", "dls"],
480             ":downloads sort order",
481             "stringlist", "-active,+filename",
482             {
483                 values: {
484                     active:     "Whether download is active",
485                     complete:   "Percent complete",
486                     date:       "Date and time the download began",
487                     filename:   "Target filename",
488                     size:       "File size",
489                     speed:      "Download speed",
490                     time:       "Time remaining",
491                     url:        "Source URL"
492                 },
493
494                 completer: function (context, extra) {
495                     let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
496
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("")]])
500                                                            .flatten().array;
501                 },
502
503                 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
504
505                 validator: function (value) {
506                     let seen = {};
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;
510                 }
511             });
512     }
513 });
514
515 endModule();
516
517 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
518
519 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: