]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/downloads.jsm
403c1e418a4642ec6867d1a6b47babb0b8557549
[dactyl.git] / common / modules / downloads.jsm
1 // Copyright (c) 2011 by 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 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("downloads", {
9     exports: ["Download", "Downloads", "downloads"],
10     use: ["io", "prefs", "services", "util"]
11 }, this);
12
13 Cu.import("resource://gre/modules/DownloadUtils.jsm", this);
14
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))
19                 .toObject();
20
21 var Download = Class("Download", {
22     init: function init(id, list) {
23         let self = XPCSafeJSObjectWrapper(services.downloadManager.getDownload(id));
24         self.__proto__ = this;
25         this.instance = this;
26         this.list = list;
27
28         this.nodes = {
29             commandTarget: self
30         };
31         util.xmlToDom(
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>
38                     </span>
39                 </td>
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>
48                 </td>
49                 <td highlight="DownloadProgress" key="progress">
50                     <span highlight="DownloadProgressHave" key="progressHave"
51                     />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
52                 </td>
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>
56             </tr>,
57             this.list.document, this.nodes);
58
59         self.updateStatus();
60         return self;
61     },
62
63     get status() states[this.state],
64
65     inState: function inState(states) states.indexOf(this.status) >= 0,
66
67     get alive() this.inState(["downloading", "notstarted", "paused", "queued", "scanning"]),
68
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"])
78     })),
79
80     command: function command(name) {
81         util.assert(set.has(this.allowedCommands, name), "Unknown command");
82         util.assert(this.allowedCommands[name], "Command not allowed");
83
84         services.downloadManager[name + "Download"](this.id);
85     },
86
87     commands: {
88         delete: function delete_() {
89             this.targetFile.remove(false);
90             this.updateStatus();
91         },
92         launch: function launch() {
93             let self = this;
94             // Behavior mimics that of the builtin Download Manager.
95             function action() {
96                 try {
97                     if (this.MIMEInfo && this.MIMEInfo.preferredAction == this.MIMEInfo.useHelperApp)
98                         this.MIMEInfo.launchWithFile(file)
99                     else
100                         file.launch();
101                 }
102                 catch (e) {
103                     services.externalProtocol.loadUrl(this.target);
104                 }
105             }
106
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) ",
110                     function (resp) {
111                         if (/^a(lways)$/i.test(resp)) {
112                             prefs.set("browser.download.manager.alertOnEXEOpen", false);
113                             resp = "yes";
114                         }
115                         if (/^y(es)?$/i.test(resp))
116                             action.call(self);
117                     });
118             else
119                 action.call(this);
120         }
121     },
122
123     compare: function compare(other) String.localeCompare(this.displayName, other.displayName),
124
125     timeRemaining: Infinity,
126
127     updateProgress: function updateProgress() {
128         let self = this.__proto__;
129
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";
134         else {
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);
139             else
140                 this.nodes.time.textContent = "~1 second";
141         }
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, "");
145
146         this.nodes.percent.textContent = this.size ? Math.round(this.amountTransferred * 100 / this.size) + "%" : "";
147     },
148
149     updateStatus: function updateStatus() {
150
151         this.nodes.row[this.alive ? "setAttribute" : "removeAttribute"]("active", "true");
152
153         this.nodes.row.setAttribute("status", this.status);
154         this.nodes.state.textContent = util.capitalize(this.status);
155
156         for (let node in values(this.nodes))
157             if (node.update)
158                 node.update();
159
160         this.updateProgress();
161     }
162 });
163
164 var DownloadList = Class("DownloadList",
165                          XPCOM([Ci.nsIDownloadProgressListener,
166                                 Ci.nsIObserver,
167                                 Ci.nsISupportsWeakReference]), {
168     init: function init(modules, filter) {
169         this.modules = modules;
170         this.filter = filter && filter.toLowerCase();
171         this.nodes = {
172             commandTarget: this
173         };
174         this.downloads = {};
175     },
176     cleanup: function cleanup() {
177         this.observe.unregister();
178         services.downloadManager.removeListener(this);
179     },
180
181     message: Class.memoize(function () {
182
183         util.xmlToDom(<table highlight="Downloads" key="list" xmlns={XHTML}>
184                         <tr highlight="DownloadHead">
185                             <span>Title</span>
186                             <span>Status</span>
187                             <span/>
188                             <span>Progress</span>
189                             <span/>
190                             <span>Time remaining</span>
191                             <span>Source</span>
192                         </tr>
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>&#xa0;<span key="total"/></td>
196                             <td/>
197                             <td highlight="DownloadButtons">
198                                 <a highlight="Button" key="clear">Clear</a>
199                             </td>
200                             <td highlight="DownloadProgress" key="progress">
201                                 <span highlight="DownloadProgressHave" key="progressHave"
202                                 />/<span highlight="DownloadProgressTotal" key="progressTotal"/>
203                             </td>
204                             <td highlight="DownloadPercent" key="percent"/>
205                             <td highlight="DownloadTime" key="time"/>
206                             <td/>
207                         </tr>
208                       </table>, this.document, this.nodes);
209
210         for (let row in iter(services.downloadManager.DBConnection
211                                      .createStatement("SELECT id FROM moz_downloads")))
212             this.addDownload(row.id);
213         this.update();
214
215         util.addObserver(this);
216         services.downloadManager.addListener(this);
217         return this.nodes.list;
218     }),
219
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)
224                 return;
225
226             this.downloads[id] = download;
227             let index = values(this.downloads).sort(function (a, b) a.compare(b))
228                                               .indexOf(download);
229
230             this.nodes.list.insertBefore(download.nodes.row,
231                                          this.nodes.list.childNodes[index + 1]);
232         }
233     },
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];
238         }
239     },
240
241     leave: function leave(stack) {
242         if (stack.pop)
243             this.cleanup();
244     },
245
246     allowedCommands: Class.memoize(function () let (self = this) ({
247         get clear() values(self.downloads).some(function (dl) dl.allowedCommands.remove)
248     })),
249
250     commands: {
251         clear: function () {
252             services.downloadManager.cleanUp();
253         }
254     },
255
256     update: function update() {
257         for (let node in values(this.nodes))
258             if (node.update && node.update != update)
259                 node.update();
260         this.updateProgress();
261
262         let event = this.document.createEvent("Events");
263         event.initEvent("dactyl-commandupdate", true, false);
264         this.document.dispatchEvent(event);
265     },
266
267     timeRemaining: Infinity,
268
269     updateProgress: function updateProgress() {
270         let downloads = values(this.downloads).toArray();
271
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);
275
276         Download.prototype.updateProgress.call(self);
277
278         let active = downloads.filter(function (dl) dl.alive).length;
279         if (active)
280             this.nodes.total.textContent = active + " active";
281         else for (let key in values(["total", "percent", "time"]))
282             this.nodes[key].textContent = "";
283     },
284
285     observers: {
286         "download-manager-remove-download": function (id) {
287             if (id == null)
288                 id = [k for ([k, dl] in iter(this.downloads)) if (dl.allowedCommands.remove)];
289             else
290                 id = [id.QueryInterface(Ci.nsISupportsPRUint32).data];
291
292             Array.concat(id).map(this.closure.removeDownload);
293             this.update();
294         }
295     },
296
297     onDownloadStateChange: function (state, download) {
298         try {
299             if (download.id in this.downloads)
300                 this.downloads[download.id].updateStatus();
301             else {
302                 this.addDownload(download.id);
303
304                 this.modules.mow.resize(false);
305                 this.nodes.list.scrollIntoView(false);
306             }
307             this.update();
308         }
309         catch (e) {
310             util.reportError(e);
311         }
312     },
313     onProgressChange: function (webProgress, request,
314                                 curProgress, maxProgress,
315                                 curTotalProgress, maxTotalProgress,
316                                 download) {
317         try {
318             if (download.id in this.downloads)
319                 this.downloads[download.id].updateProgress();
320             this.updateProgress();
321         }
322         catch (e) {
323             util.reportError(e);
324         }
325     }
326 });
327
328 var Downloads = Module("downloads", {
329 }, {
330 }, {
331     commands: function (dactyl, modules, window) {
332         const { commands } = modules;
333
334         commands.add(["downl[oads]", "dl"],
335             "Display the downloads list",
336             function (args) {
337                 let downloads = DownloadList(modules, args[0]);
338                 modules.commandline.echo(downloads);
339             },
340             {
341                 argCount: "?"
342             });
343     }
344 });
345
346 endModule();
347
348 // catch(e){ if (isString(e)) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
349
350 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: