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", "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">
35 <a key="launch" dactyl:command="download.command"
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" key="pause">Pause</a>
43 <a highlight="Button" key="remove">Remove</a>
44 <a highlight="Button" key="resume">Resume</a>
45 <a highlight="Button" key="retry">Retry</a>
46 <a highlight="Button" key="cancel">Cancel</a>
47 <a highlight="Button" key="delete">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="DownloadTime" key="time"/>
55 <td><a highlight="DownloadSource" key="source" href={self.source.spec}>{self.source.spec}</a></td>
57 this.list.document, this.nodes);
63 get status() states[this.state],
65 inState: function inState(states) states.indexOf(this.status) >= 0,
67 get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
69 allowedCommands: Class.memoize(function () let (self = this) ({
70 get cancel() self.cancelable && self.inState(["downloading", "paused", "starting"]),
71 get delete() !this.cancel && self.targetFile.exists(),
72 get launch() self.targetFile.exists() && self.inState(["finished"]),
73 get pause() self.inState(["downloading"]),
74 get remove() self.inState(["blocked_parental", "blocked_policy",
75 "canceled", "dirty", "failed", "finished"]),
76 get resume() self.resumable && self.inState(["paused"]),
77 get retry() self.inState(["canceled", "failed"])
80 command: function command(name) {
81 util.assert(set.has(this.allowedCommands, name), "Unknown command");
82 util.assert(this.allowedCommands[name], "Command not allowed");
84 services.downloadManager[name + "Download"](this.id);
88 delete: function delete_() {
89 this.targetFile.remove(false);
92 launch: function launch() {
94 // Behavior mimics that of the builtin Download Manager.
97 if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
98 this.MIMEInfo.launchWithFile(file)
103 services.externalProtocol.loadUrl(this.target);
107 let file = io.File(this.targetFile);
108 if (file.isExecutable() && prefs.get("browser.download.manager.alertOnEXEOpen", true))
109 this.list.modules.commandline.input("This will launch an executable download. Continue? (yes/[no]/always) ",
111 if (/^a(lways)$/i.test(resp)) {
112 prefs.set("browser.download.manager.alertOnEXEOpen", false);
115 if (/^y(es)?$/i.test(resp))
123 compare: function compare(other) String.localeCompare(this.displayName, other.displayName),
125 timeRemaining: Infinity,
127 updateProgress: function updateProgress() {
128 let self = this.__proto__;
130 if (this.amountTransferred === this.size)
131 this.nodes.time.textContent = "";
132 else if (this.speed == 0 || this.size == 0)
133 this.nodes.time.textContent = "Unknown";
135 let seconds = (this.size - this.amountTransferred) / this.speed;
136 [, self.timeRemaining] = DownloadUtils.getTimeLeft(seconds, this.timeRemaining);
137 if (this.timeRemaining)
138 this.nodes.time.textContent = util.formatSeconds(this.timeRemaining);
140 this.nodes.time.textContent = "~1 second";
142 let total = this.nodes.progressTotal.textContent = this.size ? util.formatBytes(this.size, 1, true) : "Unknown";
143 let suffix = RegExp(/( [a-z]+)?$/i.exec(total)[0] + "$");
144 this.nodes.progressHave.textContent = util.formatBytes(this.amountTransferred, 1, true).replace(suffix, "");
146 this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
149 updateStatus: function updateStatus() {
151 this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
153 this.nodes.row.setAttribute("status", this.status);
154 this.nodes.state.textContent = util.capitalize(this.status);
156 for (let node in values(this.nodes))
160 this.updateProgress();
164 var DownloadList = Class("DownloadList",
165 XPCOM([Ci.nsIDownloadProgressListener,
167 Ci.nsISupportsWeakReference]), {
168 init: function init(modules, filter) {
169 this.modules = modules;
170 this.filter = filter && filter.toLowerCase();
176 cleanup: function cleanup() {
177 this.observe.unregister();
178 services.downloadManager.removeListener(this);
181 message: Class.memoize(function () {
183 util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
184 <tr highlight="DownloadHead">
188 <span>Progress</span>
190 <span>Time remaining</span>
193 <tr highlight="Download"><span><div style="min-height: 1ex; /* FIXME */"/></span></tr>
194 <tr highlight="Download" key="totals" active="true">
195 <td><span highlight="Title">Totals:</span> <span key="total"/></td>
197 <td highlight="DownloadButtons">
198 <a highlight="Button" key="clear">Clear</a>
200 <td highlight="DownloadProgress" key="progress">
201 <span highlight="DownloadProgressHave" key="progressHave"
202 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
204 <td highlight="DownloadPercent" key="percent"/>
205 <td highlight="DownloadTime" key="time"/>
208 </table>, this.document, this.nodes);
210 for (let row in iter(services.downloadManager.DBConnection
211 .createStatement("SELECT id FROM moz_downloads")))
212 this.addDownload(row.id);
215 util.addObserver(this);
216 services.downloadManager.addListener(this);
217 return this.nodes.list;
220 addDownload: function addDownload(id) {
221 if (!(id in this.downloads)) {
222 let download = Download(id, this);
223 if (this.filter && download.displayName.indexOf(this.filter) === -1)
226 this.downloads[id] = download;
227 let index = values(this.downloads).sort(function (a, b) a.compare(b))
230 this.nodes.list.insertBefore(download.nodes.row,
231 this.nodes.list.childNodes[index + 1]);
234 removeDownload: function removeDownload(id) {
235 if (id in this.downloads) {
236 this.nodes.list.removeChild(this.downloads[id].nodes.row);
237 delete this.downloads[id];
241 leave: function leave(stack) {
246 allowedCommands: Class.memoize(function () let (self = this) ({
247 get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
252 services.downloadManager.cleanUp();
256 update: function update() {
257 for (let node in values(this.nodes))
258 if (node.update && node.update != update)
260 this.updateProgress();
262 let event = this.document.createEvent("Events");
263 event.initEvent("dactyl-commandupdate", true, false);
264 this.document.dispatchEvent(event);
267 timeRemaining: Infinity,
269 updateProgress: function updateProgress() {
270 let downloads = values(this.downloads).toArray();
272 let self = Object.create(this);
273 for (let prop in values(["amountTransferred", "size", "speed", "timeRemaining"]))
274 this[prop] = downloads.reduce(function (acc, dl) dl[prop] + acc, 0);
276 Download.prototype.updateProgress.call(self);
278 let active = downloads.filter(function (dl) dl.alive).length;
280 this.nodes.total.textContent = active + " active";
281 else for (let key in values(["total", "percent", "time"]))
282 this.nodes[key].textContent = "";
286 "download-manager-remove-download": function (id) {
288 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
290 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
292 Array.concat(id).map(this.closure.removeDownload);
297 onDownloadStateChange: function (state, download) {
299 if (download.id in this.downloads)
300 this.downloads[download.id].updateStatus();
302 this.addDownload(download.id);
304 this.modules.mow.resize(false);
305 this.nodes.list.scrollIntoView(false);
313 onProgressChange: function (webProgress, request,
314 curProgress, maxProgress,
315 curTotalProgress, maxTotalProgress,
318 if (download.id in this.downloads)
319 this.downloads[download.id].updateProgress();
320 this.updateProgress();
328 var Downloads = Module("downloads", {
331 commands: function (dactyl, modules, window) {
332 const { commands } = modules;
334 commands.add(["downl[oads]", "dl"],
335 "Display the downloads list",
337 let downloads = DownloadList(modules, args[0]);
338 modules.commandline.echo(downloads);
348 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
350 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: