download-manager.js: add "open URL" option to download prompt
[conkeror.git] / modules / download-manager.js
blobbe0c8138b0bdf50c8f54a5b4be1cf141f542b640
1 /**
2  * (C) Copyright 2008 Jeremy Maitin-Shepard
3  *
4  * Use, modification, and distribution are subject to the terms specified in the
5  * COPYING file.
6 **/
8 require("special-buffer.js");
9 require("mime-type-override.js");
10 require("minibuffer-read-mime-type.js");
12 var download_manager_service = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
13 //var download_manager_ui = Cc["@mozilla.org/download-manager-ui;1"].getService(Ci.nsIDownloadManagerUI);
14 var download_manager_builtin_ui = Components.classesByID["{7dfdf0d1-aff6-4a34-bad1-d0fe74601642}"]
15     .getService(Ci.nsIDownloadManagerUI);
17 /* This implements nsIHelperAppLauncherDialog interface. */
18 function download_helper()
20 download_helper.prototype = {
21     QueryInterface: generate_QI(Ci.nsIHelperAppLauncherDialog, Ci.nsIWebProgressListener2),
23     handle_show: function () {
24         var action_chosen = false;
26         var can_view_internally = this.frame != null &&
27                 can_override_mime_type_for_uri(this.launcher.source);
28         try {
29             this.panel = create_info_panel(this.window, "download-panel",
30                                            [["downloading", "Downloading:", this.launcher.source.spec],
31                                             ["mime-type", "Mime type:", this.launcher.MIMEInfo.MIMEType]]);
32             var action = yield this.window.minibuffer.read_single_character_option(
33                 $prompt = "Action to perform: (s: save; o: open; O: open URL; " +
34                     (can_view_internally ? "i: view internally; t: view as text)" : ")"),
35                 $options = (can_view_internally ? ["s", "o", "O", "i", "t"] : ["s", "o", "O"]));
37             if (action == "s") {
38                 var suggested_path = suggest_save_path_from_file_name(this.launcher.suggestedFileName, this.buffer);
39                 var file = yield this.window.minibuffer.read_file_check_overwrite(
40                     $prompt = "Save to file:",
41                     $initial_value = suggested_path,
42                     $select);
43                 register_download(this.buffer, this.launcher.source);
44                 this.launcher.saveToDisk(file, false);
45                 action_chosen = true;
47             } else if (action == "o") {
48                 var cwd = this.buffer ? this.buffer.cwd : default_directory.path;
49                 var mime_type = this.launcher.MIMEInfo.MIMEType;
50                 var suggested_action = get_external_handler_for_mime_type(mime_type);
51                 var command = yield this.window.minibuffer.read_shell_command(
52                     $initial_value = suggested_action,
53                     $cwd = cwd);
54                 var file = get_temporary_file(this.launcher.suggestedFileName);
55                 var info = register_download(this.buffer, this.launcher.source);
56                 info.temporary_status = DOWNLOAD_TEMPORARY_FOR_COMMAND;
57                 info.set_shell_command(command, cwd);
58                 this.launcher.saveToDisk(file, false);
59                 action_chosen = true;
60             } else if (action == "O") {
61                 action_chosen = true;
62                 this.abort(); // abort download
63                 let mime_type = this.launcher.MIMEInfo.MIMEType;
64                 let cwd = this.buffer ? this.buffer.cwd : this.window.buffers.current.cwd;
65                 let cmd = yield this.window.minibuffer.read_shell_command(
66                     $cwd = cwd,
67                     $initial_value = get_external_handler_for_mime_type(mime_type));
68                 shell_command_with_argument_blind(cmd, this.launcher.source.spec, $cwd = cwd);
69             } else /* if (action == "i" || action == "t") */ {
70                 let mime_type;
71                 if (action == "t")
72                     mime_type = "text/plain";
73                 else {
74                     let suggested_type = this.launcher.MIMEInfo.MIMEType;
75                     if (gecko_viewable_mime_type_list.indexOf(suggested_type) == -1)
76                         suggested_type = "text/plain";
77                     mime_type = yield this.window.minibuffer.read_gecko_viewable_mime_type(
78                         $prompt = "View internally as",
79                         $initial_value = suggested_type,
80                         $select);
81                 }
82                 action_chosen = true;
83                 this.abort(); // abort before reloading
85                 override_mime_type_for_next_load(this.launcher.source, mime_type);
86                 this.frame.location = this.launcher.source.spec; // reload
87             }
88         } catch (e) {
89             handle_interactive_error(this.window, e);
90         } finally {
91             if (!action_chosen)
92                 this.abort();
93             this.cleanup();
94         }
95     },
97     show : function (launcher, context, reason) {
98         this.launcher = launcher;
100         // Get associated buffer; if that fails (hopefully not), just get any window
101         var buffer = null;
102         var window = null;
103         var frame = null;
104         try {
105             frame = context.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal);
106             window = get_window_from_frame(frame.top);
107             if (window)
108                 buffer = get_buffer_from_frame(window, frame);
109         } catch (e) {
110             window = get_recent_conkeror_window();
112             if (window == null) {
113                 // FIXME: need to handle this case perhaps where no windows exist
114                 this.abort(); // for now, just cancel the download
115                 return;
116             }
117         }
119         this.frame = frame;
120         this.window = window;
121         this.buffer = buffer;
123         co_call(this.handle_show());
124     },
126     abort : function () {
127         const NS_BINDING_ABORTED = 0x804b0002;
128         this.launcher.cancel(NS_BINDING_ABORTED);
129     },
131     cleanup : function () {
132         if (this.panel)
133             this.panel.destroy();
134         this.panel = null;
135         this.launcher = null;
136         this.window = null;
137         this.buffer = null;
138         this.frame = null;
139     },
141     promptForSaveToFile : function(launcher, context, default_file, suggested_file_extension) {
142         return null;
143     }
147 var unmanaged_download_info_list = [];
148 var id_to_download_info = {};
150 // Import these constants for convenience
151 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
152 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
153 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
154 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
155 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
156 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
157 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
158 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
159 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
162 const DOWNLOAD_NOT_TEMPORARY = 0;
163 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
164 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
166 function download_info(source_buffer, mozilla_info) {
167     this.source_buffer = source_buffer;
168     if (mozilla_info != null)
169         this.attach(mozilla_info);
171 download_info.prototype = {
172     attach : function (mozilla_info) {
173         this.mozilla_info = mozilla_info;
174         id_to_download_info[mozilla_info.id] = this;
175         download_added_hook.run(this);
176     },
178     shell_command : null,
180     shell_command_cwd : null,
182     temporary_status : DOWNLOAD_NOT_TEMPORARY,
184     action_description : null,
186     set_shell_command : function (str, cwd) {
187         this.shell_command = str;
188         this.shell_command_cwd = cwd;
189         if (this.mozilla_info)
190             download_shell_command_change_hook.run(this);
191     },
193     /**
194      * None of the following members may be used until attach is called
195      */
197     // Reflectors to properties of nsIDownload
198     get state () { return this.mozilla_info.state; },
199     get target_file () { return this.mozilla_info.targetFile; },
200     get amount_transferred () { return this.mozilla_info.amountTransferred; },
201     get percent_complete () { return this.mozilla_info.percentComplete; },
202     get size () {
203         var s = this.mozilla_info.size;
204         /* nsIDownload.size is a PRUint64, and will have value
205          * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
206          * JavaScript only has a double numerical type, this value
207          * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
208         if (s < 68719476736 /* 2^36 */)
209             return s;
210         return -1;
211     },
212     get source () { return this.mozilla_info.source; },
213     get start_time () { return this.mozilla_info.startTime; },
214     get speed () { return this.mozilla_info.speed; },
215     get MIME_info () { return this.mozilla_info.MIMEInfo; },
216     get MIME_type () {
217         if (this.MIME_info)
218             return this.MIME_info.MIMEType;
219         return null;
220     },
221     get id () { return this.mozilla_info.id; },
222     get referrer () { return this.mozilla_info.referrer; },
224     throw_if_removed : function () {
225         if (this.removed)
226             throw interactive_error("Download has already been removed from the download manager.");
227     },
229     throw_state_error : function () {
230         switch (this.state) {
231         case DOWNLOAD_DOWNLOADING:
232             throw interactive_error("Download is already in progress.");
233         case DOWNLOAD_FINISHED:
234             throw interactive_error("Download has already completed.");
235         case DOWNLOAD_FAILED:
236             throw interactive_error("Download has already failed.");
237         case DOWNLOAD_CANCELED:
238             throw interactive_error("Download has already been canceled.");
239         case DOWNLOAD_PAUSED:
240             throw interactive_error("Download has already been paused.");
241         case DOWNLOAD_QUEUED:
242             throw interactive_error("Download is queued.");
243         default:
244             throw new Error("Download has unexpected state: " + this.state);
245         }
246     },
248     // Download manager operations
249     cancel : function ()  {
250         this.throw_if_removed();
251         switch (this.state) {
252         case DOWNLOAD_DOWNLOADING:
253         case DOWNLOAD_PAUSED:
254         case DOWNLOAD_QUEUED:
255             try {
256                 download_manager_service.cancelDownload(this.id);
257             } catch (e) {
258                 throw interactive_error("Download cannot be canceled.");
259             }
260             break;
261         default:
262             this.throw_state_error();
263         }
264     },
266     retry : function () {
267         this.throw_if_removed();
268         switch (this.state) {
269         case DOWNLOAD_CANCELED:
270         case DOWNLOAD_FAILED:
271             try {
272                 download_manager_service.retryDownload(this.id);
273             } catch (e) {
274                 throw interactive_error("Download cannot be retried.");
275             }
276             break;
277         default:
278             this.throw_state_error();
279         }
280     },
282     resume : function () {
283         this.throw_if_removed();
284         switch (this.state) {
285         case DOWNLOAD_PAUSED:
286             try {
287                 download_manager_service.resumeDownload(this.id);
288             } catch (e) {
289                 throw interactive_error("Download cannot be resumed.");
290             }
291             break;
292         default:
293             this.throw_state_error();
294         }
295     },
297     pause : function () {
298         this.throw_if_removed();
299         switch (this.state) {
300         case DOWNLOAD_DOWNLOADING:
301         case DOWNLOAD_QUEUED:
302             try {
303                 download_manager_service.pauseDownload(this.id);
304             } catch (e) {
305                 throw interactive_error("Download cannot be paused.");
306             }
307             break;
308         default:
309             this.throw_state_error();
310         }
311     },
313     remove : function () {
314         this.throw_if_removed();
315         switch (this.state) {
316         case DOWNLOAD_FAILED:
317         case DOWNLOAD_CANCELED:
318         case DOWNLOAD_FINISHED:
319             try {
320                 download_manager_service.removeDownload(this.id);
321             } catch (e) {
322                 throw interactive_error("Download cannot be removed.");
323             }
324             break;
325         default:
326             throw interactive_error("Download is still in progress.");
327         }
328     },
330     delete_target : function () {
331         if (this.state != DOWNLOAD_FINISHED)
332             throw interactive_error("Download has not finished.");
333         try {
334             this.target_file.remove(false);
335         } catch (e) {
336             if (result in e) {
337                 switch (e) {
338                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
339                     throw interactive_error("File has already been deleted.");
340                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
341                     throw interactive_error("Access denied");
342                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
343                     throw interactive_error("Failed to delete file.");
344                 }
345             }
346             throw e;
347         }
348     }
351 var define_download_local_hook = simple_local_hook_definer();
353 // FIXME: add more parameters
354 function register_download(buffer, source_uri) {
355     var info = new download_info(buffer);
356     info.registered_time_stamp = Date.now();
357     info.registered_source_uri = source_uri;
358     unmanaged_download_info_list.push(info);
359     return info;
362 function match_registered_download(mozilla_info) {
363     let list = unmanaged_download_info_list;
364     let t = Date.now();
365     for (let i = 0; i < list.length; ++i) {
366         let x = list[i];
367         if (x.registered_source_uri == mozilla_info.source) {
368             list.splice(i, 1);
369             return x;
370         }
371         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
372             list.splice(i, 1);
373             --i;
374             continue;
375         }
376     }
377     return null;
380 define_download_local_hook("download_added_hook");
381 define_download_local_hook("download_removed_hook");
382 define_download_local_hook("download_finished_hook");
383 define_download_local_hook("download_progress_change_hook");
384 define_download_local_hook("download_state_change_hook");
385 define_download_local_hook("download_shell_command_change_hook");
387 var download_info_max_queue_delay = 100;
389 var download_progress_listener = {
390     QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
392     onDownloadStateChange : function (state, download) {
393         var info = null;
394         /* FIXME: Determine if only new downloads will have this state
395          * as their previous state. */
397         dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
399         if (state == DOWNLOAD_NOTSTARTED) {
400             info = match_registered_download(download);
401             if (info == null) {
402                 info = new download_info(null, download);
403                 dumpln("error: encountered unknown new download");
404             } else {
405                 info.attach(download);
406             }
407         } else {
408             info = id_to_download_info[download.id];
409             if (info == null) {
410                 dumpln("Error: encountered unknown download");
412             } else {
413                 info.mozilla_info = download;
414                 download_state_change_hook.run(info);
415                 if (info.state == DOWNLOAD_FINISHED) {
416                     download_finished_hook.run(info);
418                     if (info.shell_command != null) {
419                         info.running_shell_command = true;
420                         co_call(function () {
421                             try {
422                                 yield shell_command_with_argument(info.shell_command,
423                                                                   info.target_file.path,
424                                                                   $cwd = info.shell_command_cwd);
425                             } finally  {
426                                 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
427                                     info.target_file.remove(false /* not recursive */);
428                                 info.running_shell_command = false;
429                                 download_shell_command_change_hook.run(info);
430                             }
431                         }());
432                         download_shell_command_change_hook.run(info);
433                     }
434                 }
435             }
436         }
437     },
439     onProgressChange : function (progress, request, cur_self_progress, max_self_progress,
440                                  cur_total_progress, max_total_progress,
441                                  download) {
442         var info = id_to_download_info[download.id];
443         if (info == null) {
444             dumpln("error: encountered unknown download in progress change");
445             return;
446         }
447         info.mozilla_info = download;
448         download_progress_change_hook.run(info);
449         //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
450         // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
451     },
453     onSecurityChange : function (progress, request, state, download) {
454     },
456     onStateChange : function (progress, request, state_flags, status, download) {
457     }
460 var download_observer = {
461     observe : function(subject, topic, data) {
462         switch(topic) {
463         case "download-manager-remove-download":
464             var ids = [];
465             if (!subject) {
466                 // Remove all downloads
467                 for (let i in id_to_download_info)
468                     ids.push(i);
469             } else {
470                 let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
471                 /* FIXME: determine if this should really be an error */
472                 if (!(id in id_to_download_info)) {
473                     dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
474                 } else
475                     ids.push(id);
476             }
477             for each (let i in ids) {
478                 dumpln("deleting download: " + i);
479                 let d = id_to_download_info[i];
480                 d.removed = true;
481                 download_removed_hook.run(d);
482                 delete id_to_download_info[i];
483             }
484             break;
485         }
486     }
488 observer_service.addObserver(download_observer, "download-manager-remove-download", false);
490 download_manager_service.addListener(download_progress_listener);
492 function pretty_print_file_size(val) {
493     const GIBI = 1073741824; /* 2^30 */
494     const MEBI = 1048576; /* 2^20 */
495     const KIBI = 1024; /* 2^10 */
496     var suffix, div;
497     if (val < KIBI) {
498         div = 1;
499         suffix = "B";
500     }
501     else if (val < MEBI) {
502         suffix = "KiB";
503         div = KIBI;
504     } else if (val < GIBI) {
505         suffix = "MiB";
506         div = MEBI;
507     } else {
508         suffix = "GiB";
509         div = GIBI;
510     }
511     val = val / div;
512     var precision = 2;
513     if (val > 10)
514         precision = 1;
515     if (val > 100)
516         precision = 0;
517     return [val.toFixed(precision), suffix];
520 function pretty_print_time(val) {
521     val = Math.round(val);
522     var seconds = val % 60;
524     val = Math.floor(val / 60);
526     var minutes = val % 60;
528     var hours = Math.floor(val / 60);
530     var parts = [];
532     if (hours > 1)
533         parts.push(hours + " hours");
534     else if (hours == 1)
535         parts.push("1 hour");
537     if (minutes > 1)
538         parts.push(minutes + " minutes");
539     else if (minutes == 1)
540         parts.push("1 minute");
542     if (minutes <= 1 && hours == 0) {
543         if (seconds != 1)
544             parts.push(seconds + " seconds");
545         else
546             parts.push("1 second");
547     }
549     return parts.join(", ");
552 define_variable(
553     "download_buffer_min_update_interval", 2000,
554     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
555         "Lowering this interval will increase the promptness of the progress display at " +
556         "the cost of using additional processor time.");
558 define_keywords("$info");
559 function download_buffer(window, element) {
560     this.constructor_begin();
561     keywords(arguments);
562     special_buffer.call(this, window, element, forward_keywords(arguments));
563     this.info = arguments.$info;
564     this.configuration.cwd = this.info.mozilla_info.targetFile.parent.path;
565     this.description = this.info.mozilla_info.source.spec;
566     this.keymap = download_buffer_keymap;
567     this.update_title();
569     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
570     add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
571     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
572     this.command_change_handler_fn = method_caller(this, this.update_command_field);
573     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
574     this.constructor_end();
576 download_buffer.prototype = {
577     __proto__: special_buffer.prototype,
579     handle_kill : function () {
580         this.__proto__.handle_kill();
581         remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
582         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
583         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
585         // Remove all node references
586         delete this.status_textnode;
587         delete this.transferred_div_node;
588         delete this.transferred_textnode;
589         delete this.progress_container_node;
590         delete this.progress_bar_node;
591         delete this.time_textnode;
592         delete this.command_div_node;
593         delete this.command_label_textnode;
594         delete this.command_textnode;
595     },
597     update_title : function () {
598         // FIXME: do this properly
599         var new_title;
600         var info = this.info;
601         var append_transfer_info = false;
602         var append_speed_info = true;
603         var label = null;
604         switch(info.state) {
605         case DOWNLOAD_DOWNLOADING:
606             label = "Downloading";
607             append_transfer_info = true;
608             break;
609         case DOWNLOAD_FINISHED:
610             label = "Download complete";
611             break;
612         case DOWNLOAD_FAILED:
613             label = "Download failed";
614             append_transfer_info = true;
615             append_speed_info = false;
616             break;
617         case DOWNLOAD_CANCELED:
618             label = "Download canceled";
619             append_transfer_info = true;
620             append_speed_info = false;
621             break;
622         case DOWNLOAD_PAUSED:
623             label = "Download paused";
624             append_transfer_info = true;
625             append_speed_info = false;
626             break;
627         case DOWNLOAD_QUEUED:
628         default:
629             label = "Download queued";
630             break;
631         }
633         if (append_transfer_info) {
634             if (append_speed_info)
635                 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
636             else
637                 new_title = label + ": ";
638             var trans = pretty_print_file_size(info.amount_transferred);
639             if (info.size >= 0) {
640                 var total = pretty_print_file_size(info.size);
641                 if (trans[1] == total[1])
642                     new_title += trans[0] + "/" + total[0] + " " + total[1];
643                 else
644                     new_title += trans.join(" ") + "/" + total.join(" ");
645             } else
646                 new_title += trans.join(" ");
647             if (info.percent_complete >= 0)
648                 new_title += " (" + info.percent_complete + "%)";
649         } else
650             new_title = label;
651         if (new_title != this.title) {
652             this.title = new_title;
653             return true;
654         }
655         return false;
656     },
658     handle_progress_change : function () {
659         var cur_time = Date.now();
660         if (this.last_update == null ||
661             (cur_time - this.last_update) > download_buffer_min_update_interval ||
662             this.info.state != this.previous_state) {
664             if (this.update_title())
665                 buffer_title_change_hook.run(this);
667             if (this.generated) {
668                 this.update_fields();
669             }
670             this.previous_status = this.info.status;
671             this.last_update = cur_time;
672         }
673     },
675     generate: function() {
676         var d = this.document;
677         var g = new dom_generator(d, XHTML_NS);
679         /* Warning: If any additional node references are saved in
680          * this function, appropriate code to delete the saved
681          * properties must be added to handle_kill. */
683         var info = this.info;
685         d.body.setAttribute("class", "download-buffer");
687         g.add_stylesheet("chrome://conkeror/content/downloads.css");
689         var div;
690         var label, value;
692         div = g.element("div", d.body, "class", "download-info", "id", "download-source");
693         label = g.element("div", div, "class", "download-label");
694         this.status_textnode = g.text("", label);
695         value = g.element("div", div, "class", "download-value");
696         g.text(info.source.spec, value);
698         div = g.element("div", d.body, "class", "download-info", "id", "download-target");
699         label = g.element("div", div, "class", "download-label");
700         var target_label;
701         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
702             target_label = "Temp. file:";
703         else
704             target_label = "Target:";
705         g.text(target_label, label);
706         value = g.element("div", div, "class", "download-value");
707         g.text(info.target_file.path, value);
709         div = g.element("div", d.body, "class", "download-info", "id", "download-mime-type");
710         label = g.element("div", div, "class", "download-label");
711         g.text("MIME type:", label);
712         value = g.element("div", div, "class", "download-value");
713         g.text(info.MIME_type || "unknown", value);
715         this.transferred_div_node = div = g.element("div", d.body,
716                                                     "class", "download-info",
717                                                     "id", "download-transferred");
718         label = g.element("div", div, "class", "download-label");
719         g.text("Transferred:", label);
720         value = g.element("div", div, "class", "download-value");
721         this.transferred_textnode = g.text("", value);
722         this.progress_container_node = value = g.element("div", div, "id", "download-progress-container");
723         this.progress_bar_node = g.element("div", value, "id", "download-progress-bar");
724         value = g.element("div", div, "class", "download-value", "id", "download-percent");
725         this.percent_textnode = g.text("", value);
727         div = g.element("div", d.body, "class", "download-info", "id", "download-time");
728         label = g.element("div", div, "class", "download-label");
729         g.text("Time:", label);
730         value = g.element("div", div, "class", "download-value");
731         this.time_textnode = g.text("", value);
733         if (info.action_description != null) {
734             div = g.element("div", d.body, "class", "download-info", "id", "download-action");
735             label = g.element("div", div, "class", "download-label");
736             g.text("Action:", label);
737             value = g.element("div", div, "class", "download-value");
738             g.text(info.action_description, value);
739         }
741         this.command_div_node = div = g.element("div", d.body, "class", "download-info", "id", "download-command");
742         label = g.element("div", div, "class", "download-label");
743         this.command_label_textnode = g.text("Run command:", label);
744         value = g.element("div", div, "class", "download-value");
745         this.command_textnode = g.text("", value);
747         this.update_fields();
749         this.update_command_field();
750     },
752     update_fields : function () {
753         if (!this.generated)
754             return;
755         var info = this.info;
756         var label = null;
757         switch(info.state) {
758         case DOWNLOAD_DOWNLOADING:
759             label = "Downloading";
760             break;
761         case DOWNLOAD_FINISHED:
762             label = "Completed";
763             break;
764         case DOWNLOAD_FAILED:
765             label = "Failed";
766             break;
767         case DOWNLOAD_CANCELED:
768             label = "Canceled";
769             break;
770         case DOWNLOAD_PAUSED:
771             label = "Paused";
772             break;
773         case DOWNLOAD_QUEUED:
774         default:
775             label = "Queued";
776             break;
777         }
778         this.status_textnode.nodeValue = label + ":";
779         this.update_time_field();
781         var tran_text = "";
782         if (info.state == DOWNLOAD_FINISHED)
783             tran_text = pretty_print_file_size(info.size).join(" ");
784         else {
785             var trans = pretty_print_file_size(info.amount_transferred);
786             if (info.size >= 0) {
787                 var total = pretty_print_file_size(info.size);
788                 if (trans[1] == total[1])
789                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
790                 else
791                     tran_text += trans.join(" ") + "/" + total.join(" ");
792             } else
793                 tran_text += trans.join(" ");
794         }
795         this.transferred_textnode.nodeValue = tran_text;
796         if (info.percent_complete >= 0) {
797             this.progress_container_node.style.display = "";
798             this.percent_textnode.nodeValue = info.percent_complete + "%";
799             this.progress_bar_node.style.width = info.percent_complete + "%";
800         } else {
801             this.percent_textnode.nodeValue = "";
802             this.progress_container_node.style.display = "none";
803         }
805         this.update_command_field();
806     },
808     update_time_field : function () {
809         var info = this.info;
810         var elapsed_text = pretty_print_time((Date.now() - info.start_time / 1000) / 1000) + " elapsed";
811         var text = "";
812         if (info.state == DOWNLOAD_DOWNLOADING) {
813             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
814         }
815         if (info.state == DOWNLOAD_DOWNLOADING &&
816             info.size >= 0 &&
817             info.speed > 0) {
818             let remaining = (info.size - info.amount_transferred) / info.speed;
819             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
820         } else {
821             text = elapsed_text;
822         }
823         this.time_textnode.nodeValue = text;
824     },
826     update_command_field : function () {
827         if (!this.generated)
828             return;
829         if (this.info.shell_command != null) {
830             this.command_div_node.style.display = "";
831             var label;
832             if (this.info.running_shell_command)
833                 label = "Running:";
834             else if (this.info.state == DOWNLOAD_FINISHED)
835                 label = "Ran command:";
836             else
837                 label = "Run command:";
838             this.command_label_textnode.nodeValue = label;
839             this.command_textnode.nodeValue = this.info.shell_command;
840         } else {
841             this.command_div_node.style.display = "none";
842         }
843     }
846 function download_cancel(buffer) {
847     check_buffer(buffer, download_buffer);
848     var info = buffer.info;
849     info.cancel();
850     buffer.window.minibuffer.message("Download canceled");
852 interactive("download-cancel",
853             "Cancel the current download.\n" +
854             "The download can later be retried using the `download-retry' command, but any " +
855             "data already transferred will be lost.",
856             function (I) {download_cancel(I.buffer);});
858 function download_retry(buffer) {
859     check_buffer(buffer, download_buffer);
860     var info = buffer.info;
861     info.retry();
862     buffer.window.minibuffer.message("Download retried");
864 interactive("download-retry",
865             "Retry a failed or canceled download.\n" +
866             "This command can be used to retry a download that failed or was cancled using " +
867             "the `download-cancel' command.  The download will begin from the start again.",
868             function (I) {download_retry(I.buffer);});
870 function download_pause(buffer) {
871     check_buffer(buffer, download_buffer);
872     buffer.info.pause();
873     buffer.window.minibuffer.message("Download paused");
875 interactive("download-pause",
876             "Pause the current download.\n" +
877             "The download can later be resumed using the `download-resume' command.  The " +
878             "data already transferred will not be lost.",
879             function (I) {download_pause(I.buffer);});
881 function download_resume(buffer) {
882     check_buffer(buffer, download_buffer);
883     buffer.info.resume();
884     buffer.window.minibuffer.message("Download resumed");
886 interactive("download-resume",
887             "Resume the current download.\n" +
888             "This command can be used to resume a download paused using the `download-pause' command.",
889             function (I) {download_resume(I.buffer);});
891 function download_remove(buffer) {
892     check_buffer(buffer, download_buffer);
893     buffer.info.remove();
894     buffer.window.minibuffer.message("Download removed");
896 interactive("download-remove",
897             "Remove the current download from the download manager.\n" +
898             "This command can only be used on inactive (paused, canceled, completed, or failed) downloads.",
899             function (I) {download_remove(I.buffer);});
901 function download_retry_or_resume(buffer) {
902     check_buffer(buffer, download_buffer);
903     var info = buffer.info;
904     if (info.state == DOWNLOAD_PAUSED)
905         download_resume(buffer);
906     else
907         download_retry(buffer);
909 interactive("download-retry-or-resume",
910             "Retry or resume the current download.\n" +
911             "This command can be used to resume a download paused using the `download-pause' " +
912             "command or canceled using the `download-cancel' command.",
913             function (I) {download_retry_or_resume(I.buffer);});
915 function download_pause_or_resume(buffer) {
916     check_buffer(buffer, download_buffer);
917     var info = buffer.info;
918     if (info.state == DOWNLOAD_PAUSED)
919         download_resume(buffer);
920     else
921         download_pause(buffer);
923 interactive("download-pause-or-resume",
924             "Pause or resume the current download.\n" +
925             "This command toggles the paused state of the current download.",
926             function (I) {download_pause_or_resume(I.buffer);});
928 function download_delete_target(buffer) {
929     check_buffer(buffer, download_buffer);
930     var info = buffer.info;
931     info.delete_target();
932     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
934 interactive("download-delete-target",
935             "Delete the target file of the current download.\n"  +
936             "This command can only be used if the download has finished successfully.",
937             function (I) {download_delete_target(I.buffer);});
939 function download_shell_command(buffer, cwd, cmd) {
940     check_buffer(buffer, download_buffer);
941     var info = buffer.info;
942     if (info.state == DOWNLOAD_FINISHED) {
943         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
944         return;
945     }
946     if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED)
947         info.throw_state_error();
948     if (cmd == null || cmd.length == 0)
949         info.set_shell_command(null, cwd);
950     else
951         info.set_shell_command(cmd, cwd);
952     buffer.window.minibuffer.message("Queued shell command: " + cmd);
954 interactive("download-shell-command",
955             "Run a shell command on the target file of the current download.\n" +
956             "If the download is still in progress, the shell command will be queued " +
957             "to run when the download finishes.",
958             function (I) {
959                 var buffer = check_buffer(I.buffer, download_buffer);
960                 var cwd = buffer.info.shell_command_cwd || buffer.cwd;
961                 var cmd = yield I.minibuffer.read_shell_command(
962                     $cwd = cwd,
963                     $initial_value = buffer.info.shell_command ||
964                         get_external_handler_for_mime_type(buffer.info.MIME_type));
965                 download_shell_command(buffer, cwd, cmd);
966             });
968 function download_manager_ui()
970 download_manager_ui.prototype = {
971     QueryInterface : XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
973     getAttention : function () {},
974     show : function () {},
975     visible : false
979 function download_manager_show_builtin_ui(window) {
980     download_manager_builtin_ui.show(window);
982 interactive("download-manager-show-builtin-ui",
983             "Show the built-in (Firefox-style) download manager user interface.",
984             function (I) {download_manager_show_builtin_ui(I.window);});
988 define_variable("download_temporary_file_open_buffer_delay", 500,
989                      "Delay (in milliseconds) before a download buffer is opened for temporary downloads.\n" +
990                      "This variable takes effect only if `open_download_buffer_automatically' is in " +
991                      "`download_added_hook', as it is by default.");
994 define_variable("download_buffer_automatic_open_target", OPEN_NEW_WINDOW,
995                      "Target for download buffers created by the `open_download_buffer_automatically' function.\n" +
996                      "This variable takes effect only if `open_download_buffer_auotmatically' is in " +
997                      "`download_added_hook', as it is by default.");
999 function open_download_buffer_automatically(info) {
1000     var buf = info.source_buffer;
1001     var target = download_buffer_automatic_open_target;
1002     if (buf == null)
1003         target = OPEN_NEW_WINDOW;
1004     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
1005         !(download_temporary_file_open_buffer_delay > 0))
1006         create_buffer(buf, buffer_creator(download_buffer, $info = info), target);
1007     else {
1008         var timer = null;
1009         function finish() {
1010             timer.cancel();
1011         }
1012         add_hook.call(info, "download_finished_hook", finish);
1013         timer = call_after_timeout(function () {
1014                 remove_hook.call(info, "download_finished_hook", finish);
1015                 create_buffer(buf, buffer_creator(download_buffer, $info = info), target);
1016             }, download_temporary_file_open_buffer_delay);
1017     }
1019 add_hook("download_added_hook", open_download_buffer_automatically);