]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/downloads.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / downloads.jsm
1 // Copyright (c) 2011-2014 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 lazyRequire("promises", ["Task", "promises"]);
14
15 lazyRequire("resource://gre/modules/Downloads.jsm", ["Downloads"]);
16 lazyRequire("resource://gre/modules/DownloadUtils.jsm", ["DownloadUtils"]);
17
18 var MAX_LOAD_TIME = 10 * 1000;
19
20 let prefix = "DOWNLOAD_";
21 var states = iter([v, k.slice(prefix.length).toLowerCase()]
22                   for ([k, v] in Iterator(Ci.nsIDownloadManager))
23                   if (k.startsWith(prefix)))
24                 .toObject();
25
26 var Download = Class("Download", {
27     init: function init(download, list) {
28         this.download = download;
29         this.list = list;
30
31         this.nodes = {
32             commandTarget: this
33         };
34         DOM.fromJSON(
35             ["tr", { highlight: "Download", key: "row" },
36                 ["td", { highlight: "DownloadTitle" },
37                     ["span", { highlight: "Link" },
38                         ["a", { key: "launch", href: this.target.spec, path: this.targetFile.path },
39                             this.displayName],
40                         ["span", { highlight: "LinkInfo" },
41                             this.targetFile.path]]],
42                 ["td", { highlight: "DownloadState", key: "state" }],
43                 ["td", { highlight: "DownloadButtons Buttons" },
44                     ["a", { highlight: "Button", href: "javascript:0", key: "stop"   }, _("download.action.Stop")],
45                     ["a", { highlight: "Button", href: "javascript:0", key: "remove" }, _("download.action.Remove")],
46                     ["a", { highlight: "Button", href: "javascript:0", key: "resume" }, _("download.action.Resume")],
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.url },
57                         this.source.url]]],
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 active() !this.stopped,
72
73     get targetFile() File(this.download.target.path),
74
75     get displayName() this.targetFile.leafName,
76
77     get status() states[this.state],
78
79     inState: function inState(states) states.indexOf(this.status) >= 0,
80
81     allowedCommands: Class.Memoize(function () let (self = this) ({
82         get delete() !self.active && (self.targetFile.exists() || self.hasPartialData),
83         get launch() self.targetFile.exists() && self.succeeded,
84         get stop()  self.active,
85         get remove() !self.active,
86         get resume() self.canceled
87     })),
88
89     command: function command(name) {
90         util.assert(hasOwnProperty(this.allowedCommands, name), _("download.unknownCommand"));
91         util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
92
93         if (hasOwnProperty(this.commands, name))
94             this.commands[name].call(this);
95     },
96
97     commands: {
98         delete: promises.task(function delete_() {
99             if (this.hasPartialData)
100                 yield this.removePartialData();
101             else if (this.targetFile.exists())
102                 this.targetFile.remove(false);
103             this.updateStatus();
104         }),
105         launch: function launch() {
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                     (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(this);
129                     });
130             else
131                 action.call(this);
132         },
133         resume: function resume() {
134             this.download.start();
135         },
136         remove: promises.task(function remove() {
137             yield this.list.list.remove(this.download);
138             yield this.download.finalize(true);
139         }),
140         stop: function stop() {
141             this.download.cancel();
142         },
143     },
144
145     _compare: {
146         active:   (a, b) => a.active - b.active,
147         complete: (a, b) => a.percentComplete - b.percentComplete,
148         date:     (a, b) => a.startTime - b.startTime,
149         filename: (a, b) => String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
150         size:     (a, b) => a.totalBytes - b.totalBytes,
151         speed:    (a, b) => a.speed - b.speed,
152         time:     (a, b) => a.timeRemaining - b.timeRemaining,
153         url:      (a, b) => String.localeCompare(a.source.url, b.source.url)
154     },
155
156     compare: function compare(other) values(this.list.sortOrder).map(function (order) {
157         let val = this._compare[order.substr(1)](this, other);
158
159         return (order[0] == "-") ? -val : val;
160     }, this).find(util.identity) || 0,
161
162     timeRemaining: Infinity,
163
164     updateProgress: function updateProgress() {
165         let self = this.__proto__;
166
167         if (!this.active) {
168             this.nodes.speed.textContent = "";
169             this.nodes.time.textContent = "";
170         }
171         else {
172             this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
173
174             if (this.speed == 0 || !this.hasProgress)
175                 this.nodes.time.textContent = _("download.unknown");
176             else {
177                 let seconds = (this.totalBytes - this.currentBytes) / this.speed;
178                 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
179                 if (this.timeRemaining)
180                     this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
181                 else
182                     this.nodes.time.textContent = _("download.almostDone");
183             }
184         }
185
186         let total = this.nodes.progressTotal.textContent =
187             this.hasProgress && (this.totalBytes || !this.nActive)
188                 ? util.formatBytes(this.totalBytes, 1, true)
189                 : _("download.unknown");
190
191         let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
192         this.nodes.progressHave.textContent = util.formatBytes(this.currentBytes, 1, true).replace(suffix, "");
193
194         this.nodes.percent.textContent = this.hasProgress ? this.progress + "%" : "";
195     },
196
197     updateStatus: function updateStatus() {
198
199         this.nodes.row[this.active ? "setAttribute" : "removeAttribute"]("active", "true");
200
201         this.nodes.row.setAttribute("status", this.status);
202         this.nodes.state.textContent = util.capitalize(this.status);
203
204         for (let node in values(this.nodes))
205             if (node.update)
206                 node.update();
207
208         this.updateProgress();
209     }
210 });
211
212 var DownloadList = Class("DownloadList",
213                          XPCOM([Ci.nsIDownloadProgressListener,
214                                 Ci.nsIObserver,
215                                 Ci.nsISupportsWeakReference]), {
216     init: function init(modules, filter, sort) {
217         this.sortOrder = sort;
218         this.modules = modules;
219         this.filter = filter && filter.toLowerCase();
220         this.nodes = {
221             commandTarget: this
222         };
223         this.downloads = Map();
224     },
225
226     cleanup: function cleanup() {
227         if (this.list)
228             this.list.removeView(this);
229         this.dead = true;
230     },
231
232     message: Class.Memoize(function () {
233
234         DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
235                         ["tr", { highlight: "DownloadHead", key: "head" },
236                             ["span", {}, _("title.Title")],
237                             ["span", {}, _("title.Status")],
238                             ["span"],
239                             ["span", {}, _("title.Progress")],
240                             ["span"],
241                             ["span", {}, _("title.Speed")],
242                             ["span", {}, _("title.Time remaining")],
243                             ["span", {}, _("title.Source")]],
244                         ["tr", { highlight: "Download" },
245                             ["span", {},
246                                 ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
247                         ["tr", { highlight: "Download", key: "totals", active: "true" },
248                             ["td", {},
249                                 ["span", { highlight: "Title" },
250                                     _("title.Totals") + ":"],
251                                 " ",
252                                 ["span", { key: "total" }]],
253                             ["td"],
254                             ["td", { highlight: "DownloadButtons" },
255                                 ["a", { highlight: "Button", href: "javascript:0", key: "clear" }, _("download.action.Clear")]],
256                             ["td", { highlight: "DownloadProgress", key: "progress" },
257                                 ["span", { highlight: "DownloadProgressHave", key: "progressHave" }],
258                                 "/",
259                                 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
260                             ["td", { highlight: "DownloadPercent", key: "percent" }],
261                             ["td", { highlight: "DownloadSpeed", key: "speed" }],
262                             ["td", { highlight: "DownloadTime", key: "time" }],
263                             ["td"]]],
264                       this.document, this.nodes);
265
266         this.index = Array.indexOf(this.nodes.list.childNodes,
267                                    this.nodes.head);
268
269         Task.spawn(function () {
270             this.list = yield Downloads.getList(Downloads.ALL);
271
272             let start = Date.now();
273             for (let download of yield this.list.getAll()) {
274                 if (Date.now() - start > MAX_LOAD_TIME) {
275                     util.dactyl.warn(_("download.givingUpAfter", (Date.now() - start) / 1000));
276                     break;
277                 }
278                 this.addDownload(download);
279             }
280             this.update();
281
282             if (!this.dead)
283                 this.list.addView(this);
284         }.bind(this));
285         return this.nodes.list;
286     }),
287
288     addDownload: function addDownload(download) {
289         if (!this.downloads.has(download)) {
290             download = Download(download, this);
291             if (this.filter && !download.displayName.contains(this.filter))
292                 return;
293
294             this.downloads.set(download.download, download);
295             let index = values(this.downloads).toArray()
296                             .sort((a, b) => a.compare(b))
297                             .indexOf(download);
298
299             this.nodes.list.insertBefore(download.nodes.row,
300                                          this.nodes.list.childNodes[index + this.index + 1]);
301         }
302     },
303     removeDownload: function removeDownload(download) {
304         if (this.downloads.has(download)) {
305             this.nodes.list.removeChild(this.downloads.get(download).nodes.row);
306             delete this.downloads.delete(download);
307         }
308     },
309
310     leave: function leave(stack) {
311         if (stack.pop)
312             this.cleanup();
313     },
314
315     allowedCommands: Class.Memoize(function () let (self = this) ({
316         get clear() iter(self.downloads.values()).some(dl => dl.allowedCommands.remove)
317     })),
318
319     commands: {
320         clear: function () {
321             this.list.removeFinished();
322         }
323     },
324
325     sort: function sort() {
326         let list = iter(this.downloads.values()).sort((a, b) => a.compare(b));
327
328         for (let [i, download] in iter(list))
329             if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
330                 this.nodes.list.insertBefore(download.nodes.row,
331                                              this.nodes.list.childNodes[i + 1]);
332     },
333
334     shouldSort: function shouldSort() Array.some(arguments, val => this.sortOrder.some(v => v.substr(1) == val)),
335
336     update: function update() {
337         for (let node in values(this.nodes))
338             if (node.update && node.update != update)
339                 node.update();
340         this.updateProgress();
341
342         let event = this.document.createEvent("Events");
343         event.initEvent("dactyl-commandupdate", true, false);
344         this.document.dispatchEvent(event);
345     },
346
347     timeRemaining: Infinity,
348
349     updateProgress: function updateProgress() {
350         let downloads = iter(this.downloads.values()).toArray();
351         let active    = downloads.filter(d => d.active);
352
353         let self = Object.create(this);
354         for (let prop in values(["currentBytes", "totalBytes", "speed", "timeRemaining"]))
355             this[prop] = active.reduce((acc, dl) => dl[prop] + acc, 0);
356
357         this.hasProgress = active.every(d => d.hasProgress);
358         this.progress = Math.round((this.currentBytes / this.totalBytes) * 100);
359         this.nActive = active.length;
360
361         Download.prototype.updateProgress.call(self);
362
363         if (active.length)
364             this.nodes.total.textContent = _("download.nActive", active.length);
365         else for (let key in values(["total", "percent", "speed", "time"]))
366             this.nodes[key].textContent = "";
367
368         if (this.shouldSort("complete", "size", "speed", "time"))
369             this.sort();
370     },
371
372     onDownloadAdded: function onDownloadAdded(download) {
373         this.addDownload(download);
374
375         this.modules.mow.resize(false);
376         this.nodes.list.scrollIntoView(false);
377     },
378
379     onDownloadRemoved: function onDownloadRemoved(download) {
380         this.removeDownload(download);
381     },
382
383     onDownloadChanged: function onDownloadChanged(download) {
384         if (this.downloads.has(download)) {
385             download = this.downloads.get(download)
386
387             download.updateStatus();
388             download.updateProgress();
389
390             this.update();
391
392             if (this.shouldSort("active"))
393                 this.sort();
394         }
395     }
396 });
397 ["canceled",
398  "contentType",
399  "currentBytes",
400  "error",
401  "hasPartialData",
402  "hasProgress",
403  "launchWhenSucceeded",
404  "launcherPath",
405  "progress",
406  "saver",
407  "source",
408  "speed",
409  "startTime",
410  "stopped",
411  "succeeded",
412  "target",
413  "totalBytes",
414  "tryToKeepPartialData"].forEach(key => {
415     if (!(key in Download.prototype))
416         Object.defineProperty(Download.prototype, key, {
417             get: function get() this.download[key],
418             set: function set(val) this.download[key] = val,
419             configurable: true
420         });
421 });
422
423
424 var Downloads_ = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
425     init: function () {
426         Downloads.getList(Downloads.ALL).then(list => {
427             this.list = list;
428             if (!this.dead)
429                 this.list.addView(this);
430         });
431     },
432
433     cleanup: function destroy() {
434         if (this.list)
435             this.list.removeView(this);
436         this.dead = true;
437     },
438
439     onDownloadAdded: function onDownloadAdded(download) {
440     },
441
442     onDownloadRemoved: function onDownloadRemoved(download) {
443     },
444
445     onDownloadChanged: function onDownloadChanged(download) {
446         if (download.succeeded) {
447             let target = File(download.target.path);
448
449             let url   = download.source.url;
450             let title = target.leafName;
451             let file  = target.path;
452             let size  = download.totalBytes;
453
454             overlay.modules.forEach(function (modules) {
455                 modules.dactyl.echomsg({ domains: [util.getHost(url)], message: _("io.downloadFinished", title, file) },
456                                        1, modules.commandline.ACTIVE_WINDOW);
457                 modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
458             });
459         }
460     }
461 }, {
462 }, {
463     commands: function initCommands(dactyl, modules, window) {
464         const { commands, CommandOption } = modules;
465
466         commands.add(["downl[oads]", "dl"],
467             "Display the downloads list",
468             function (args) {
469                 let downloads = DownloadList(modules, args[0], args["-sort"]);
470                 modules.commandline.echo(downloads);
471             },
472             {
473                 argCount: "?",
474                 options: [
475                     {
476                         names: ["-sort", "-s"],
477                         description: "Sort order (see 'downloadsort')",
478                         type: CommandOption.LIST,
479                         get default() modules.options["downloadsort"],
480                         completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
481                         validator: function (value) modules.options.get("downloadsort").validator(value)
482                     }
483                 ]
484             });
485
486         commands.add(["dlc[lear]"],
487             "Clear completed downloads",
488             function (args) { downloads.list.removeFinished(); });
489     },
490     options: function initOptions(dactyl, modules, window) {
491         const { options } = modules;
492
493         if (false)
494         options.add(["downloadcolumns", "dlc"],
495             "The columns to show in the download manager",
496             "stringlist", "filename,state,buttons,progress,percent,time,url",
497             {
498                 values: {
499                     buttons:    "Control buttons",
500                     filename:   "Target filename",
501                     percent:    "Percent complete",
502                     size:       "File size",
503                     speed:      "Download speed",
504                     state:      "The download's state",
505                     time:       "Time remaining",
506                     url:        "Source URL"
507                 }
508             });
509
510         options.add(["downloadsort", "dlsort", "dls"],
511             ":downloads sort order",
512             "stringlist", "-active,+filename",
513             {
514                 values: {
515                     active:     "Whether download is active",
516                     complete:   "Percent complete",
517                     date:       "Date and time the download began",
518                     filename:   "Target filename",
519                     size:       "File size",
520                     speed:      "Download speed",
521                     time:       "Time remaining",
522                     url:        "Source URL"
523                 },
524
525                 completer: function (context, extra) {
526                     let seen = RealSet(extra.values.map(val => val.substr(1)));
527
528                     context.completions = iter(this.values).filter(([k, v]) => !seen.has(k))
529                                                            .map(([k, v]) => [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
530                                                                              ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
531                                                            .flatten().array;
532                 },
533
534                 has: function () Array.some(arguments, val => this.value.some(v => v.substr(1) == val)),
535
536                 validator: function (value) {
537                     let seen = RealSet();
538                     return value.every(val => /^[+-]/.test(val) && hasOwnProperty(this.values, val.substr(1))
539                                                                 && !seen.add(val.substr(1)))
540                         && value.length;
541                 }
542             });
543     }
544 });
545
546 endModule();
547
548 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
549
550 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: