1 // Copyright (c) 2011-2014 Kris Maglione <maglione.k@gmail.com>
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
7 defineModule("downloads", {
8 exports: ["Download", "Downloads", "downloads"],
12 lazyRequire("overlay", ["overlay"]);
13 lazyRequire("promises", ["Task", "promises"]);
15 lazyRequire("resource://gre/modules/Downloads.jsm", ["Downloads"]);
16 lazyRequire("resource://gre/modules/DownloadUtils.jsm", ["DownloadUtils"]);
18 var MAX_LOAD_TIME = 10 * 1000;
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)))
26 var Download = Class("Download", {
27 init: function init(download, list) {
28 this.download = download;
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 },
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" }],
51 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],,
52 ["td", { highlight: "DownloadPercent", key: "percent" }],
53 ["td", { highlight: "DownloadSpeed", key: "speed" }],
54 ["td", { highlight: "DownloadTime", key: "time" }],
56 ["a", { highlight: "DownloadSource", key: "source", href: this.source.url },
58 this.list.document, this.nodes);
60 this.nodes.launch.addEventListener("click", (event) => {
61 if (event.button == 0) {
62 event.preventDefault();
63 this.command("launch");
71 get active() !this.stopped,
73 get targetFile() File(this.download.target.path),
75 get displayName() this.targetFile.leafName,
77 get status() states[this.state],
79 inState: function inState(states) states.indexOf(this.status) >= 0,
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
89 command: function command(name) {
90 util.assert(hasOwnProperty(this.allowedCommands, name), _("download.unknownCommand"));
91 util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
93 if (hasOwnProperty(this.commands, name))
94 this.commands[name].call(this);
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);
105 launch: function launch() {
106 // Behavior mimics that of the builtin Download Manager.
109 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
110 this.MIMEInfo.launchWithFile(file.file);
115 services.externalProtocol.loadUrl(this.target);
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") + " ",
123 if (/^a(lways)$/i.test(resp)) {
124 prefs.set("browser.download.manager.alertOnEXEOpen", false);
127 if (/^y(es)?$/i.test(resp))
133 resume: function resume() {
134 this.download.start();
136 remove: promises.task(function remove() {
137 yield this.list.list.remove(this.download);
138 yield this.download.finalize(true);
140 stop: function stop() {
141 this.download.cancel();
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)
156 compare: function compare(other) values(this.list.sortOrder).map(function (order) {
157 let val = this._compare[order.substr(1)](this, other);
159 return (order[0] == "-") ? -val : val;
160 }, this).find(util.identity) || 0,
162 timeRemaining: Infinity,
164 updateProgress: function updateProgress() {
165 let self = this.__proto__;
168 this.nodes.speed.textContent = "";
169 this.nodes.time.textContent = "";
172 this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
174 if (this.speed == 0 || !this.hasProgress)
175 this.nodes.time.textContent = _("download.unknown");
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);
182 this.nodes.time.textContent = _("download.almostDone");
186 let total = this.nodes.progressTotal.textContent =
187 this.hasProgress && (this.totalBytes || !this.nActive)
188 ? util.formatBytes(this.totalBytes, 1, true)
189 : _("download.unknown");
191 let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
192 this.nodes.progressHave.textContent = util.formatBytes(this.currentBytes, 1, true).replace(suffix, "");
194 this.nodes.percent.textContent = this.hasProgress ? this.progress + "%" : "";
197 updateStatus: function updateStatus() {
199 this.nodes.row[this.active ? "setAttribute" : "removeAttribute"]("active", "true");
201 this.nodes.row.setAttribute("status", this.status);
202 this.nodes.state.textContent = util.capitalize(this.status);
204 for (let node in values(this.nodes))
208 this.updateProgress();
212 var DownloadList = Class("DownloadList",
213 XPCOM([Ci.nsIDownloadProgressListener,
215 Ci.nsISupportsWeakReference]), {
216 init: function init(modules, filter, sort) {
217 this.sortOrder = sort;
218 this.modules = modules;
219 this.filter = filter && filter.toLowerCase();
223 this.downloads = Map();
226 cleanup: function cleanup() {
228 this.list.removeView(this);
232 message: Class.Memoize(function () {
234 DOM.fromJSON(["table", { highlight: "Downloads", key: "list" },
235 ["tr", { highlight: "DownloadHead", key: "head" },
236 ["span", {}, _("title.Title")],
237 ["span", {}, _("title.Status")],
239 ["span", {}, _("title.Progress")],
241 ["span", {}, _("title.Speed")],
242 ["span", {}, _("title.Time remaining")],
243 ["span", {}, _("title.Source")]],
244 ["tr", { highlight: "Download" },
246 ["div", { style: "min-height: 1ex; /* FIXME */" }]]],
247 ["tr", { highlight: "Download", key: "totals", active: "true" },
249 ["span", { highlight: "Title" },
250 _("title.Totals") + ":"],
252 ["span", { key: "total" }]],
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" }],
259 ["span", { highlight: "DownloadProgressTotal", key: "progressTotal" }]],
260 ["td", { highlight: "DownloadPercent", key: "percent" }],
261 ["td", { highlight: "DownloadSpeed", key: "speed" }],
262 ["td", { highlight: "DownloadTime", key: "time" }],
264 this.document, this.nodes);
266 this.index = Array.indexOf(this.nodes.list.childNodes,
269 Task.spawn(function () {
270 this.list = yield Downloads.getList(Downloads.ALL);
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));
278 this.addDownload(download);
283 this.list.addView(this);
285 return this.nodes.list;
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))
294 this.downloads.set(download.download, download);
295 let index = values(this.downloads).toArray()
296 .sort((a, b) => a.compare(b))
299 this.nodes.list.insertBefore(download.nodes.row,
300 this.nodes.list.childNodes[index + this.index + 1]);
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);
310 leave: function leave(stack) {
315 allowedCommands: Class.Memoize(function () let (self = this) ({
316 get clear() iter(self.downloads.values()).some(dl => dl.allowedCommands.remove)
321 this.list.removeFinished();
325 sort: function sort() {
326 let list = iter(this.downloads.values()).sort((a, b) => a.compare(b));
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]);
334 shouldSort: function shouldSort() Array.some(arguments, val => this.sortOrder.some(v => v.substr(1) == val)),
336 update: function update() {
337 for (let node in values(this.nodes))
338 if (node.update && node.update != update)
340 this.updateProgress();
342 let event = this.document.createEvent("Events");
343 event.initEvent("dactyl-commandupdate", true, false);
344 this.document.dispatchEvent(event);
347 timeRemaining: Infinity,
349 updateProgress: function updateProgress() {
350 let downloads = iter(this.downloads.values()).toArray();
351 let active = downloads.filter(d => d.active);
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);
357 this.hasProgress = active.every(d => d.hasProgress);
358 this.progress = Math.round((this.currentBytes / this.totalBytes) * 100);
359 this.nActive = active.length;
361 Download.prototype.updateProgress.call(self);
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 = "";
368 if (this.shouldSort("complete", "size", "speed", "time"))
372 onDownloadAdded: function onDownloadAdded(download) {
373 this.addDownload(download);
375 this.modules.mow.resize(false);
376 this.nodes.list.scrollIntoView(false);
379 onDownloadRemoved: function onDownloadRemoved(download) {
380 this.removeDownload(download);
383 onDownloadChanged: function onDownloadChanged(download) {
384 if (this.downloads.has(download)) {
385 download = this.downloads.get(download)
387 download.updateStatus();
388 download.updateProgress();
392 if (this.shouldSort("active"))
403 "launchWhenSucceeded",
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,
424 var Downloads_ = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
426 Downloads.getList(Downloads.ALL).then(list => {
429 this.list.addView(this);
433 cleanup: function destroy() {
435 this.list.removeView(this);
439 onDownloadAdded: function onDownloadAdded(download) {
442 onDownloadRemoved: function onDownloadRemoved(download) {
445 onDownloadChanged: function onDownloadChanged(download) {
446 if (download.succeeded) {
447 let target = File(download.target.path);
449 let url = download.source.url;
450 let title = target.leafName;
451 let file = target.path;
452 let size = download.totalBytes;
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 });
463 commands: function initCommands(dactyl, modules, window) {
464 const { commands, CommandOption } = modules;
466 commands.add(["downl[oads]", "dl"],
467 "Display the downloads list",
469 let downloads = DownloadList(modules, args[0], args["-sort"]);
470 modules.commandline.echo(downloads);
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)
486 commands.add(["dlc[lear]"],
487 "Clear completed downloads",
488 function (args) { downloads.list.removeFinished(); });
490 options: function initOptions(dactyl, modules, window) {
491 const { options } = modules;
494 options.add(["downloadcolumns", "dlc"],
495 "The columns to show in the download manager",
496 "stringlist", "filename,state,buttons,progress,percent,time,url",
499 buttons: "Control buttons",
500 filename: "Target filename",
501 percent: "Percent complete",
503 speed: "Download speed",
504 state: "The download's state",
505 time: "Time remaining",
510 options.add(["downloadsort", "dlsort", "dls"],
511 ":downloads sort order",
512 "stringlist", "-active,+filename",
515 active: "Whether download is active",
516 complete: "Percent complete",
517 date: "Date and time the download began",
518 filename: "Target filename",
520 speed: "Download speed",
521 time: "Time remaining",
525 completer: function (context, extra) {
526 let seen = RealSet(extra.values.map(val => val.substr(1)));
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("")]])
534 has: function () Array.some(arguments, val => this.value.some(v => v.substr(1) == val)),
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)))
548 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
550 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: