]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/downloads.jsm
e4d2f11f7981dfd9eb87406718cde8875ca61669
[dactyl.git] / common / modules / downloads.jsm
1 // Copyright (c) 2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("downloads", {
9     exports: ["Download", "Downloads", "downloads"]
10 }, this);
11
12 this.lazyRequire("overlay", ["overlay"]);
13
14 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
15
16 let prefix = "DOWNLOAD_";
17 var states = iter([v, k.slice(prefix.length).toLowerCase()]
18                   for ([k, v] in Iterator(Ci.nsIDownloadManager))
19                   if (k.indexOf(prefix) == 0))
20                 .toObject();
21
22 var Download = Class("Download", {
23     init: function init(id, list) {
24         let self = this;
25         this.download = services.downloadManager.getDownload(id);
26         this.list = list;
27
28         this.nodes = {
29             commandTarget: self
30         };
31         XML.ignoreWhitespace = true;
32         XML.prettyPrinting = false;
33         util.xmlToDom(
34             <tr highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
35                 <td highlight="DownloadTitle">
36                     <span highlight="Link">
37                         <a key="launch"
38                            href={self.target.spec} path={self.targetFile.path}>{self.displayName}</a>
39                         <span highlight="LinkInfo">{self.targetFile.path}</span>
40                     </span>
41                 </td>
42                 <td highlight="DownloadState" key="state"/>
43                 <td highlight="DownloadButtons Buttons">
44                     <a highlight="Button" href="javascript:0" key="pause">{_("download.action.Pause")}</a>
45                     <a highlight="Button" href="javascript:0" key="remove">{_("download.action.Remove")}</a>
46                     <a highlight="Button" href="javascript:0" key="resume">{_("download.action.Resume")}</a>
47                     <a highlight="Button" href="javascript:0" key="retry">{_("download.action.Retry")}</a>
48                     <a highlight="Button" href="javascript:0" key="cancel">{_("download.action.Cancel")}</a>
49                     <a highlight="Button" href="javascript:0" key="delete">{_("download.action.Delete")}</a>
50                 </td>
51                 <td highlight="DownloadProgress" key="progress">
52                     <span highlight="DownloadProgressHave" key="progressHave"
53                     />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
54                 </td>
55                 <td highlight="DownloadPercent" key="percent"/>
56                 <td highlight="DownloadSpeed" key="speed"/>
57                 <td highlight="DownloadTime" key="time"/>
58                 <td><a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a></td>
59             </tr>,
60             this.list.document, this.nodes);
61
62         this.nodes.launch.addEventListener("click", function (event) {
63             if (event.button == 0) {
64                 event.preventDefault();
65                 self.command("launch");
66             }
67         }, false);
68
69         self.updateStatus();
70         return self;
71     },
72
73     get status() states[this.state],
74
75     inState: function inState(states) states.indexOf(this.status) >= 0,
76
77     get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
78
79     allowedCommands: Class.Memoize(function () let (self = this) ({
80         get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
81         get delete() !this.cancel && self.targetFile.exists(),
82         get launch() self.targetFile.exists() && self.inState(["finished"]),
83         get pause() self.inState(["downloading"]),
84         get remove() self.inState(["blocked_parental", "blocked_policy",
85                                    "canceled", "dirty", "failed", "finished"]),
86         get resume() self.resumable && self.inState(["paused"]),
87         get retry() self.inState(["canceled", "failed"])
88     })),
89
90     command: function command(name) {
91         util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
92         util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
93
94         if (Set.has(this.commands, name))
95             this.commands[name].call(this);
96         else
97             services.downloadManager[name + "Download"](this.id);
98     },
99
100     commands: {
101         delete: function delete_() {
102             this.targetFile.remove(false);
103             this.updateStatus();
104         },
105         launch: function launch() {
106             let self = this;
107             // Behavior mimics that of the builtin Download Manager.
108             function action() {
109                 try {
110                     if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
111                         this.MIMEInfo.launchWithFile(file);
112                     else
113                         file.launch();
114                 }
115                 catch (e) {
116                     services.externalProtocol.loadUrl(this.target);
117                 }
118             }
119
120             let file = io.File(this.targetFile);
121             if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
122                 this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
123                     function (resp) {
124                         if (/^a(lways)$/i.test(resp)) {
125                             prefs.set("browser.download.manager.alertOnEXEOpen", false);
126                             resp = "yes";
127                         }
128                         if (/^y(es)?$/i.test(resp))
129                             action.call(self);
130                     });
131             else
132                 action.call(this);
133         }
134     },
135
136     _compare: {
137         active: function (a, b) a.alive - b.alive,
138         complete: function (a, b) a.percentComplete - b.percentComplete,
139         date: function (a, b) a.startTime - b.startTime,
140         filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
141         size: function (a, b) a.size - b.size,
142         speed: function (a, b) a.speed - b.speed,
143         time: function (a, b) a.timeRemaining - b.timeRemaining,
144         url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
145     },
146
147     compare: function compare(other) values(this.list.sortOrder).map(function (order) {
148         let val = this._compare[order.substr(1)](this, other);
149
150         return (order[0] == "-") ? -val : val;
151     }, this).nth(util.identity, 0) || 0,
152
153     timeRemaining: Infinity,
154
155     updateProgress: function updateProgress() {
156         let self = this.__proto__;
157
158         if (this.amountTransferred === this.size) {
159             this.nodes.speed.textContent = "";
160             this.nodes.time.textContent = "";
161         }
162         else {
163             this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
164
165             if (this.speed == 0 || this.size == 0)
166                 this.nodes.time.textContent = _("download.unknown");
167             else {
168                 let seconds = (this.size - this.amountTransferred) / this.speed;
169                 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
170                 if (this.timeRemaining)
171                     this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
172                 else
173                     this.nodes.time.textContent = _("download.almostDone");
174             }
175         }
176
177         let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
178                                                                                       : _("download.unknown");
179         let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
180         this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
181
182         this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
183     },
184
185     updateStatus: function updateStatus() {
186
187         this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
188
189         this.nodes.row.setAttribute("status", this.status);
190         this.nodes.state.textContent = util.capitalize(this.status);
191
192         for (let node in values(this.nodes))
193             if (node.update)
194                 node.update();
195
196         this.updateProgress();
197     }
198 });
199 Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
200     if (!(key in Download.prototype))
201         Object.defineProperty(Download.prototype, key, {
202             get: function get() this.download[key],
203             set: function set(val) this.download[key] = val,
204             configurable: true
205         });
206 });
207
208 var DownloadList = Class("DownloadList",
209                          XPCOM([Ci.nsIDownloadProgressListener,
210                                 Ci.nsIObserver,
211                                 Ci.nsISupportsWeakReference]), {
212     init: function init(modules, filter, sort) {
213         this.sortOrder = sort;
214         this.modules = modules;
215         this.filter = filter && filter.toLowerCase();
216         this.nodes = {
217             commandTarget: this
218         };
219         this.downloads = {};
220     },
221
222     cleanup: function cleanup() {
223         this.observe.unregister();
224         services.downloadManager.removeListener(this);
225     },
226
227     message: Class.Memoize(function () {
228
229         XML.ignoreWhitespace = true;
230         XML.prettyPrinting = false;
231         util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
232                         <tr highlight="DownloadHead" key="head">
233                             <span>{_("title.Title")}</span>
234                             <span>{_("title.Status")}</span>
235                             <span/>
236                             <span>{_("title.Progress")}</span>
237                             <span/>
238                             <span>{_("title.Speed")}</span>
239                             <span>{_("title.Time remaining")}</span>
240                             <span>{_("title.Source")}</span>
241                         </tr>
242                         <tr highlight="Download"><span><div style="min-height: 1ex; /* FIXME */"/></span></tr>
243                         <tr highlight="Download" key="totals" active="true">
244                             <td><span highlight="Title">{_("title.Totals")}:</span>&#xa0;<span key="total"/></td>
245                             <td/>
246                             <td highlight="DownloadButtons">
247                                 <a highlight="Button" href="javascript:0" key="clear">{_("download.action.Clear")}</a>
248                             </td>
249                             <td highlight="DownloadProgress" key="progress">
250                                 <span highlight="DownloadProgressHave" key="progressHave"
251                                 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
252                             </td>
253                             <td highlight="DownloadPercent" key="percent"/>
254                             <td highlight="DownloadSpeed" key="speed"/>
255                             <td highlight="DownloadTime" key="time"/>
256                             <td/>
257                         </tr>
258                       </table>, this.document, this.nodes);
259
260         this.index = Array.indexOf(this.nodes.list.childNodes,
261                                    this.nodes.head);
262
263         for (let row in iter(services.downloadManager.DBConnection
264                                      .createStatement("SELECT id FROM moz_downloads")))
265             this.addDownload(row.id);
266         this.update();
267
268         util.addObserver(this);
269         services.downloadManager.addListener(this);
270         return this.nodes.list;
271     }),
272
273     addDownload: function addDownload(id) {
274         if (!(id in this.downloads)) {
275             let download = Download(id, this);
276             if (this.filter && download.displayName.indexOf(this.filter) === -1)
277                 return;
278
279             this.downloads[id] = download;
280             let index = values(this.downloads).sort(function (a, b) a.compare(b))
281                                               .indexOf(download);
282
283             this.nodes.list.insertBefore(download.nodes.row,
284                                          this.nodes.list.childNodes[index + this.index + 1]);
285         }
286     },
287     removeDownload: function removeDownload(id) {
288         if (id in this.downloads) {
289             this.nodes.list.removeChild(this.downloads[id].nodes.row);
290             delete this.downloads[id];
291         }
292     },
293
294     leave: function leave(stack) {
295         if (stack.pop)
296             this.cleanup();
297     },
298
299     allowedCommands: Class.Memoize(function () let (self = this) ({
300         get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
301     })),
302
303     commands: {
304         clear: function () {
305             services.downloadManager.cleanUp();
306         }
307     },
308
309     sort: function sort() {
310         let list = values(this.downloads).sort(function (a, b) a.compare(b));
311
312         for (let [i, download] in iter(list))
313             if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
314                 this.nodes.list.insertBefore(download.nodes.row,
315                                              this.nodes.list.childNodes[i + 1]);
316     },
317
318     shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
319
320     update: function update() {
321         for (let node in values(this.nodes))
322             if (node.update && node.update != update)
323                 node.update();
324         this.updateProgress();
325
326         let event = this.document.createEvent("Events");
327         event.initEvent("dactyl-commandupdate", true, false);
328         this.document.dispatchEvent(event);
329     },
330
331     timeRemaining: Infinity,
332
333     updateProgress: function updateProgress() {
334         let downloads = values(this.downloads).toArray();
335         let active    = downloads.filter(function (d) d.alive);
336
337         let self = Object.create(this);
338         for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
339             this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
340
341         Download.prototype.updateProgress.call(self);
342
343         this.nActive = active.length;
344         if (active.length)
345             this.nodes.total.textContent = _("download.nActive", active.length);
346         else for (let key in values(["total", "percent", "speed", "time"]))
347             this.nodes[key].textContent = "";
348
349         if (this.shouldSort("complete", "size", "speed", "time"))
350             this.sort();
351     },
352
353     observers: {
354         "download-manager-remove-download": function (id) {
355             if (id == null)
356                 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
357             else
358                 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
359
360             Array.concat(id).map(this.closure.removeDownload);
361             this.update();
362         }
363     },
364
365     onDownloadStateChange: function (state, download) {
366         try {
367             if (download.id in this.downloads)
368                 this.downloads[download.id].updateStatus();
369             else {
370                 this.addDownload(download.id);
371
372                 this.modules.mow.resize(false);
373                 this.nodes.list.scrollIntoView(false);
374             }
375             this.update();
376
377             if (this.shouldSort("active"))
378                 this.sort();
379         }
380         catch (e) {
381             util.reportError(e);
382         }
383     },
384
385     onProgressChange: function (webProgress, request,
386                                 curProgress, maxProgress,
387                                 curTotalProgress, maxTotalProgress,
388                                 download) {
389         try {
390             if (download.id in this.downloads)
391                 this.downloads[download.id].updateProgress();
392             this.updateProgress();
393         }
394         catch (e) {
395             util.reportError(e);
396         }
397     }
398 });
399
400 var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
401     init: function () {
402         services.downloadManager.addListener(this);
403     },
404
405     destroy: function destroy() {
406         services.downloadManager.removeListener(this);
407     },
408
409     onDownloadStateChange: function (state, download) {
410         if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
411             let url   = download.source.spec;
412             let title = download.displayName;
413             let file  = download.targetFile.path;
414             let size  = download.size;
415
416
417             overlay.modules.forEach(function (modules) {
418                 modules.dactyl.echomsg({ domains: [util.getHost(url)], message: _("io.downloadFinished", title, file) },
419                                        1, modules.commandline.ACTIVE_WINDOW);
420                 modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
421             });
422         }
423     }
424 }, {
425 }, {
426     commands: function initCommands(dactyl, modules, window) {
427         const { commands, CommandOption } = modules;
428
429         commands.add(["downl[oads]", "dl"],
430             "Display the downloads list",
431             function (args) {
432                 let downloads = DownloadList(modules, args[0], args["-sort"]);
433                 modules.commandline.echo(downloads);
434             },
435             {
436                 argCount: "?",
437                 options: [
438                     {
439                         names: ["-sort", "-s"],
440                         description: "Sort order (see 'downloadsort')",
441                         type: CommandOption.LIST,
442                         get default() modules.options["downloadsort"],
443                         completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
444                         validator: function (value) modules.options.get("downloadsort").validator(value)
445                     }
446                 ]
447             });
448
449         commands.add(["dlc[lear]"],
450             "Clear completed downloads",
451             function (args) { services.downloadManager.cleanUp(); });
452     },
453     options: function initOptions(dactyl, modules, window) {
454         const { options } = modules;
455
456         if (false)
457         options.add(["downloadcolumns", "dlc"],
458             "The columns to show in the download manager",
459             "stringlist", "filename,state,buttons,progress,percent,time,url",
460             {
461                 values: {
462                     buttons:    "Control buttons",
463                     filename:   "Target filename",
464                     percent:    "Percent complete",
465                     size:       "File size",
466                     speed:      "Download speed",
467                     state:      "The download's state",
468                     time:       "Time remaining",
469                     url:        "Source URL"
470                 }
471             });
472
473         options.add(["downloadsort", "dlsort", "dls"],
474             ":downloads sort order",
475             "stringlist", "-active,+filename",
476             {
477                 values: {
478                     active:     "Whether download is active",
479                     complete:   "Percent complete",
480                     date:       "Date and time the download began",
481                     filename:   "Target filename",
482                     size:       "File size",
483                     speed:      "Download speed",
484                     time:       "Time remaining",
485                     url:        "Source URL"
486                 },
487
488                 completer: function (context, extra) {
489                     let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
490
491                     context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
492                                                            .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
493                                                                                    ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
494                                                            .flatten().array;
495                 },
496
497                 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
498
499                 validator: function (value) {
500                     let seen = {};
501                     return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
502                                                                         && !Set.add(seen, val.substr(1)),
503                                        this) && value.length;
504                 }
505             });
506     }
507 });
508
509 endModule();
510
511 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
512
513 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: