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