1 // Copyright (c) 2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("downloads", {
9 exports: ["Download", "Downloads", "downloads"]
12 this.lazyRequire("overlay", ["overlay"]);
14 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
16 let prefix = "DOWNLOAD_";
17 var states = iter([v, k.slice(prefix.length).toLowerCase()]
18 for ([k, v] in Iterator(Ci.nsIDownloadManager))
19 if (k.indexOf(prefix) == 0))
22 var Download = Class("Download", {
23 init: function init(id, list) {
25 this.download = services.downloadManager.getDownload(id);
31 XML.ignoreWhitespace = true;
32 XML.prettyPrinting = false;
34 <tr highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
35 <td highlight="DownloadTitle">
36 <span highlight="Link">
38 href={self.target.spec} path={self.targetFile.path}>{self.displayName}</a>
39 <span highlight="LinkInfo">{self.targetFile.path}</span>
42 <td highlight="DownloadState" key="state"/>
43 <td highlight="DownloadButtons Buttons">
44 <a highlight="Button" href="javascript:0" key="pause">{_("download.action.Pause")}</a>
45 <a highlight="Button" href="javascript:0" key="remove">{_("download.action.Remove")}</a>
46 <a highlight="Button" href="javascript:0" key="resume">{_("download.action.Resume")}</a>
47 <a highlight="Button" href="javascript:0" key="retry">{_("download.action.Retry")}</a>
48 <a highlight="Button" href="javascript:0" key="cancel">{_("download.action.Cancel")}</a>
49 <a highlight="Button" href="javascript:0" key="delete">{_("download.action.Delete")}</a>
51 <td highlight="DownloadProgress" key="progress">
52 <span highlight="DownloadProgressHave" key="progressHave"
53 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
55 <td highlight="DownloadPercent" key="percent"/>
56 <td highlight="DownloadSpeed" key="speed"/>
57 <td highlight="DownloadTime" key="time"/>
58 <td><a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a></td>
60 this.list.document, this.nodes);
62 this.nodes.launch.addEventListener("click", function (event) {
63 if (event.button == 0) {
64 event.preventDefault();
65 self.command("launch");
73 get status() states[this.state],
75 inState: function inState(states) states.indexOf(this.status) >= 0,
77 get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
79 allowedCommands: Class.Memoize(function () let (self = this) ({
80 get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
81 get delete() !this.cancel && self.targetFile.exists(),
82 get launch() self.targetFile.exists() && self.inState(["finished"]),
83 get pause() self.inState(["downloading"]),
84 get remove() self.inState(["blocked_parental", "blocked_policy",
85 "canceled", "dirty", "failed", "finished"]),
86 get resume() self.resumable && self.inState(["paused"]),
87 get retry() self.inState(["canceled", "failed"])
90 command: function command(name) {
91 util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
92 util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
94 if (Set.has(this.commands, name))
95 this.commands[name].call(this);
97 services.downloadManager[name + "Download"](this.id);
101 delete: function delete_() {
102 this.targetFile.remove(false);
105 launch: function launch() {
107 // Behavior mimics that of the builtin Download Manager.
110 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
111 this.MIMEInfo.launchWithFile(file);
116 services.externalProtocol.loadUrl(this.target);
120 let file = io.File(this.targetFile);
121 if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
122 this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
124 if (/^a(lways)$/i.test(resp)) {
125 prefs.set("browser.download.manager.alertOnEXEOpen", false);
128 if (/^y(es)?$/i.test(resp))
137 active: function (a, b) a.alive - b.alive,
138 complete: function (a, b) a.percentComplete - b.percentComplete,
139 date: function (a, b) a.startTime - b.startTime,
140 filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
141 size: function (a, b) a.size - b.size,
142 speed: function (a, b) a.speed - b.speed,
143 time: function (a, b) a.timeRemaining - b.timeRemaining,
144 url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
147 compare: function compare(other) values(this.list.sortOrder).map(function (order) {
148 let val = this._compare[order.substr(1)](this, other);
150 return (order[0] == "-") ? -val : val;
151 }, this).nth(util.identity, 0) || 0,
153 timeRemaining: Infinity,
155 updateProgress: function updateProgress() {
156 let self = this.__proto__;
158 if (this.amountTransferred === this.size) {
159 this.nodes.speed.textContent = "";
160 this.nodes.time.textContent = "";
163 this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
165 if (this.speed == 0 || this.size == 0)
166 this.nodes.time.textContent = _("download.unknown");
168 let seconds = (this.size - this.amountTransferred) / this.speed;
169 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
170 if (this.timeRemaining)
171 this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
173 this.nodes.time.textContent = _("download.almostDone");
177 let total = this.nodes.progressTotal.textContent = this.size || !this.nActive ? util.formatBytes(this.size, 1, true)
178 : _("download.unknown");
179 let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
180 this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
182 this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
185 updateStatus: function updateStatus() {
187 this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
189 this.nodes.row.setAttribute("status", this.status);
190 this.nodes.state.textContent = util.capitalize(this.status);
192 for (let node in values(this.nodes))
196 this.updateProgress();
199 Object.keys(XPCOMShim([Ci.nsIDownload])).forEach(function (key) {
200 if (!(key in Download.prototype))
201 Object.defineProperty(Download.prototype, key, {
202 get: function get() this.download[key],
203 set: function set(val) this.download[key] = val,
208 var DownloadList = Class("DownloadList",
209 XPCOM([Ci.nsIDownloadProgressListener,
211 Ci.nsISupportsWeakReference]), {
212 init: function init(modules, filter, sort) {
213 this.sortOrder = sort;
214 this.modules = modules;
215 this.filter = filter && filter.toLowerCase();
222 cleanup: function cleanup() {
223 this.observe.unregister();
224 services.downloadManager.removeListener(this);
227 message: Class.Memoize(function () {
229 XML.ignoreWhitespace = true;
230 XML.prettyPrinting = false;
231 util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
232 <tr highlight="DownloadHead" key="head">
233 <span>{_("title.Title")}</span>
234 <span>{_("title.Status")}</span>
236 <span>{_("title.Progress")}</span>
238 <span>{_("title.Speed")}</span>
239 <span>{_("title.Time remaining")}</span>
240 <span>{_("title.Source")}</span>
242 <tr highlight="Download"><span><div style="min-height: 1ex; /* FIXME */"/></span></tr>
243 <tr highlight="Download" key="totals" active="true">
244 <td><span highlight="Title">{_("title.Totals")}:</span> <span key="total"/></td>
246 <td highlight="DownloadButtons">
247 <a highlight="Button" href="javascript:0" key="clear">{_("download.action.Clear")}</a>
249 <td highlight="DownloadProgress" key="progress">
250 <span highlight="DownloadProgressHave" key="progressHave"
251 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
253 <td highlight="DownloadPercent" key="percent"/>
254 <td highlight="DownloadSpeed" key="speed"/>
255 <td highlight="DownloadTime" key="time"/>
258 </table>, this.document, this.nodes);
260 this.index = Array.indexOf(this.nodes.list.childNodes,
263 for (let row in iter(services.downloadManager.DBConnection
264 .createStatement("SELECT id FROM moz_downloads")))
265 this.addDownload(row.id);
268 util.addObserver(this);
269 services.downloadManager.addListener(this);
270 return this.nodes.list;
273 addDownload: function addDownload(id) {
274 if (!(id in this.downloads)) {
275 let download = Download(id, this);
276 if (this.filter && download.displayName.indexOf(this.filter) === -1)
279 this.downloads[id] = download;
280 let index = values(this.downloads).sort(function (a, b) a.compare(b))
283 this.nodes.list.insertBefore(download.nodes.row,
284 this.nodes.list.childNodes[index + this.index + 1]);
287 removeDownload: function removeDownload(id) {
288 if (id in this.downloads) {
289 this.nodes.list.removeChild(this.downloads[id].nodes.row);
290 delete this.downloads[id];
294 leave: function leave(stack) {
299 allowedCommands: Class.Memoize(function () let (self = this) ({
300 get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
305 services.downloadManager.cleanUp();
309 sort: function sort() {
310 let list = values(this.downloads).sort(function (a, b) a.compare(b));
312 for (let [i, download] in iter(list))
313 if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
314 this.nodes.list.insertBefore(download.nodes.row,
315 this.nodes.list.childNodes[i + 1]);
318 shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
320 update: function update() {
321 for (let node in values(this.nodes))
322 if (node.update && node.update != update)
324 this.updateProgress();
326 let event = this.document.createEvent("Events");
327 event.initEvent("dactyl-commandupdate", true, false);
328 this.document.dispatchEvent(event);
331 timeRemaining: Infinity,
333 updateProgress: function updateProgress() {
334 let downloads = values(this.downloads).toArray();
335 let active = downloads.filter(function (d) d.alive);
337 let self = Object.create(this);
338 for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
339 this[prop] = active.reduce(function (acc, dl) dl[prop] + acc, 0);
341 Download.prototype.updateProgress.call(self);
343 this.nActive = active.length;
345 this.nodes.total.textContent = _("download.nActive", active.length);
346 else for (let key in values(["total", "percent", "speed", "time"]))
347 this.nodes[key].textContent = "";
349 if (this.shouldSort("complete", "size", "speed", "time"))
354 "download-manager-remove-download": function (id) {
356 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
358 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
360 Array.concat(id).map(this.closure.removeDownload);
365 onDownloadStateChange: function (state, download) {
367 if (download.id in this.downloads)
368 this.downloads[download.id].updateStatus();
370 this.addDownload(download.id);
372 this.modules.mow.resize(false);
373 this.nodes.list.scrollIntoView(false);
377 if (this.shouldSort("active"))
385 onProgressChange: function (webProgress, request,
386 curProgress, maxProgress,
387 curTotalProgress, maxTotalProgress,
390 if (download.id in this.downloads)
391 this.downloads[download.id].updateProgress();
392 this.updateProgress();
400 var Downloads = Module("downloads", XPCOM(Ci.nsIDownloadProgressListener), {
402 services.downloadManager.addListener(this);
405 destroy: function destroy() {
406 services.downloadManager.removeListener(this);
409 onDownloadStateChange: function (state, download) {
410 if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
411 let url = download.source.spec;
412 let title = download.displayName;
413 let file = download.targetFile.path;
414 let size = download.size;
417 overlay.modules.forEach(function (modules) {
418 modules.dactyl.echomsg({ domains: [util.getHost(url)], message: _("io.downloadFinished", title, file) },
419 1, modules.commandline.ACTIVE_WINDOW);
420 modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
426 commands: function initCommands(dactyl, modules, window) {
427 const { commands, CommandOption } = modules;
429 commands.add(["downl[oads]", "dl"],
430 "Display the downloads list",
432 let downloads = DownloadList(modules, args[0], args["-sort"]);
433 modules.commandline.echo(downloads);
439 names: ["-sort", "-s"],
440 description: "Sort order (see 'downloadsort')",
441 type: CommandOption.LIST,
442 get default() modules.options["downloadsort"],
443 completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
444 validator: function (value) modules.options.get("downloadsort").validator(value)
449 commands.add(["dlc[lear]"],
450 "Clear completed downloads",
451 function (args) { services.downloadManager.cleanUp(); });
453 options: function initOptions(dactyl, modules, window) {
454 const { options } = modules;
457 options.add(["downloadcolumns", "dlc"],
458 "The columns to show in the download manager",
459 "stringlist", "filename,state,buttons,progress,percent,time,url",
462 buttons: "Control buttons",
463 filename: "Target filename",
464 percent: "Percent complete",
466 speed: "Download speed",
467 state: "The download's state",
468 time: "Time remaining",
473 options.add(["downloadsort", "dlsort", "dls"],
474 ":downloads sort order",
475 "stringlist", "-active,+filename",
478 active: "Whether download is active",
479 complete: "Percent complete",
480 date: "Date and time the download began",
481 filename: "Target filename",
483 speed: "Download speed",
484 time: "Time remaining",
488 completer: function (context, extra) {
489 let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
491 context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
492 .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
493 ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
497 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
499 validator: function (value) {
501 return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
502 && !Set.add(seen, val.substr(1)),
503 this) && value.length;
511 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
513 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: