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"],
10 use: ["io", "messages", "prefs", "services", "util"]
13 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
15 let prefix = "DOWNLOAD_";
16 var states = iter([v, k.slice(prefix.length).toLowerCase()]
17 for ([k, v] in Iterator(Ci.nsIDownloadManager))
18 if (k.indexOf(prefix) == 0))
21 var Download = Class("Download", {
22 init: function init(id, list) {
23 let self = XPCSafeJSObjectWrapper(services.downloadManager.getDownload(id));
24 self.__proto__ = this;
32 <tr highlight="Download" key="row" xmlns:dactyl={NS} xmlns={XHTML}>
33 <td highlight="DownloadTitle">
34 <span highlight="Link">
36 href={self.target.spec} path={self.targetFile.path}>{self.displayName}</a>
37 <span highlight="LinkInfo">{self.targetFile.path}</span>
40 <td highlight="DownloadState" key="state"/>
41 <td highlight="DownloadButtons Buttons">
42 <a highlight="Button" href="javascript:0" key="pause">{_("download.action.Pause")}</a>
43 <a highlight="Button" href="javascript:0" key="remove">{_("download.action.Remove")}</a>
44 <a highlight="Button" href="javascript:0" key="resume">{_("download.action.Resume")}</a>
45 <a highlight="Button" href="javascript:0" key="retry">{_("download.action.Retry")}</a>
46 <a highlight="Button" href="javascript:0" key="cancel">{_("download.action.Cancel")}</a>
47 <a highlight="Button" href="javascript:0" key="delete">{_("download.action.Delete")}</a>
49 <td highlight="DownloadProgress" key="progress">
50 <span highlight="DownloadProgressHave" key="progressHave"
51 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
53 <td highlight="DownloadPercent" key="percent"/>
54 <td highlight="DownloadSpeed" key="speed"/>
55 <td highlight="DownloadTime" key="time"/>
56 <td><a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a></td>
58 this.list.document, this.nodes);
60 this.nodes.launch.addEventListener("click", function (event) {
61 if (event.button == 0) {
62 event.preventDefault();
63 self.command("launch");
71 get status() states[this.state],
73 inState: function inState(states) states.indexOf(this.status) >= 0,
75 get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
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"])
88 command: function command(name) {
89 util.assert(Set.has(this.allowedCommands, name), _("download.unknownCommand"));
90 util.assert(this.allowedCommands[name], _("download.commandNotAllowed"));
92 if (Set.has(this.commands, name))
93 this.commands[name].call(this);
95 services.downloadManager[name + "Download"](this.id);
99 delete: function delete_() {
100 this.targetFile.remove(false);
103 launch: function launch() {
105 // Behavior mimics that of the builtin Download Manager.
108 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
109 this.MIMEInfo.launchWithFile(file);
114 services.externalProtocol.loadUrl(this.target);
118 let file = io.File(this.targetFile);
119 if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
120 this.list.modules.commandline.input(_("download.prompt.launchExecutable") + " ",
122 if (/^a(lways)$/i.test(resp)) {
123 prefs.set("browser.download.manager.alertOnEXEOpen", false);
126 if (/^y(es)?$/i.test(resp))
135 active: function (a, b) a.alive - b.alive,
136 complete: function (a, b) a.percentComplete - b.percentComplete,
137 date: function (a, b) a.startTime - b.startTime,
138 filename: function (a, b) String.localeCompare(a.targetFile.leafName, b.targetFile.leafName),
139 size: function (a, b) a.size - b.size,
140 speed: function (a, b) a.speed - b.speed,
141 time: function (a, b) a.timeRemaining - b.timeRemaining,
142 url: function (a, b) String.localeCompare(a.source.spec, b.source.spec)
145 compare: function compare(other) values(this.list.sortOrder).map(function (order) {
146 let val = this._compare[order.substr(1)](this, other);
148 return (order[0] == "-") ? -val : val;
149 }, this).nth(util.identity, 0) || 0,
151 timeRemaining: Infinity,
153 updateProgress: function updateProgress() {
154 let self = this.__proto__;
156 if (this.amountTransferred === this.size) {
157 this.nodes.speed.textContent = "";
158 this.nodes.time.textContent = "";
161 this.nodes.speed.textContent = util.formatBytes(this.speed, 1, true) + "/s";
163 if (this.speed == 0 || this.size == 0)
164 this.nodes.time.textContent = _("download.unknown");
166 let seconds = (this.size - this.amountTransferred) / this.speed;
167 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
168 if (this.timeRemaining)
169 this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
171 this.nodes.time.textContent = _("download.almostDone");
175 let total = this.nodes.progressTotal.textContent = this.size ? util.formatBytes(this.size, 1, true) : _("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, "");
179 this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
182 updateStatus: function updateStatus() {
184 this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
186 this.nodes.row.setAttribute("status", this.status);
187 this.nodes.state.textContent = util.capitalize(this.status);
189 for (let node in values(this.nodes))
193 this.updateProgress();
197 var DownloadList = Class("DownloadList",
198 XPCOM([Ci.nsIDownloadProgressListener,
200 Ci.nsISupportsWeakReference]), {
201 init: function init(modules, filter, sort) {
202 this.sortOrder = sort;
203 this.modules = modules;
204 this.filter = filter && filter.toLowerCase();
211 cleanup: function cleanup() {
212 this.observe.unregister();
213 services.downloadManager.removeListener(this);
216 message: Class.memoize(function () {
218 util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
219 <tr highlight="DownloadHead">
220 <span>{_("title.Title")}</span>
221 <span>{_("title.Status")}</span>
223 <span>{_("title.Progress")}</span>
225 <span>{_("title.Speed")}</span>
226 <span>{_("title.Time remaining")}</span>
227 <span>{_("title.Source")}</span>
229 <tr highlight="Download"><span><div style="min-height: 1ex; /* FIXME */"/></span></tr>
230 <tr highlight="Download" key="totals" active="true">
231 <td><span highlight="Title">{_("title.Totals")}:</span> <span key="total"/></td>
233 <td highlight="DownloadButtons">
234 <a highlight="Button" href="javascript:0" key="clear">{_("download.action.Clear")}</a>
236 <td highlight="DownloadProgress" key="progress">
237 <span highlight="DownloadProgressHave" key="progressHave"
238 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
240 <td highlight="DownloadPercent" key="percent"/>
241 <td highlight="DownloadSpeed" key="speed"/>
242 <td highlight="DownloadTime" key="time"/>
245 </table>, this.document, this.nodes);
247 for (let row in iter(services.downloadManager.DBConnection
248 .createStatement("SELECT id FROM moz_downloads")))
249 this.addDownload(row.id);
252 util.addObserver(this);
253 services.downloadManager.addListener(this);
254 return this.nodes.list;
257 addDownload: function addDownload(id) {
258 if (!(id in this.downloads)) {
259 let download = Download(id, this);
260 if (this.filter && download.displayName.indexOf(this.filter) === -1)
263 this.downloads[id] = download;
264 let index = values(this.downloads).sort(function (a, b) a.compare(b))
267 this.nodes.list.insertBefore(download.nodes.row,
268 this.nodes.list.childNodes[index + 1]);
271 removeDownload: function removeDownload(id) {
272 if (id in this.downloads) {
273 this.nodes.list.removeChild(this.downloads[id].nodes.row);
274 delete this.downloads[id];
278 leave: function leave(stack) {
283 allowedCommands: Class.memoize(function () let (self = this) ({
284 get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
289 services.downloadManager.cleanUp();
293 sort: function sort() {
294 let list = values(this.downloads).sort(function (a, b) a.compare(b));
296 for (let [i, download] in iter(list))
297 if (this.nodes.list.childNodes[i + 1] != download.nodes.row)
298 this.nodes.list.insertBefore(download.nodes.row,
299 this.nodes.list.childNodes[i + 1]);
302 shouldSort: function shouldSort() Array.some(arguments, function (val) this.sortOrder.some(function (v) v.substr(1) == val), this),
304 update: function update() {
305 for (let node in values(this.nodes))
306 if (node.update && node.update != update)
308 this.updateProgress();
310 let event = this.document.createEvent("Events");
311 event.initEvent("dactyl-commandupdate", true, false);
312 this.document.dispatchEvent(event);
315 timeRemaining: Infinity,
317 updateProgress: function updateProgress() {
318 let downloads = values(this.downloads).toArray();
320 let self = Object.create(this);
321 for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
322 this[prop] = downloads.reduce(function (acc, dl) dl[prop] + acc, 0);
324 Download.prototype.updateProgress.call(self);
326 let active = downloads.filter(function (dl) dl.alive).length;
328 this.nodes.total.textContent = _("download.nActive", active);
329 else for (let key in values(["total", "percent", "speed", "time"]))
330 this.nodes[key].textContent = "";
332 if (this.shouldSort("complete", "size", "speed", "time"))
337 "download-manager-remove-download": function (id) {
339 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
341 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
343 Array.concat(id).map(this.closure.removeDownload);
348 onDownloadStateChange: function (state, download) {
350 if (download.id in this.downloads)
351 this.downloads[download.id].updateStatus();
353 this.addDownload(download.id);
355 this.modules.mow.resize(false);
356 this.nodes.list.scrollIntoView(false);
360 if (this.shouldSort("active"))
368 onProgressChange: function (webProgress, request,
369 curProgress, maxProgress,
370 curTotalProgress, maxTotalProgress,
373 if (download.id in this.downloads)
374 this.downloads[download.id].updateProgress();
375 this.updateProgress();
383 var Downloads = Module("downloads", {
386 commands: function initCommands(dactyl, modules, window) {
387 const { commands, CommandOption } = modules;
389 commands.add(["downl[oads]", "dl"],
390 "Display the downloads list",
392 let downloads = DownloadList(modules, args[0], args["-sort"]);
393 modules.commandline.echo(downloads);
399 names: ["-sort", "-s"],
400 description: "Sort order (see 'downloadsort')",
401 type: CommandOption.LIST,
402 get default() modules.options["downloadsort"],
403 completer: function (context, args) modules.options.get("downloadsort").completer(context, { values: args["-sort"] }),
404 validator: function (value) modules.options.get("downloadsort").validator(value)
409 commands.add(["dlc[lear]"],
410 "Clear completed downloads",
411 function (args) { services.downloadManager.cleanUp(); });
413 options: function initOptions(dactyl, modules, window) {
414 const { options } = modules;
417 options.add(["downloadcolumns", "dlc"],
418 "The columns to show in the download manager",
419 "stringlist", "filename,state,buttons,progress,percent,time,url",
422 buttons: "Control buttons",
423 filename: "Target filename",
424 percent: "Percent complete",
426 speed: "Download speed",
427 state: "The download's state",
428 time: "Time remaining",
433 options.add(["downloadsort", "dlsort", "dls"],
434 ":downloads sort order",
435 "stringlist", "-active,+filename",
438 active: "Whether download is active",
439 complete: "Percent complete",
440 date: "Date and time the download began",
441 filename: "Target filename",
443 speed: "Download speed",
444 time: "Time remaining",
448 completer: function (context, extra) {
449 let seen = Set.has(Set(extra.values.map(function (val) val.substr(1))));
451 context.completions = iter(this.values).filter(function ([k, v]) !seen(k))
452 .map(function ([k, v]) [["+" + k, [v, " (", _("sort.ascending"), ")"].join("")],
453 ["-" + k, [v, " (", _("sort.descending"), ")"].join("")]])
457 has: function () Array.some(arguments, function (val) this.value.some(function (v) v.substr(1) == val)),
459 validator: function (value) {
461 return value.every(function (val) /^[+-]/.test(val) && Set.has(this.values, val.substr(1))
462 && !Set.add(seen, val.substr(1)),
463 this) && value.length;
471 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
473 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: