download manager: show display name, allow target file name to change
[conkeror.git] / modules / download-manager.js
blob3fd146a073dee5ec01d55887605a1f2fb4c04535
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; c: copy URL; " +
34                     (can_view_internally ? "i: view internally; t: view as text)" : ")"),
35                 $options = (can_view_internally ? ["s", "o", "O", "c", "i", "t"] : ["s", "o", "O", "c"]));
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_mime_type_external_handler(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_mime_type_external_handler(mime_type));
68                 shell_command_with_argument_blind(cmd, this.launcher.source.spec, $cwd = cwd);
69             } else if (action == "c") {
70                 action_chosen = true;
71                 this.abort(); // abort download
72                 let uri = this.launcher.source.spec;
73                 writeToClipboard(uri);
74                 this.window.minibuffer.message("Copied: " + uri);
75             } else /* if (action == "i" || action == "t") */ {
76                 let mime_type;
77                 if (action == "t")
78                     mime_type = "text/plain";
79                 else {
80                     let suggested_type = this.launcher.MIMEInfo.MIMEType;
81                     if (gecko_viewable_mime_type_list.indexOf(suggested_type) == -1)
82                         suggested_type = "text/plain";
83                     mime_type = yield this.window.minibuffer.read_gecko_viewable_mime_type(
84                         $prompt = "View internally as",
85                         $initial_value = suggested_type,
86                         $select);
87                 }
88                 action_chosen = true;
89                 this.abort(); // abort before reloading
91                 override_mime_type_for_next_load(this.launcher.source, mime_type);
92                 this.frame.location = this.launcher.source.spec; // reload
93             }
94         } catch (e) {
95             handle_interactive_error(this.window, e);
96         } finally {
97             if (!action_chosen)
98                 this.abort();
99             this.cleanup();
100         }
101     },
103     show : function (launcher, context, reason) {
104         this.launcher = launcher;
106         // Get associated buffer; if that fails (hopefully not), just get any window
107         var buffer = null;
108         var window = null;
109         var frame = null;
110         try {
111             frame = context.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowInternal);
112             window = get_window_from_frame(frame.top);
113             if (window)
114                 buffer = get_buffer_from_frame(window, frame);
115         } catch (e) {
116             window = get_recent_conkeror_window();
118             if (window == null) {
119                 // FIXME: need to handle this case perhaps where no windows exist
120                 this.abort(); // for now, just cancel the download
121                 return;
122             }
123         }
125         this.frame = frame;
126         this.window = window;
127         this.buffer = buffer;
129         co_call(this.handle_show());
130     },
132     abort : function () {
133         const NS_BINDING_ABORTED = 0x804b0002;
134         this.launcher.cancel(NS_BINDING_ABORTED);
135     },
137     cleanup : function () {
138         if (this.panel)
139             this.panel.destroy();
140         this.panel = null;
141         this.launcher = null;
142         this.window = null;
143         this.buffer = null;
144         this.frame = null;
145     },
147     promptForSaveToFile : function(launcher, context, default_file, suggested_file_extension) {
148         return null;
149     }
153 var unmanaged_download_info_list = [];
154 var id_to_download_info = {};
156 // Import these constants for convenience
157 const DOWNLOAD_NOTSTARTED = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
158 const DOWNLOAD_DOWNLOADING = Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING;
159 const DOWNLOAD_FINISHED = Ci.nsIDownloadManager.DOWNLOAD_FINISHED;
160 const DOWNLOAD_FAILED = Ci.nsIDownloadManager.DOWNLOAD_FAILED;
161 const DOWNLOAD_CANCELED = Ci.nsIDownloadManager.DOWNLOAD_CANCELED;
162 const DOWNLOAD_PAUSED = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
163 const DOWNLOAD_QUEUED = Ci.nsIDownloadManager.DOWNLOAD_QUEUED;
164 const DOWNLOAD_BLOCKED = Ci.nsIDownloadManager.DOWNLOAD_BLOCKED;
165 const DOWNLOAD_SCANNING = Ci.nsIDownloadManager.DOWNLOAD_SCANNING;
168 const DOWNLOAD_NOT_TEMPORARY = 0;
169 const DOWNLOAD_TEMPORARY_FOR_ACTION = 1;
170 const DOWNLOAD_TEMPORARY_FOR_COMMAND = 2;
172 function download_info(source_buffer, mozilla_info, target_file) {
173     this.source_buffer = source_buffer;
174     this.target_file = target_file;
175     if (mozilla_info != null)
176         this.attach(mozilla_info);
178 download_info.prototype = {
179     attach : function (mozilla_info) {
180         if (!this.target_file)
181             target_file = this.mozilla_info.targetFile;
182         else if (this.target_file.path != mozilla_info.targetFile.path)
183             throw interactive_error("Download target file unexpected.");
184         this.mozilla_info = mozilla_info;
185         id_to_download_info[mozilla_info.id] = this;
186         download_added_hook.run(this);
187     },
189     target_file : null,
191     shell_command : null,
193     shell_command_cwd : null,
195     temporary_status : DOWNLOAD_NOT_TEMPORARY,
197     action_description : null,
199     set_shell_command : function (str, cwd) {
200         this.shell_command = str;
201         this.shell_command_cwd = cwd;
202         if (this.mozilla_info)
203             download_shell_command_change_hook.run(this);
204     },
206     /**
207      * None of the following members may be used until attach is called
208      */
210     // Reflectors to properties of nsIDownload
211     get state () { return this.mozilla_info.state; },
212     get display_name () { return this.mozilla_info.displayName; },
213     get amount_transferred () { return this.mozilla_info.amountTransferred; },
214     get percent_complete () { return this.mozilla_info.percentComplete; },
215     get size () {
216         var s = this.mozilla_info.size;
217         /* nsIDownload.size is a PRUint64, and will have value
218          * LL_MAXUINT (2^64 - 1) to indicate an unknown size.  Because
219          * JavaScript only has a double numerical type, this value
220          * cannot be represented exactly, so 2^36 is used instead as the cutoff. */
221         if (s < 68719476736 /* 2^36 */)
222             return s;
223         return -1;
224     },
225     get source () { return this.mozilla_info.source; },
226     get start_time () { return this.mozilla_info.startTime; },
227     get speed () { return this.mozilla_info.speed; },
228     get MIME_info () { return this.mozilla_info.MIMEInfo; },
229     get MIME_type () {
230         if (this.MIME_info)
231             return this.MIME_info.MIMEType;
232         return null;
233     },
234     get id () { return this.mozilla_info.id; },
235     get referrer () { return this.mozilla_info.referrer; },
237     target_file_text : function() {
238         let target = this.target_file.path;
239         let display = this.display_name;
240         if (target.indexOf(display, target.length - display.length) == -1)
241             target += " (" + display + ")";
242         return target;
243     },
245     throw_if_removed : function () {
246         if (this.removed)
247             throw interactive_error("Download has already been removed from the download manager.");
248     },
250     throw_state_error : function () {
251         switch (this.state) {
252         case DOWNLOAD_DOWNLOADING:
253             throw interactive_error("Download is already in progress.");
254         case DOWNLOAD_FINISHED:
255             throw interactive_error("Download has already completed.");
256         case DOWNLOAD_FAILED:
257             throw interactive_error("Download has already failed.");
258         case DOWNLOAD_CANCELED:
259             throw interactive_error("Download has already been canceled.");
260         case DOWNLOAD_PAUSED:
261             throw interactive_error("Download has already been paused.");
262         case DOWNLOAD_QUEUED:
263             throw interactive_error("Download is queued.");
264         default:
265             throw new Error("Download has unexpected state: " + this.state);
266         }
267     },
269     // Download manager operations
270     cancel : function ()  {
271         this.throw_if_removed();
272         switch (this.state) {
273         case DOWNLOAD_DOWNLOADING:
274         case DOWNLOAD_PAUSED:
275         case DOWNLOAD_QUEUED:
276             try {
277                 download_manager_service.cancelDownload(this.id);
278             } catch (e) {
279                 throw interactive_error("Download cannot be canceled.");
280             }
281             break;
282         default:
283             this.throw_state_error();
284         }
285     },
287     retry : function () {
288         this.throw_if_removed();
289         switch (this.state) {
290         case DOWNLOAD_CANCELED:
291         case DOWNLOAD_FAILED:
292             try {
293                 download_manager_service.retryDownload(this.id);
294             } catch (e) {
295                 throw interactive_error("Download cannot be retried.");
296             }
297             break;
298         default:
299             this.throw_state_error();
300         }
301     },
303     resume : function () {
304         this.throw_if_removed();
305         switch (this.state) {
306         case DOWNLOAD_PAUSED:
307             try {
308                 download_manager_service.resumeDownload(this.id);
309             } catch (e) {
310                 throw interactive_error("Download cannot be resumed.");
311             }
312             break;
313         default:
314             this.throw_state_error();
315         }
316     },
318     pause : function () {
319         this.throw_if_removed();
320         switch (this.state) {
321         case DOWNLOAD_DOWNLOADING:
322         case DOWNLOAD_QUEUED:
323             try {
324                 download_manager_service.pauseDownload(this.id);
325             } catch (e) {
326                 throw interactive_error("Download cannot be paused.");
327             }
328             break;
329         default:
330             this.throw_state_error();
331         }
332     },
334     remove : function () {
335         this.throw_if_removed();
336         switch (this.state) {
337         case DOWNLOAD_FAILED:
338         case DOWNLOAD_CANCELED:
339         case DOWNLOAD_FINISHED:
340             try {
341                 download_manager_service.removeDownload(this.id);
342             } catch (e) {
343                 throw interactive_error("Download cannot be removed.");
344             }
345             break;
346         default:
347             throw interactive_error("Download is still in progress.");
348         }
349     },
351     delete_target : function () {
352         if (this.state != DOWNLOAD_FINISHED)
353             throw interactive_error("Download has not finished.");
354         try {
355             this.target_file.remove(false);
356         } catch (e) {
357             if ("result" in e) {
358                 switch (e.result) {
359                 case Cr.NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
360                     throw interactive_error("File has already been deleted.");
361                 case Cr.NS_ERROR_FILE_ACCESS_DENIED:
362                     throw interactive_error("Access denied");
363                 case Cr.NS_ERROR_FILE_DIR_NOT_EMPTY:
364                     throw interactive_error("Failed to delete file.");
365                 }
366             }
367             throw e;
368         }
369     }
372 var define_download_local_hook = simple_local_hook_definer();
374 // FIXME: add more parameters
375 function register_download(buffer, source_uri, target_file) {
376     var info = new download_info(buffer, null, target_file);
377     info.registered_time_stamp = Date.now();
378     info.registered_source_uri = source_uri;
379     unmanaged_download_info_list.push(info);
380     return info;
383 function match_registered_download(mozilla_info) {
384     let list = unmanaged_download_info_list;
385     let t = Date.now();
386     for (let i = 0; i < list.length; ++i) {
387         let x = list[i];
388         if (x.registered_source_uri == mozilla_info.source) {
389             list.splice(i, 1);
390             return x;
391         }
392         if (t - x.registered_time_stamp > download_info_max_queue_delay) {
393             list.splice(i, 1);
394             --i;
395             continue;
396         }
397     }
398     return null;
401 define_download_local_hook("download_added_hook");
402 define_download_local_hook("download_removed_hook");
403 define_download_local_hook("download_finished_hook");
404 define_download_local_hook("download_progress_change_hook");
405 define_download_local_hook("download_state_change_hook");
406 define_download_local_hook("download_shell_command_change_hook");
408 define_variable('delete_temporary_files_for_command', true,
409                 'If this is set to true, temporary files '
410                 + 'downloaded to run a command on them will be '
411                 + 'deleted once the command completes. If not, the '
412                 + 'file will stay around forever unless deleted '
413                 + 'outside the browser.');
415 var download_info_max_queue_delay = 100;
417 var download_progress_listener = {
418     QueryInterface: generate_QI(Ci.nsIDownloadProgressListener),
420     onDownloadStateChange : function (state, download) {
421         var info = null;
422         /* FIXME: Determine if only new downloads will have this state
423          * as their previous state. */
425         dumpln("download state change: " + download.source.spec + ": " + state + ", " + download.state + ", " + download.id);
427         if (state == DOWNLOAD_NOTSTARTED) {
428             info = match_registered_download(download);
429             if (info == null) {
430                 info = new download_info(null, download);
431                 dumpln("error: encountered unknown new download");
432             } else {
433                 info.attach(download);
434             }
435         } else {
436             info = id_to_download_info[download.id];
437             if (info == null) {
438                 dumpln("Error: encountered unknown download");
440             } else {
441                 info.mozilla_info = download;
442                 download_state_change_hook.run(info);
443                 if (info.state == DOWNLOAD_FINISHED) {
444                     download_finished_hook.run(info);
446                     if (info.shell_command != null) {
447                         info.running_shell_command = true;
448                         co_call(function () {
449                             try {
450                                 yield shell_command_with_argument(info.shell_command,
451                                                                   info.target_file.path,
452                                                                   $cwd = info.shell_command_cwd);
453                             } finally  {
454                                 if (info.temporary_status == DOWNLOAD_TEMPORARY_FOR_COMMAND)
455                                     if(delete_temporary_files_for_command) {
456                                         info.target_file.remove(false /* not recursive */);
457                                     }
458                                 info.running_shell_command = false;
459                                 download_shell_command_change_hook.run(info);
460                             }
461                         }());
462                         download_shell_command_change_hook.run(info);
463                     }
464                 }
465             }
466         }
467     },
469     onProgressChange : function (progress, request, cur_self_progress, max_self_progress,
470                                  cur_total_progress, max_total_progress,
471                                  download) {
472         var info = id_to_download_info[download.id];
473         if (info == null) {
474             dumpln("error: encountered unknown download in progress change");
475             return;
476         }
477         info.mozilla_info = download;
478         download_progress_change_hook.run(info);
479         //dumpln("download progress change: " + download.source.spec + ": " + cur_self_progress + "/" + max_self_progress + " "
480         // + cur_total_progress + "/" + max_total_progress + ", " + download.state + ", " + download.id);
481     },
483     onSecurityChange : function (progress, request, state, download) {
484     },
486     onStateChange : function (progress, request, state_flags, status, download) {
487     }
490 var download_observer = {
491     observe : function(subject, topic, data) {
492         switch(topic) {
493         case "download-manager-remove-download":
494             var ids = [];
495             if (!subject) {
496                 // Remove all downloads
497                 for (let i in id_to_download_info)
498                     ids.push(i);
499             } else {
500                 let id = subject.QueryInterface(Ci.nsISupportsPRUint32);
501                 /* FIXME: determine if this should really be an error */
502                 if (!(id in id_to_download_info)) {
503                     dumpln("Error: download-manager-remove-download event received for unknown download: " + id);
504                 } else
505                     ids.push(id);
506             }
507             for each (let i in ids) {
508                 dumpln("deleting download: " + i);
509                 let d = id_to_download_info[i];
510                 d.removed = true;
511                 download_removed_hook.run(d);
512                 delete id_to_download_info[i];
513             }
514             break;
515         }
516     }
518 observer_service.addObserver(download_observer, "download-manager-remove-download", false);
520 download_manager_service.addListener(download_progress_listener);
522 function pretty_print_file_size(val) {
523     const GIBI = 1073741824; /* 2^30 */
524     const MEBI = 1048576; /* 2^20 */
525     const KIBI = 1024; /* 2^10 */
526     var suffix, div;
527     if (val < KIBI) {
528         div = 1;
529         suffix = "B";
530     }
531     else if (val < MEBI) {
532         suffix = "KiB";
533         div = KIBI;
534     } else if (val < GIBI) {
535         suffix = "MiB";
536         div = MEBI;
537     } else {
538         suffix = "GiB";
539         div = GIBI;
540     }
541     val = val / div;
542     var precision = 2;
543     if (val > 10)
544         precision = 1;
545     if (val > 100)
546         precision = 0;
547     return [val.toFixed(precision), suffix];
550 function pretty_print_time(val) {
551     val = Math.round(val);
552     var seconds = val % 60;
554     val = Math.floor(val / 60);
556     var minutes = val % 60;
558     var hours = Math.floor(val / 60);
560     var parts = [];
562     if (hours > 1)
563         parts.push(hours + " hours");
564     else if (hours == 1)
565         parts.push("1 hour");
567     if (minutes > 1)
568         parts.push(minutes + " minutes");
569     else if (minutes == 1)
570         parts.push("1 minute");
572     if (minutes <= 1 && hours == 0) {
573         if (seconds != 1)
574             parts.push(seconds + " seconds");
575         else
576             parts.push("1 second");
577     }
579     return parts.join(", ");
582 define_variable(
583     "download_buffer_min_update_interval", 2000,
584     "Minimum interval (in milliseconds) between updates in download progress buffers.\n" +
585         "Lowering this interval will increase the promptness of the progress display at " +
586         "the cost of using additional processor time.");
588 define_keywords("$info");
589 function download_buffer(window, element) {
590     this.constructor_begin();
591     keywords(arguments);
592     special_buffer.call(this, window, element, forward_keywords(arguments));
593     this.info = arguments.$info;
594     this.configuration.cwd = this.info.mozilla_info.targetFile.parent.path;
595     this.description = this.info.mozilla_info.source.spec;
596     this.keymap = download_buffer_keymap;
597     this.update_title();
599     this.progress_change_handler_fn = method_caller(this, this.handle_progress_change);
600     add_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
601     add_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
602     this.command_change_handler_fn = method_caller(this, this.update_command_field);
603     add_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
604     this.constructor_end();
606 download_buffer.prototype = {
607     __proto__: special_buffer.prototype,
609     handle_kill : function () {
610         this.__proto__.__proto__.handle_kill.call(this);
611         remove_hook.call(this.info, "download_progress_change_hook", this.progress_change_handler_fn);
612         remove_hook.call(this.info, "download_state_change_hook", this.progress_change_handler_fn);
613         remove_hook.call(this.info, "download_shell_command_change_hook", this.command_change_handler_fn);
615         // Remove all node references
616         delete this.status_textnode;
617         delete this.target_file_node;
618         delete this.transferred_div_node;
619         delete this.transferred_textnode;
620         delete this.progress_container_node;
621         delete this.progress_bar_node;
622         delete this.time_textnode;
623         delete this.command_div_node;
624         delete this.command_label_textnode;
625         delete this.command_textnode;
626     },
628     update_title : function () {
629         // FIXME: do this properly
630         var new_title;
631         var info = this.info;
632         var append_transfer_info = false;
633         var append_speed_info = true;
634         var label = null;
635         switch(info.state) {
636         case DOWNLOAD_DOWNLOADING:
637             label = "Downloading";
638             append_transfer_info = true;
639             break;
640         case DOWNLOAD_FINISHED:
641             label = "Download complete";
642             break;
643         case DOWNLOAD_FAILED:
644             label = "Download failed";
645             append_transfer_info = true;
646             append_speed_info = false;
647             break;
648         case DOWNLOAD_CANCELED:
649             label = "Download canceled";
650             append_transfer_info = true;
651             append_speed_info = false;
652             break;
653         case DOWNLOAD_PAUSED:
654             label = "Download paused";
655             append_transfer_info = true;
656             append_speed_info = false;
657             break;
658         case DOWNLOAD_QUEUED:
659         default:
660             label = "Download queued";
661             break;
662         }
664         if (append_transfer_info) {
665             if (append_speed_info)
666                 new_title = label + " at " + pretty_print_file_size(info.speed).join(" ") + "/s: ";
667             else
668                 new_title = label + ": ";
669             var trans = pretty_print_file_size(info.amount_transferred);
670             if (info.size >= 0) {
671                 var total = pretty_print_file_size(info.size);
672                 if (trans[1] == total[1])
673                     new_title += trans[0] + "/" + total[0] + " " + total[1];
674                 else
675                     new_title += trans.join(" ") + "/" + total.join(" ");
676             } else
677                 new_title += trans.join(" ");
678             if (info.percent_complete >= 0)
679                 new_title += " (" + info.percent_complete + "%)";
680         } else
681             new_title = label;
682         if (new_title != this.title) {
683             this.title = new_title;
684             return true;
685         }
686         return false;
687     },
689     handle_progress_change : function () {
690         var cur_time = Date.now();
691         if (this.last_update == null ||
692             (cur_time - this.last_update) > download_buffer_min_update_interval ||
693             this.info.state != this.previous_state) {
695             if (this.update_title())
696                 buffer_title_change_hook.run(this);
698             if (this.generated) {
699                 this.update_fields();
700             }
701             this.previous_status = this.info.status;
702             this.last_update = cur_time;
703         }
704     },
706     generate: function() {
707         var d = this.document;
708         var g = new dom_generator(d, XHTML_NS);
710         /* Warning: If any additional node references are saved in
711          * this function, appropriate code to delete the saved
712          * properties must be added to handle_kill. */
714         var info = this.info;
716         d.body.setAttribute("class", "download-buffer");
718         g.add_stylesheet("chrome://conkeror-gui/content/downloads.css");
720         var div;
721         var label, value;
723         div = g.element("div", d.body, "class", "download-info", "id", "download-source");
724         label = g.element("div", div, "class", "download-label");
725         this.status_textnode = g.text("", label);
726         value = g.element("div", div, "class", "download-value");
727         g.text(info.source.spec, value);
729         div = g.element("div", d.body, "class", "download-info", "id", "download-target");
730         label = g.element("div", div, "class", "download-label");
731         var target_label;
732         if (info.temporary_status != DOWNLOAD_NOT_TEMPORARY)
733             target_label = "Temp. file:";
734         else
735             target_label = "Target:";
736         g.text(target_label, label);
737         value = g.element("div", div, "class", "download-value");
738         this.target_file_node = g.text("", value);
740         div = g.element("div", d.body, "class", "download-info", "id", "download-mime-type");
741         label = g.element("div", div, "class", "download-label");
742         g.text("MIME type:", label);
743         value = g.element("div", div, "class", "download-value");
744         g.text(info.MIME_type || "unknown", value);
746         this.transferred_div_node = div = g.element("div", d.body,
747                                                     "class", "download-info",
748                                                     "id", "download-transferred");
749         label = g.element("div", div, "class", "download-label");
750         g.text("Transferred:", label);
751         value = g.element("div", div, "class", "download-value");
752         this.transferred_textnode = g.text("", value);
753         this.progress_container_node = value = g.element("div", div, "id", "download-progress-container");
754         this.progress_bar_node = g.element("div", value, "id", "download-progress-bar");
755         value = g.element("div", div, "class", "download-value", "id", "download-percent");
756         this.percent_textnode = g.text("", value);
758         div = g.element("div", d.body, "class", "download-info", "id", "download-time");
759         label = g.element("div", div, "class", "download-label");
760         g.text("Time:", label);
761         value = g.element("div", div, "class", "download-value");
762         this.time_textnode = g.text("", value);
764         if (info.action_description != null) {
765             div = g.element("div", d.body, "class", "download-info", "id", "download-action");
766             label = g.element("div", div, "class", "download-label");
767             g.text("Action:", label);
768             value = g.element("div", div, "class", "download-value");
769             g.text(info.action_description, value);
770         }
772         this.command_div_node = div = g.element("div", d.body, "class", "download-info", "id", "download-command");
773         label = g.element("div", div, "class", "download-label");
774         this.command_label_textnode = g.text("Run command:", label);
775         value = g.element("div", div, "class", "download-value");
776         this.command_textnode = g.text("", value);
778         this.update_fields();
780         this.update_command_field();
781     },
783     update_fields : function () {
784         if (!this.generated)
785             return;
786         var info = this.info;
787         var label = null;
788         switch(info.state) {
789         case DOWNLOAD_DOWNLOADING:
790             label = "Downloading";
791             break;
792         case DOWNLOAD_FINISHED:
793             label = "Completed";
794             break;
795         case DOWNLOAD_FAILED:
796             label = "Failed";
797             break;
798         case DOWNLOAD_CANCELED:
799             label = "Canceled";
800             break;
801         case DOWNLOAD_PAUSED:
802             label = "Paused";
803             break;
804         case DOWNLOAD_QUEUED:
805         default:
806             label = "Queued";
807             break;
808         }
809         this.status_textnode.nodeValue = label + ":";
810         this.target_file_node.nodeValue = info.target_file_text();
811         this.update_time_field();
813         var tran_text = "";
814         if (info.state == DOWNLOAD_FINISHED)
815             tran_text = pretty_print_file_size(info.size).join(" ");
816         else {
817             var trans = pretty_print_file_size(info.amount_transferred);
818             if (info.size >= 0) {
819                 var total = pretty_print_file_size(info.size);
820                 if (trans[1] == total[1])
821                     tran_text += trans[0] + "/" + total[0] + " " + total[1];
822                 else
823                     tran_text += trans.join(" ") + "/" + total.join(" ");
824             } else
825                 tran_text += trans.join(" ");
826         }
827         this.transferred_textnode.nodeValue = tran_text;
828         if (info.percent_complete >= 0) {
829             this.progress_container_node.style.display = "";
830             this.percent_textnode.nodeValue = info.percent_complete + "%";
831             this.progress_bar_node.style.width = info.percent_complete + "%";
832         } else {
833             this.percent_textnode.nodeValue = "";
834             this.progress_container_node.style.display = "none";
835         }
837         this.update_command_field();
838     },
840     update_time_field : function () {
841         var info = this.info;
842         var elapsed_text = pretty_print_time((Date.now() - info.start_time / 1000) / 1000) + " elapsed";
843         var text = "";
844         if (info.state == DOWNLOAD_DOWNLOADING) {
845             text = pretty_print_file_size(info.speed).join(" ") + "/s, ";
846         }
847         if (info.state == DOWNLOAD_DOWNLOADING &&
848             info.size >= 0 &&
849             info.speed > 0) {
850             let remaining = (info.size - info.amount_transferred) / info.speed;
851             text += pretty_print_time(remaining) + " left (" + elapsed_text + ")";
852         } else {
853             text = elapsed_text;
854         }
855         this.time_textnode.nodeValue = text;
856     },
858     update_command_field : function () {
859         if (!this.generated)
860             return;
861         if (this.info.shell_command != null) {
862             this.command_div_node.style.display = "";
863             var label;
864             if (this.info.running_shell_command)
865                 label = "Running:";
866             else if (this.info.state == DOWNLOAD_FINISHED)
867                 label = "Ran command:";
868             else
869                 label = "Run command:";
870             this.command_label_textnode.nodeValue = label;
871             this.command_textnode.nodeValue = this.info.shell_command;
872         } else {
873             this.command_div_node.style.display = "none";
874         }
875     }
878 function download_cancel(buffer) {
879     check_buffer(buffer, download_buffer);
880     var info = buffer.info;
881     info.cancel();
882     buffer.window.minibuffer.message("Download canceled");
884 interactive("download-cancel",
885             "Cancel the current download.\n" +
886             "The download can later be retried using the `download-retry' command, but any " +
887             "data already transferred will be lost.",
888             function (I) {download_cancel(I.buffer);});
890 function download_retry(buffer) {
891     check_buffer(buffer, download_buffer);
892     var info = buffer.info;
893     info.retry();
894     buffer.window.minibuffer.message("Download retried");
896 interactive("download-retry",
897             "Retry a failed or canceled download.\n" +
898             "This command can be used to retry a download that failed or was cancled using " +
899             "the `download-cancel' command.  The download will begin from the start again.",
900             function (I) {download_retry(I.buffer);});
902 function download_pause(buffer) {
903     check_buffer(buffer, download_buffer);
904     buffer.info.pause();
905     buffer.window.minibuffer.message("Download paused");
907 interactive("download-pause",
908             "Pause the current download.\n" +
909             "The download can later be resumed using the `download-resume' command.  The " +
910             "data already transferred will not be lost.",
911             function (I) {download_pause(I.buffer);});
913 function download_resume(buffer) {
914     check_buffer(buffer, download_buffer);
915     buffer.info.resume();
916     buffer.window.minibuffer.message("Download resumed");
918 interactive("download-resume",
919             "Resume the current download.\n" +
920             "This command can be used to resume a download paused using the `download-pause' command.",
921             function (I) {download_resume(I.buffer);});
923 function download_remove(buffer) {
924     check_buffer(buffer, download_buffer);
925     buffer.info.remove();
926     buffer.window.minibuffer.message("Download removed");
928 interactive("download-remove",
929             "Remove the current download from the download manager.\n" +
930             "This command can only be used on inactive (paused, canceled, completed, or failed) downloads.",
931             function (I) {download_remove(I.buffer);});
933 function download_retry_or_resume(buffer) {
934     check_buffer(buffer, download_buffer);
935     var info = buffer.info;
936     if (info.state == DOWNLOAD_PAUSED)
937         download_resume(buffer);
938     else
939         download_retry(buffer);
941 interactive("download-retry-or-resume",
942             "Retry or resume the current download.\n" +
943             "This command can be used to resume a download paused using the `download-pause' " +
944             "command or canceled using the `download-cancel' command.",
945             function (I) {download_retry_or_resume(I.buffer);});
947 function download_pause_or_resume(buffer) {
948     check_buffer(buffer, download_buffer);
949     var info = buffer.info;
950     if (info.state == DOWNLOAD_PAUSED)
951         download_resume(buffer);
952     else
953         download_pause(buffer);
955 interactive("download-pause-or-resume",
956             "Pause or resume the current download.\n" +
957             "This command toggles the paused state of the current download.",
958             function (I) {download_pause_or_resume(I.buffer);});
960 function download_delete_target(buffer) {
961     check_buffer(buffer, download_buffer);
962     var info = buffer.info;
963     info.delete_target();
964     buffer.window.minibuffer.message("Deleted file: " + info.target_file.path);
966 interactive("download-delete-target",
967             "Delete the target file of the current download.\n"  +
968             "This command can only be used if the download has finished successfully.",
969             function (I) {download_delete_target(I.buffer);});
971 function download_shell_command(buffer, cwd, cmd) {
972     check_buffer(buffer, download_buffer);
973     var info = buffer.info;
974     if (info.state == DOWNLOAD_FINISHED) {
975         shell_command_with_argument_blind(cmd, info.target_file.path, $cwd = cwd);
976         return;
977     }
978     if (info.state != DOWNLOAD_DOWNLOADING && info.state != DOWNLOAD_PAUSED && info.state != DOWNLOAD_QUEUED)
979         info.throw_state_error();
980     if (cmd == null || cmd.length == 0)
981         info.set_shell_command(null, cwd);
982     else
983         info.set_shell_command(cmd, cwd);
984     buffer.window.minibuffer.message("Queued shell command: " + cmd);
986 interactive("download-shell-command",
987             "Run a shell command on the target file of the current download.\n" +
988             "If the download is still in progress, the shell command will be queued " +
989             "to run when the download finishes.",
990             function (I) {
991                 var buffer = check_buffer(I.buffer, download_buffer);
992                 var cwd = buffer.info.shell_command_cwd || buffer.cwd;
993                 var cmd = yield I.minibuffer.read_shell_command(
994                     $cwd = cwd,
995                     $initial_value = buffer.info.shell_command ||
996                         get_mime_type_external_handler(buffer.info.MIME_type));
997                 download_shell_command(buffer, cwd, cmd);
998             });
1000 function download_manager_ui()
1002 download_manager_ui.prototype = {
1003     QueryInterface : XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI]),
1005     getAttention : function () {},
1006     show : function () {},
1007     visible : false
1011 function download_manager_show_builtin_ui(window) {
1012     download_manager_builtin_ui.show(window);
1014 interactive("download-manager-show-builtin-ui",
1015             "Show the built-in (Firefox-style) download manager user interface.",
1016             function (I) {download_manager_show_builtin_ui(I.window);});
1020 define_variable("download_temporary_file_open_buffer_delay", 500,
1021                      "Delay (in milliseconds) before a download buffer is opened for temporary downloads.\n" +
1022                      "This variable takes effect only if `open_download_buffer_automatically' is in " +
1023                      "`download_added_hook', as it is by default.");
1026 define_variable("download_buffer_automatic_open_target", OPEN_NEW_WINDOW,
1027                      "Target for download buffers created by the `open_download_buffer_automatically' function.\n" +
1028                      "This variable takes effect only if `open_download_buffer_automatically' is in " +
1029                      "`download_added_hook', as it is by default.");
1031 function open_download_buffer_automatically(info) {
1032     var buf = info.source_buffer;
1033     var target = download_buffer_automatic_open_target;
1034     if (buf == null)
1035         target = OPEN_NEW_WINDOW;
1036     if (info.temporary_status == DOWNLOAD_NOT_TEMPORARY ||
1037         !(download_temporary_file_open_buffer_delay > 0))
1038         create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
1039     else {
1040         var timer = null;
1041         function finish() {
1042             timer.cancel();
1043         }
1044         add_hook.call(info, "download_finished_hook", finish);
1045         timer = call_after_timeout(function () {
1046                 remove_hook.call(info, "download_finished_hook", finish);
1047                 create_buffer(buf.window, buffer_creator(download_buffer, $info = info), target);
1048             }, download_temporary_file_open_buffer_delay);
1049     }
1051 add_hook("download_added_hook", open_download_buffer_automatically);