]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/downloads.jsm
cbcd9a6e4fcaf4ff3ffccc4ae1bb3e35213c4850
[dactyl.git] / common / modules / downloads.jsm
1 // Copyright (c) 2011-2013 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         this.download = services.downloadManager.getDownload(id);
27         this.list = list;
28
29         this.nodes = {
30             commandTarget: this
31         };
32         DOM.fromJSON(
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 },
37                             this.displayName],
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" }],
50                     "/",
51                     ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],,
52                 ["td", { highlight: "DownloadPercent", key: "percent" }],
53                 ["td", { highlight: "DownloadSpeed", key: "speed" }],
54                 ["td", { highlight: "DownloadTime", key: "time" }],
55                 ["td", {},
56                     ["a", { highlight: "DownloadSource", key: "source", href: this.source.spec },
57                         this.source.spec]]],
58             this.list.document, this.nodes);
59
60         this.nodes.launch.addEventListener("click", (event) => {
61             if (event.button == 0) {
62                 event.preventDefault();
63                 this.command("launch");
64             }
65         }, false);
66
67         this.updateStatus();
68         return this;
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             // Behavior mimics that of the builtin Download Manager.
105             function action() {
106                 try {
107                     if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
108                         this.MIMEInfo.launchWithFile(file.file);
109                     else
110                         file.launch();
111                 }
112                 catch (e) {
113                     services.externalProtocol.loadUrl(this.target);
114                 }
115             }
116
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") + " ",
120                     (resp) => {
121                         if (/^a(lways)$/i.test(resp)) {
122                             prefs.set("browser.download.manager.alertOnEXEOpen", false);
123                             resp = "yes";
124                         }
125                         if (/^y(es)?$/i.test(resp))
126                             action.call(this);
127                     });
128             else
129                 action.call(this);
130         }
131     },
132
133     _compare: {
134         active: (a, b) => a.alive - b.alive,
135         complete: (a, b) => a.percentComplete - b.percentComplete,
136         date: (a, b) => a.startTime - b.startTime,
137         filename: (a, b) => String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
138         size: (a, b) => a.size - b.size,
139         speed: (a, b) => a.speed - b.speed,
140         time: (a, b) => a.timeRemaining - b.timeRemaining,
141         url: (a, b) => String.localeCompare(a.source.spec, b.source.spec)
142     },
143
144     compare: function compare(other) values(this.list.sortOrder).map(function (order) {
145         let val = this._compare[order.substr(1)](this, other);
146
147         return (order[0] == "-") ? -val : val;
148     }, this).nth(util.identity, 0) || 0,
149
150     timeRemaining: Infinity,
151
152     updateProgress: function updateProgress() {
153         let self = this.__proto__;
154
155         if (this.amountTransferred === this.size) {
156             this.nodes.speed.textContent = "";
157             this.nodes.time.textContent = "";
158         }
159         else {
160             this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
161
162             if (this.speed == 0 || this.size == 0)
163                 this.nodes.time.textContent = _("download.unknown");
164             else {
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);
169                 else
170                     this.nodes.time.textContent = _("download.almostDone");
171             }
172         }
173
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, "");
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 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,
201             configurable: true
202         });
203 });
204
205 var DownloadList = Class("DownloadList",
206                          XPCOM([Ci.nsIDownloadProgressListener,
207                                 Ci.nsIObserver,
208                                 Ci.nsISupportsWeakReference]), {
209     init: function init(modules, filter, sort) {
210         this.sortOrder = sort;
211         this.modules = modules;
212         this.filter = filter && filter.toLowerCase();
213         this.nodes = {
214             commandTarget: this
215         };
216         this.downloads = {};
217     },
218
219     cleanup: function cleanup() {
220         this.observe.unregister();
221         services.downloadManager.removeListener(this);
222     },
223
224     message: Class.Memoize(function () {
225
226         DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
227                         ["tr", { highlight: "DownloadHead", key: "head" },
228                             ["span", {}, _("title.Title")],
229                             ["span", {}, _("title.Status")],
230                             ["span"],
231                             ["span", {}, _("title.Progress")],
232                             ["span"],
233                             ["span", {}, _("title.Speed")],
234                             ["span", {}, _("title.Time remaining")],
235                             ["span", {}, _("title.Source")]],
236                         ["tr", { highlight: "Download" },
237                             ["span", {},
238                                 ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
239                         ["tr", { highlight: "Download", key: "totals", active: "true" },
240                             ["td", {},
241                                 ["span", { highlight: "Title" },
242                                     _("title.Totals") + ":"],
243                                 " ",
244                                 ["span", { key: "total" }]],
245                             ["td"],
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" }],
250                                 "/",
251                                 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
252                             ["td", { highlight: "DownloadPercent", key: "percent" }],
253                             ["td", { highlight: "DownloadSpeed", key: "speed" }],
254                             ["td", { highlight: "DownloadTime", key: "time" }],
255                             ["td"]]],
256                       this.document, this.nodes);
257
258         this.index = Array.indexOf(this.nodes.list.childNodes,
259                                    this.nodes.head);
260
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));
266                 break;
267             }
268             this.addDownload(row.id);
269         }
270         this.update();
271
272         util.addObserver(this);
273         services.downloadManager.addListener(this);
274         return this.nodes.list;
275     }),
276
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)
281                 return;
282
283             this.downloads[id] = download;
284             let index = values(this.downloads).sort((a, b) => a.compare(b))
285                                               .indexOf(download);
286
287             this.nodes.list.insertBefore(download.nodes.row,
288                                          this.nodes.list.childNodes[index + this.index + 1]);
289         }
290     },
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];
295         }
296     },
297
298     leave: function leave(stack) {
299         if (stack.pop)
300             this.cleanup();
301     },
302
303     allowedCommands: Class.Memoize(function () let (self = this) ({
304         get clear() values(self.downloads).some(dl => dl.allowedCommands.remove)
305     })),
306
307     commands: {
308         clear: function () {
309             services.downloadManager.cleanUp();
310         }
311     },
312
313     sort: function sort() {
314         let list = values(this.downloads).sort((a, b) => a.compare(b));
315
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]);
320     },
321
322     shouldSort: function shouldSort() Array.some(arguments, val => this.sortOrder.some(v => v.substr(1) == val)),
323
324     update: function update() {
325         for (let node in values(this.nodes))
326             if (node.update && node.update != update)
327                 node.update();
328         this.updateProgress();
329
330         let event = this.document.createEvent("Events");
331         event.initEvent("dactyl-commandupdate", true, false);
332         this.document.dispatchEvent(event);
333     },
334
335     timeRemaining: Infinity,
336
337     updateProgress: function updateProgress() {
338         let downloads = values(this.downloads).toArray();
339         let active    = downloads.filter(d => d.alive);
340
341         let self = Object.create(this);
342         for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
343             this[prop] = active.reduce((acc, dl) => dl[prop] + acc, 0);
344
345         Download.prototype.updateProgress.call(self);
346
347         this.nActive = active.length;
348         if (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 = "";
352
353         if (this.shouldSort("complete", "size", "speed", "time"))
354             this.sort();
355     },
356
357     observers: {
358         "download-manager-remove-download": function (id) {
359             if (id == null)
360                 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
361             else
362                 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
363
364             Array.concat(id).map(this.closure.removeDownload);
365             this.update();
366         }
367     },
368
369     onDownloadStateChange: function (state, download) {
370         try {
371             if (download.id in this.downloads)
372                 this.downloads[download.id].updateStatus();
373             else {
374                 this.addDownload(download.id);
375
376                 this.modules.mow.resize(false);
377                 this.nodes.list.scrollIntoView(false);
378             }
379             this.update();
380
381             if (this.shouldSort("active"))
382                 this.sort();
383         }
384         catch (e) {
385             util.reportError(e);
386         }
387     },
388
389     onProgressChange: function (webProgress, request,
390                                 curProgress, maxProgress,
391                                 curTotalProgress, maxTotalProgress,
392                                 download) {
393         try {
394             if (download.id in this.downloads)
395                 this.downloads[download.id].updateProgress();
396             this.updateProgress();
397         }
398         catch (e) {
399             util.reportError(e);
400         }
401     }
402 });
403
404 var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
405     init: function () {
406         services.downloadManager.addListener(this);
407     },
408
409     cleanup: function destroy() {
410         services.downloadManager.removeListener(this);
411     },
412
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;
419
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 });
424             });
425         }
426     }
427 }, {
428 }, {
429     commands: function initCommands(dactyl, modules, window) {
430         const { commands, CommandOption } = modules;
431
432         commands.add(["downl[oads]", "dl"],
433             "Display the downloads list",
434             function (args) {
435                 let downloads = DownloadList(modules, args[0], args["-sort"]);
436                 modules.commandline.echo(downloads);
437             },
438             {
439                 argCount: "?",
440                 options: [
441                     {
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)
448                     }
449                 ]
450             });
451
452         commands.add(["dlc[lear]"],
453             "Clear completed downloads",
454             function (args) { services.downloadManager.cleanUp(); });
455     },
456     options: function initOptions(dactyl, modules, window) {
457         const { options } = modules;
458
459         if (false)
460         options.add(["downloadcolumns", "dlc"],
461             "The columns to show in the download manager",
462             "stringlist", "filename,state,buttons,progress,percent,time,url",
463             {
464                 values: {
465                     buttons:    "Control buttons",
466                     filename:   "Target filename",
467                     percent:    "Percent complete",
468                     size:       "File size",
469                     speed:      "Download speed",
470                     state:      "The download's state",
471                     time:       "Time remaining",
472                     url:        "Source URL"
473                 }
474             });
475
476         options.add(["downloadsort", "dlsort", "dls"],
477             ":downloads sort order",
478             "stringlist", "-active,+filename",
479             {
480                 values: {
481                     active:     "Whether download is active",
482                     complete:   "Percent complete",
483                     date:       "Date and time the download began",
484                     filename:   "Target filename",
485                     size:       "File size",
486                     speed:      "Download speed",
487                     time:       "Time remaining",
488                     url:        "Source URL"
489                 },
490
491                 completer: function (context, extra) {
492                     let seen = Set.has(Set(extra.values.map(val => val.substr(1))));
493
494                     context.completions = iter(this.values).filter(([k, v]) => !seen(k))
495                                                            .map(([k, v]) => [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
496                                                                              ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
497                                                            .flatten().array;
498                 },
499
500                 has: function () Array.some(arguments, val => this.value.some(v => v.substr(1) == val)),
501
502                 validator: function (value) {
503                     let seen = {};
504                     return value.every(val => /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
505                                                                 && !Set.add(seen, val.substr(1)))
506                         && value.length;
507                 }
508             });
509     }
510 });
511
512 endModule();
513
514 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
515
516 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: