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