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